Отправлять данные в несколько сокетов с помощью труб, tee() и splice()

Я дублирую трубу "master" с tee() для записи в несколько сокетов с помощью splice(). Естественно, эти трубы будут опустошаться с разной скоростью, в зависимости от того, сколько я могу сращить() до сокетов назначения. Поэтому, когда я перехожу к добавлению данных в "главный" канал, а затем снова к tee(), у меня может возникнуть ситуация, когда я могу писать 64 Кбайт в трубку, но только тройник 4 КБ в один из "подчиненных" труб. Я предполагаю, что если я подключу() весь "главный" канал к сокету, я никогда не смогу использовать() оставшиеся 60 КБ для этого подчиненного канала. Это правда? Наверное, я могу отслеживать tee_offset (начиная с 0), который я установил в начало "unteed", а затем не склеиваю() мимо него. Таким образом, в этом случае я бы установил tee_offset в 4096 и не объединил больше, чем до тех пор, пока не смогу использовать его всем другим каналам. Я здесь, на правильном пути? Какие-нибудь советы/предупреждения для меня?

Ответ 1

Если я правильно понимаю, у вас есть источник данных в реальном времени, которые вы хотите мультиплексировать на несколько сокетов. У вас есть единственный "исходный" канал, подключенный к тому, что производит ваши данные, и у вас есть "целевой" канал для каждого сокета, по которому вы хотите отправить данные. То, что вы делаете, используется tee() для копирования данных из исходного канала в каждый из каналов назначения и splice(), чтобы скопировать его из целевых каналов в сами сокеты.

Основная проблема, с которой вы собираетесь столкнуться, - это то, что один из сокетов просто не может справиться - если вы создаете данные быстрее, чем можете отправить их, тогда у вас будет проблема. Это не связано с использованием труб, это всего лишь фундаментальная проблема. Итак, вы захотите выбрать стратегию, чтобы справиться в этом случае - я предлагаю обработать это, даже если вы не ожидаете, что это будет распространено, поскольку эти вещи часто приходят, чтобы укусить вас позже. Ваш основной выбор состоит в том, чтобы закрыть закрывающий сокет или пропустить данные до тех пор, пока он не очистит свой выходной буфер. Последний выбор может быть более подходящим для потоковой передачи аудио/видео, например.

Однако проблема, связанная с использованием вами труб, заключается в том, что в Linux размер буфера канала несколько негибкий. По умолчанию он равен 64 КБ с Linux 2.6.11 (вызов tee() был добавлен в 2.6.17) - см. man man . Начиная с версии 2.6.35 это значение можно изменить с помощью опции F_SETPIPE_SZ на fcntl() (см. fcntl manpage) до предела, указанного /proc/sys/fs/pipe-size-max, но буферизация еще более неудобна для изменения по требованию, чем динамически распределенная схема в пользовательском пространстве. Это означает, что ваша способность справляться с медленными сокетами будет несколько ограничена - будет ли это приемлемо, зависит от скорости, с которой вы ожидаете получить и сможете отправлять данные.

Предполагая, что эта стратегия буферизации приемлема, вы правы в своем предположении, что вам нужно будет отслеживать, сколько данных потребляет каждый канал назначения из источника, и это безопасно только для удаления данных, которые потребляли все каналы назначения. Это несколько осложняется тем фактом, что tee() не имеет понятия смещения - вы можете копировать только с начала канала. Следствием этого является то, что вы можете копировать только со скоростью самого медленного сокета, так как вы не можете использовать tee() для копирования в канал назначения, пока некоторые из данных не будут использованы из источника, и вы не можете сделайте это, пока все сокеты не будут иметь данные, которые вы собираетесь использовать.

Как вы справляетесь с этим, это зависит от важности ваших данных. Если вам действительно нужна скорость tee() и splice(), и вы уверены, что медленный сокет будет чрезвычайно редким событием, вы можете сделать что-то вроде этого (я предположил, что вы используете неблокирующий IO и один поток, но что-то подобное также будет работать с несколькими потоками):

  • Убедитесь, что все каналы не блокируются (используйте fcntl(d, F_SETFL, O_NONBLOCK), чтобы каждый дескриптор файла не блокировался).
  • Инициализировать переменную read_counter для каждого канала назначения до нуля.
  • Используйте что-то вроде epoll(), чтобы подождать, пока что-нибудь в исходном канале.
  • Петля по всем каналам назначения, где read_counter равна нулю, вызывая tee() для передачи данных каждому из них. Убедитесь, что вы проходите SPLICE_F_NONBLOCK в флагах.
  • Приращение read_counter для каждого канала назначения на сумму, переданную tee(). Следите за самым низким итоговым значением.
  • Найдите наименьшее результирующее значение read_counter - если оно отличное от нуля, то отбросьте этот объем данных из исходного канала (например, с помощью вызова splice() с адресатом, открытым на /dev/null)). После отбрасывания данных вычитайте количество, отбрасываемое с read_counter на всех трубах (так как это было самое низкое значение, это не может привести к тому, что любой из них станет отрицательным).
  • Повторите с шага 3.

Примечание. Одна вещь, которая помогла мне в прошлом, заключалась в том, что SPLICE_F_NONBLOCK влияет на то, что операции tee() и splice() в трубах не блокируются, а O_NONBLOCK, установленный с помощью fnctl(), влияет на являются ли взаимодействия с другими вызовами (например, read() и write()) неблокирующими. Если вы хотите, чтобы все было неблокирующим, установите оба параметра. Также не забудьте сделать ваши сокеты неблокируемыми или вызовы splice() для передачи данных им могут блокироваться (если только это не требуется, если вы используете поточный подход).

Как вы можете видеть, эта стратегия имеет серьезную проблему - как только один сокет блокируется, все останавливается - канал назначения для этого сокета будет заполняться, а затем исходный канал застаивается. Итак, если вы достигнете стадии, на которой tee() возвращает EAGAIN на шаге 4, вам нужно либо закрыть этот сокет, либо, по крайней мере, "отключить" его (т.е. Вытащить его из своего loop), так что вы ничего не пишете, пока его выходной буфер не будет пустым. Выбор, который вы выбираете, зависит от того, может ли ваш поток данных восстанавливаться из-за того, что его бит пропущен.

Если вы хотите более грамотно справляться с латентностью сети, тогда вам понадобится сделать больше буферизации, и это будет включать в себя либо буферы пользовательского пространства (что скорее отрицает преимущества tee() и splice()) или, возможно, дисковый буфер. Буферизация на основе диска почти наверняка будет значительно медленнее, чем буферизация пользовательского пространства, и, следовательно, не подходит, учитывая, что, по-видимому, вы хотите получить большую скорость, так как вы выбрали tee() и splice() в первую очередь, но я упоминаю это для полноты.

Одна вещь, которая стоит отметить, если вы в конечном итоге вставляете данные из пользовательского пространства в любой момент, это вызов vmsplice(), который может выполнять "сбор данных" из пользовательского пространства в канал, аналогично writev() вызов. Это может быть полезно, если вы делаете достаточно буферизации, что вы разделили свои данные между несколькими разными выделенными буферами (например, если вы используете подход распределения пула).

Наконец, вы могли бы представить себе подкачки сокетов между "быстрой" схемой использования tee() и splice() и, если они не успевают за ним, переместить их на более медленную буферизацию пользовательского пространства. Это усложнит вашу реализацию, но если вы обрабатываете большое количество подключений, и только очень небольшая часть из них медленная, вы все равно уменьшаете количество копий на пользовательское пространство, которое в некоторой степени связано. Тем не менее, это будет только когда-либо краткосрочной мерой для решения проблем переходных сетей. Как я уже сказал, у вас есть фундаментальная проблема, если ваши сокеты медленнее вашего источника. В конечном итоге вы нажмете ограничение на буферизацию и должны пропустить данные или закрыть соединения.

В целом я бы тщательно рассмотрел, почему вам нужна скорость tee() и splice(), и будет ли более целесообразным просто использовать буферизацию пользовательского пространства в памяти или на диске. Если вы уверены, что скорости всегда будут высокими, и допустимая буферизация приемлема, то описанный выше подход должен работать.

Кроме того, я должен упомянуть, что это сделает ваш код чрезвычайно специфичным для Linux - я не знаю, как эти вызовы поддерживаются в других вариантах Unix. Вызов sendfile() более ограничен, чем splice(), но может быть более переносимым. Если вы действительно хотите, чтобы вещи были переносимыми, придерживайтесь буферизации пользовательского пространства.

Сообщите мне, есть ли что-нибудь, что я рассмотрел, о котором вы хотели бы подробнее узнать.