Комментарии 14
Если бы только существовал транспортный протокол со встроенной поддержкой мультиплексирования, мультихоуминга, настройки приоритетов каналов, настройки гарантий доставки и очередности… Можно было бы назвать его как-то типа SCTP.
Если это сарказм по поводу непопулярности SCTP, то он не по адресу: SCTP не даёт возможности регулировать отдельные потоки и потому не защищает от HOL blocking. Всё, что дают его потоки, это тегирование. Но такое же тегирование можно устроить и внутри одного потока с таким же успехом и чуть бо́льшими накладными расходами в юзерленде. Если не согласны — покажите, как можно сказать на приёмной стороне выбрать только из конкретного потока, а не первое из того, что пришло, и задавать по каждому потоку своё окно приёма. Я долго искал, не нашёл нигде. В самом протоколе описано, что acknowledge присылается на TSN, а не по каждому потоку раздельно.
Да, у него есть некоторые другие плюшки типа ограниченных попыток передать сообщение… но к описываемой в исходной статье задаче это не имеет никакого отношения.
Неудивительно, что за 20 лет существования SCTP так и не вышел за пределы "пары инвалидов", и облачные провайдеры его знать не желают.
не даёт возможности регулировать отдельные потоки и потому не защищает от HOL blocking. Всё, что дают его потоки, это тегирование
Пофикшено в RFC 8260 чанками I-DATA. Это 2017 год, после SPDY, но всё же до QUIC.
С остальным верно, поэтому я начал разрабатывать свой muSCTP :) Однако, "выбрать только из конкретного потока" плохо ложится как на BSD Sockets API, так и на struct sockbuf в ядре...
что acknowledge присылается на TSN, а не по каждому потоку раздельно.
Тут спорный момент. Когда потоков много, общий ACK будет гораздо эффективнее, а при правильной реализации это мешать не должно.
Однако, "выбрать только из конкретного потока" плохо ложится как на BSD Sockets API, так и на struct sockbuf в ядре...
BSD sockets в базовом виде и так не дают всего, что может SCTP, и есть поэтому свой специфичный набор вызовов. Ещё один добавить - не проблема. Или можно было бы сделать, например, дочерние fd, привязанные к конкретному потоку, чтобы над ними уже вызывать хоть простейший read().
Про struct sockbuf я не в курсе, что там не так.
И если userland API нет для этой возможности, то уже неважно, есть там I-DATA или нет. Я ещё не прочитал RFC, если там что-то не понравится - выскажу отдельно.
Когда потоков много, общий ACK будет гораздо эффективнее, а при правильной реализации это мешать не должно.
А она тут в принципе может быть, эта "правильная" реализация, если у нас вот сейчас настроение выбрать всё из потока 9 не выбирая остальных, даже если для них приёмные буфера забиты?
Про struct sockbuf я не в курсе, что там не так. Или можно было бы сделать, например, дочерние fd, привязанные к конкретному потоку, чтобы над ними уже вызывать хоть простейший read().
Неточно выразился. В структуре сокета в ядре предусмотрено только два sockbuf - на прием и отправку. Возможно, именно поэтому SCTP и не стал делать раздельного чтения потоков, ибо дочерние fd выглядят коряво - да и делался он еше когда существовали еще только лишь select()
и poll()
. Впрочем, с kqueue() оно тоже в уже существующих не ложится идеально в семантику, как раз вот только обсуждал проблему:
https://lists.freebsd.org/archives/freebsd-hackers/2025-July/004749.html
ещё не прочитал RFC, если там что-то не понравится - выскажу отдельно.
Да там ничего особо выдающегося, просто устранили эту проблему архитектуры.
А она тут в принципе может быть, эта "правильная" реализация, если у нас вот сейчас настроение выбрать всё из потока 9 не выбирая остальных, даже если для них приёмные буфера забиты?
Так это ж разные уровни - ACK оно по факту приема пакета, который будет положен в буферы сокета (и его нельзя не акнуть, ибо иначе congestion control вскинется), а уже потом, позже, приложение будет решать, какой вычитать. Допустим, оно дало общий буфер сокета на много мегабайтов, а сообщения по пять кило, так что для отправителя можно акать дальше. Ах да, стримам-то необязательно быть либо "как в SCTP" либо "как отдельные TCP" - на практике-то захочется иметь иерархию стримов (даже в HTTP/2 её делали), как в SST, где ребенок borrow'ит окно у родителя, а тот у бабушки... В общем, большая сложная тема, я уже давно пишу черновики https://github.com/nuclight/musctp
ибо дочерние fd выглядят коряво
Коряво, угу. Но работает и имеет свои преимущества. Да, я видел твои замечания про линуксовый подход с xxxfd_create(), но согласись, что в этом случае резко упрощается учёт потраченных ресурсов, для всех сторон. В случае kevent, как увидеть количество, например, таймеров, навешенных на одну конкретную kqueue, процессов, и т.д.? Видеть их всех в виде дескрипторов - просто и достаточно надёжно, то есть вполне себе юниксовый подход "всё есть файл".
(Мне интересно, сколько вообще ведутся споры на эту тему? С 2000, когда появилась kqueue?)
просто устранили эту проблему архитектуры.
Я увидел пассаж:
An endpoint implementing the socket API specified in [RFC6458] MUST NOT indicate user message interleaving support unless the user has requested its use (e.g., via the socket API; see Section 4.3). This constraint is made since the usage of this chunk requires that the application is capable of handling interleaved messages upon reception within an association.
То есть мало того что переложили на юзера, заставив его агрегировать чанки, так ещё и всё равно не дали регулировки по потокам.
Я тут, конечно, ужален проблемами одного почти провального проекта, где из-за аналогичной глупости в Erlang оказалось практически нереально обеспечить стабильный поток проходящих данных, и дую на воду, но для меня теперь вариант "хлебай что дают из пожарного шланга", который реализуется таким подходом, не подходит для чего-то серьёзнее песочницы. (Там было ещё хуже: один вход у "процесса" на всё включая канал регулировки. Тут хотя бы можно назад передавать сообщения типа "добавляю окно на 100кбайт для потока 8" и они могут быть прочтены вперёд собственно данных.)
Так это ж разные уровни - ACK оно по факту приема пакета, который будет положен в буферы сокета (и его нельзя не акнуть, ибо иначе congestion control вскинется), а уже потом, позже, приложение будет решать, какой вычитать.
Если один буфер на все потоки - это уже не то, что нужно. Нужны раздельные.
Повторюсь, если этого нет, то фактически нет разницы с тем, что просто в одном потоке тегировали каждое сообщение ещё и одним байтиком перед ним (или в userdata уложили тот тег). Заодно и деление на чанки точно так же делается на уровне юзера, в пределах того же соединения.
А вот реально раздельное управление потоком по всем потокам (русский тут чего-то зажат в омонимию, per-stream flow control) ты на одном соединении не сделаешь, тут что TCP что SCTP надо сейчас несколько порождать, а тогда ещё и заботиться, чтобы пришло именно на нужный серверный процесс, указать серверу, что это соединение надо ставить в комплект вот этому... марудно. Вот потому, я так понимаю, и стали запускать SPDY с потомками - когда поняли, что от последней попытки (SCTP) ждать хорошего всё равно нельзя.
на практике-то захочется иметь иерархию стримов (даже в HTTP/2 её делали), как в SST, где ребенок borrow'ит окно у родителя, а тот у бабушки...
Я честно не понял ещё смысла в этом навороте. Поищу при случае.
Заметки тоже почитаю.
но согласись, что в этом случае резко упрощается учёт потраченных ресурсов, для всех сторон. В случае kevent, как увидеть количество, например, таймеров, навешенных на одну конкретную kqueue, процессов, и т.д.? Видеть их всех в виде дескрипторов - просто и достаточно надёжно, то есть вполне себе юниксовый подход "всё есть файл".
Хм, не думал с этой стороны, аргумент (правда, среди fd этот учет самому надо вести, нет ведь енумераторных сисколлов). Однако тут, конечно, уши примитивности юниксового подхода торчат (доведенного в Plan9 до абсурда) - в винде были бы соответствующие API для получения, они не связаны абстракцией файла, ну и какой-нибудь WaitForMultipleObjects в качестве образца для kqueue мог бы еще и мьютексы ждать, например. С другой стороны, местами изначальные архитектурные косяки юникса всё же потихоньку правят - то переход на libxo начали, то вот недавно расширенные errno втащили (второй аргумент конечно лимитирован размером, но всё же большой шаг вперёд)...
То есть мало того что переложили на юзера, заставив его агрегировать чанки, так ещё и всё равно не дали регулировки по потокам.
Ну а что ты хочешь, backward compatibility, я ж сказал - ничего выдающегося не добавили, просто из "совсем жопы" сделали "так себе" - по крайней мере HoL от потери пакетов оно в ряде случаев устранит.
Я тут, конечно, ужален проблемами одного почти провального проекта, где из-за аналогичной глупости в Erlang оказалось практически нереально обеспечить стабильный поток проходящих данных, и дую на воду, но для меня теперь вариант "хлебай что дают из пожарного шланга", который реализуется таким подходом, не подходит для чего-то серьёзнее песочницы.
О, я тут последние полмесяца постом «Что не так с ООП в 2025» и комментами под ним (жаркий срач вышел) сподвигся наконец осилить туториал по Эрлангу. Честно говоря, он меня разочаровал - написано плохо, без объяснения многих моментов, даже строк нет, функицональщина (иммутабельность) непонятно зачем, как-то коряво выглядит. Так что дочитывал я уже без запуска примеров. Меня просто привлекает акторная модель (на самом деле изначальное ООП с сообщениями, да хоть netgraph, ну ты в курсе), а автор поста заявил, что этот позволяет обходиться без мьютексов. А у меня в мета-проекте, кроме протокола, еще задача специализированного безопасного языка, который можно было бы сунуть в ядро или передать на удаленный сервер - и вот познакомившись с eBPF/XDP, долго плевался, багу в шланге заводил, нифига оно толком непригодное было для задачи системы от защиты от DDoS-атак, которая у меня стояла (там даже чистку conntrack единым таймером не сделать, ни к черту вообще). Даже начинал собственный BPF64, но потом бросил - подход ассемблеров, похоже, в корне ошибочный.
В общем, я спросил там автора поста, как бы он делал систему на сто миллионов в conntrack'е акторной моделью (у меня в голове, конечно, кластер машин по типу pfsync/carp но с И отказоустойчивостью, И балансировкой нагрузки), челлендж ему понравился, но ответа пока нет :)
Приходи туда тоже, будет интересно почитать про эти проблемы эрланга детальнее с теми, кто его ел.
(Там было ещё хуже: один вход у "процесса" на всё включая канал регулировки. Тут хотя бы можно назад передавать сообщения типа "добавляю окно на 100кбайт для потока 8" и они могут быть прочтены вперёд собственно данных.)
Что, и тут всё плохо? Там рекламировали Akka.NET, я посмотрел их сайт, бросил как раз после описаний TCP-взаимодействия, имён мэйлбоксов и т.д. Мне-то больше подход MQTT по душе (он кстати в 5.0 вполне взрослый для хайлоадного RPC - response topics, все дела). Видимо, опять придется посмотреть у функциональщиков (в данном случае эрлангив) способы реализации и делать на более мейнстримных языках фреймворки...
Если один буфер на все потоки - это уже не то, что нужно. Нужны раздельные.
Нет, это зависит от приложения. Даже в QUIC у тебя не один, а два типа присылаемых апдейтов буфера - не только на конкретный поток, но и на всё соединение. Ну ниже рассмотрим.
Повторюсь, если этого нет, то фактически нет разницы с тем, что просто в одном потоке тегировали каждое сообщение ещё и одним байтиком перед ним (или в userdata уложили тот тег). Заодно и деление на чанки точно так же делается на уровне юзера, в пределах того же соединения.
Это для программиста приложения разницы нет, и то с точки зрения написания кода, а не поведения - а с точки зрения транспортного уровня разница колоссальная. Перенос стримов и фрейминга с уровня приложения на L4 позволяет добиться очень многого - во-первых, тот самый HoL, во-вторых - и это куда более важно - транспортный уровень может оптимизировать доставку, зная, сколько в этом сообщении будет байт, и тот факт, что получателю нет смысла получать сообщение по кускам. Первым делом, это multipath - сколько с ним бьются в TCP, всё без толку, QUIC не стал и пытаться. А с делением на сообщения мы просто берем - при установившихся стабильных SRTT, RTTvar, cwnd - и по алгоритму из Multilink PPP отправляем куски по разным путям с их скоростями так, что они приезжают вместе. Далее, извечная проблема tcp reordering, которой вообще-то быть не должно в принципе, а мы должны иметь возможность делать round-robin balancer'ы - дык вот, я пока что думаю, что она решается очень простым алгоритмом получателя "если есть дыра, и самый свежий прибывший чанк не последний в сообщении (нет флага E), то задержать SACK на величину RTTvar отправителя". А свой RTTvar он нам регулярно сообщает (обо всём этот в QUIC, конечно, не подумали, да там даже unordered нет...). Уже эти две вещи дадут гигантский прогресс, кмк.
А вот реально раздельное управление потоком по всем потокам (русский тут чего-то зажат в омонимию, per-stream flow control) ты на одном соединении не сделаешь, тут что TCP что SCTP надо сейчас несколько порождать, а тогда ещё и заботиться, чтобы пришло именно на нужный серверный процесс, указать серверу, что это соединение надо ставить в комплект вот этому... марудно. Вот потому, я так понимаю, и стали запускать SPDY с потомками - когда поняли, что от последней попытки (SCTP) ждать хорошего всё равно нельзя.
Да, это иронично (впрочем чего ожидать от телефонистов?..), только Гугл показал, что не умеет в протоколы. Оно как бы не хуже SCTP вышло-то. Особенно вот этот цирк с неумением в sequence number arithmentics с компрессией номеров пакетов, которым легко устроить DoS пира (гугл толстый, ему всё равно, а вот остальным...)
Я честно не понял ещё смысла в этом навороте. Поищу при случае.
Ну см. на https://pdos.csail.mit.edu/archive/uia/sst/ картинку - она красивая :) Хочу, например, костыль SSH для stderr обобщить просто дочерними потоками в дереве. Как минимум, вновь открываемый поток должен borrow'ить окно у своего родителя - иначе нам что, бесконечно буфера добавлять? Правда, дальше этой идеи они не специфицировали, увы :( Да, в имеющихся системах, что QUIC, что SSH2, имеют на каждый поток отдельное окно - потому что так проще в реализации. Но моя техническая интуиция говорит мне, что нужно более комплексное решение - и в HTTP/2 например пытались же делать дерево потоков. В конце концов, если в названии протокола Stream Control, надо соответствовать :) а "просто как пачка tcp" на это не тянет - в конце концов, при общем congestion control мы можем использовать эффективнее, хоть WFQ, и должны это делать в условиях ограничений, на которых мой протокол пытается выживать, например... собственно, вот да - допустим у тебя сто или тысяча потоков, обновлять rwnd каждого будет неприлично chatty, если этого не требуется, а полоса scarce.
Попробую придумать пример... Ну, поскольку делается оно для помеси мессенджера с соцсетями, возьмем в пример Telegram. Допустим, мы чатимся, и в это время слушается музыка и качаются файлы. Понятно, приоритет отдадим тексту, но музыке незачем ограничивать окно вообще (чтоб заикалась, если ACK потеряется), а файлам всем дать общее окно - всё равно пишущий их тред упирается в скорость HDD, а какой из них быстрее скачается, какая разница. Тут, понятно, и дескрипторы бы отдельные, да не все (то есть треду наверное хватит одного для сразу нескольких потоков), но тут наверное сложно в реализации, не придумаю сходу (что-то вроде peeloff из поддерева, штоль).
Заметки тоже почитаю.
Да, меня пока еще никто толком не поревьювил, к сожалению. Правда, тамошняя свалка этому способствует :) Я бы вот, например, всё хотел объединить все типы потоков в единый - сейчас у меня как бы три типа, bytestream (tcp-like) channel'ы (наподобие SSH) и два делящихся на сообщения, ordered и unordered (как в SCTP), плюс id'ы сообщений чтоб опциональная докачка сообщений при обрыве (если QoS>0 как в MQTT - да, мы сессионый протокол, L5). Но взаимно управлять потоками удобнее, когда вот у него просто номер: 1, 2... и эти номера не делятся дополнительно на типы (но суть unordered в том, что он именно что в том же потоке логически, как решение URG/^C telnet). Как это втиснуть в SOCK_STREAM/SOCK_DGRAM, если выделять отдельные fd вообще не представляю. Если нет.. всё вопрос красивости единого API стоит. В SSH2 интересно сделали, там channel'ы byte-oriented, хотя и делятся на чанки ниже уровнем, и в них возможны request'ы/reply синхронные - то есть дочитали до байта, тут раз, сообщение целиком (как бы переключение, но нет, эдакий URG отдельный большого размера), потом снова просто байты будут. Но там просто луп в коде парсит сообщения и сразу обрабаттывает, им там до теоретической вынесенности и красивости API нет дела, они уже и так на L7...
Вот же городят костыль на костыле, чтобы криво написанные сайты в криво написанных браузерах меньше лагали.
Сайты ни при чем, все это решения проблем сети.
Нет, когда сайту надо скачать 10мб мусора (да ещё и обязательно в несколько криво синхронизированных потоков) ради вывода простой странички — это проблема сайта, а не сети.
Ну для видеозвонков тоже есть эта проблема(особенно для протоколов на базе tcp). Поэтому всё таки проблема сети в основе.
Это не частая проблема на популярных сайтах. Аналогично игры растут в размере за счёт текстур и прочих медиа. Сайты прирастают всяким JS, картинками для ретины, шрифтами и т.п. Конечно проблема code bloat есть и можно запросто нагрузить сайт всякой фигней. Но популярные сайты все же более-менее оптимизированы и им всякие http/2 и 3 очень даже.
Отличная статья! Без бэкграунда в сетях, конечно, мало что будет понятно, но, к счастью, знания о L4 есть.
Понравилось, то, что объясняется смысл технологии, а не пересказываются сухие спецификации, в которых нужно разбираться значительно дольше.
Перевод хороший, спасибо за старания!
Head-of-Line Blocking в QUIC и HTTP/3: Подробности