Pull to refresh

Comments 37

Спасибище! Очень нужная тема затронута и актуальна для меня.
Мы юзали сервер с 4 сетевыми картами — в общей сложности оно обслуживало до 50к подключений (исходящих), тоже на epoll. Единственное — каждый сокет перед коннектом надо ручками биндить на правильную сетевушку (да и вообще AFAIR у автоматического бинда есть количественные ограничения, но точно не вспомню — 7 лет прошло)
Как всегда отличное видео.
Хотелось бы ещё добавить про необходимость отложеного удаления(сборщик мусора) при использовании указателей в epoll_event.data.ptr, тк в случае когда у нас идёт обработка результатов, которые вернул epoll_wait и результат первого евента повлёк к ситуации, когда нужно уничтожить «состояние», связаное с одним из последующих евентов, то нельзя убивать то на что указывает epoll_event.data.ptr, нужно это делать после обработки всех результатов, возвращаемых epoll_wait'ом.
Что поразительно так это то, насколько регулярно в подобных обзорах/обучающих материалах «забывают» про единственный правильный способ: асинхронный ввод/вывод, который наконец-то появился даже в юниксах. Можете назвать хоть одно преимущество «неблокирующего» ввода/вывода перед настоящим асинхронным?
Более поразительным является то что люди, которые никогда в жизни не использовали ни epoll, ни этот aio, рассказывают о том что является единственным правильным способом.

>Можете назвать хоть одно преимущество «неблокирующего» ввода/вывода перед настоящим асинхронным?
ну начнём с того что это апи для работы с фс, а не сетью.
а закончим тем что в случае с проактор паттерном приходится выделять буфферы для чтения на каждый сокет чтобы пропихнуть его в aio_read, из-за чего получим проблемы с масштабируемостью в случае с сотнями тысяч слабоактивных сокетов.
Как оказывается, еще более поразительным является то, что даже те, кто вроде бы и знают о существовании асинхронного ввода/вывода, совершенно не представляют себе что это такое и почему это единственный правильный способ.

ну начнём с того что это апи для работы с фс, а не сетью.

Ну начнем с того, что подтвердить это утверждение очень просто — нужно всего лишь привести цитатку из стандарта. Предупреждая Вашу попытку выставить aio_fildes как доказательство, отмечу, что socket возвращает как раз его: «Upon successful completion, socket() shall return a non-negative integer, the socket file descriptor».

Более того, в Линуксе есть асинхронные операции предназначенные исключительно для работы с диском, пусть это и является довольно уродливым решением с точки зрения дизайна.

а закончим тем что в случае с проактор паттерном приходится выделять буфферы для чтения на каждый сокет чтобы пропихнуть его в aio_read

Вот Вы сейчас противопоставляете необходимость выделения буфера под прием данных чему? Неужели сокет сам по себе не имеет буфера на прием? Неужто SO_RCVBUF в Линуксе по умолчанию установлен в 0 и любые данные, на которые нет outstanding read мгновенно теряются. Вот это новость.

Но позвольте мне продолжить Вашу мысль:

В-третьих: мы избавляемся от лишнего копирования: сначала в буфер драйвера tcp, а потом в пользовательский (пользователь в данном случае — это пользователь API, то есть программист). К тому же при наличии TOE (да, я знаю, что он не решает «проблему» c10k, но он очень хорошо вписывается в модель асинхронного ввода/вывода) количество копирований можно сократить до 0 (НУЛЯ): с «провода» сразу в пользовательский буфер по DMA.

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

В-пятых: асинхронный ввод/вывод концептуально чище: он превращается в синхронный (блокирующий или нет) всего лишь оберткой с одним дополнительным wait-ом в конце, в то время как синхронный в асинхронный не превращается никак (можно придумать схемы с пулом потоков или с порождением новых потоков на каждую операцию, но они будут либо слишком «дорогими», либо слишком неустойчивыми к «нестандартной» нагрузке, либо и то другое вместе).

Список можно продлжать, но на мой взгляд, то что приведено выше — уже достаточно веские основания задуматься.

В заключение хочу отметить, что конкретная РЕАЛИЗАЦИЯ асинхронного ввода/вывода может быть очень плохой (что в случае Линукса меня бы совершенно не удивило), что не отменяет моего изначального тезиса о том, что настоящий асинхронный ввод/вывод имеет кучу преимуществ перед «неблокирующим», в то время как обратное несправедливо: нет ни одного преимущества «неблокирующего» ввода/вывода перед асинхронным.
>the socket file descriptor
для работы с дисковым IO, либо с NFS

>Неужели сокет сам по себе не имеет буфера на прием?
он динамически растёт и можно задавать максимальные границы с помощью SO_RCVBUF.
Поэтому даже в вашей любимой винде используется примерно такая же схема для работы с сотнями тысяч соединений, когда делают WSARead с нулевым буффером.

>В-третьих
да, избавляемся.
в линуксе есть vmsplice/splice, этот подход конечно немного хуже тк потребует делать системный вызов splice для zero-copy чтения. Я не использовал этот механизм, так что может кто другой расскажет об этом.

>В-четвертых…
забыли упомянуть про то что нужен будет раундтрип для того чтобы снова сделать WSARead, в итоге получаем те же два раундтрипа.
Хотя в случае когда будем использовать нулевые буфферы для уведомления, а потом неблокирующее чтение — получим три раундтрипа ;)

>В-пятых…
всё будет ок ;) см. Haskell, Ocaml итд
>>В-пятых…
>всё будет ок ;) см. Haskell, Ocaml итд
Ой, что-то ступил, вы же о другом :) вы же о концептуальной чистоте асинхронных операций.
в линуксе что-то обсуждали о том чтобы дать возможность исполнять любой системный вызов асинхронно, было вроде даже много разных вариантов как это делать.
Но всё же нужны оба варианта, тк в одном случае выигрываем производительность, в другом наоборот.
Хм, я не понял в каком случае мы проигрываем производительность, а в каком наоборот? Асинхронный ввод/вывод — один из тех немногих случаев, когда компромиссов мы не делаем: выигрыш идет по всем параметрам.

Насчет обсуждения возможности вызова произвольного сискола асинхронно я тоже помню — но не помню, где видел. По факту это все тот же пул потоков плюс/минус специальные «костыли» в скедьюлере.

В любом случае, описать prefork, select и poll, но не упомянуть асинхронный ввод/вывод (независимо от того, насколько ужасна его реализация) — это несколько поверхностно, не находите?
для работы с дисковым IO, либо с NFS

Для асинхронного I/O. Точка. Больше ничего в стандарте на этот счет не сказано. Асинхронный вывод нужен там, где операция ввода/вывода может занять неопределенное время (то есть там же, где и «неблокирующий»). То есть при работе с сетью тоже. Все остальное — Ваше ЛИЧНОЕ мнение пока Вы не подтвердите его цитатой.

он динамически растёт и можно задавать максимальные границы с помощью SO_RCVBUF.

Позволю себе процитировать источник, который, как Вы успели заметить, мне не нравится, что не освобождает Линукс от необходимости следования ему «The SO_RCVBUF option requests that the buffer space allocated for receive operations on this socket be set to the value, in bytes, of the option value.»

Я вообще не упоминал «мою любимую винду», но да — там это сделано по уму (хотя WinSock мне не очень нравится — уж очень сильное влияние BSD sockets).
Поведение WSARecv с нулевым буфером неопределено, и у меня есть серьезные подозрения, что это закончится AV. Фактически выделение буфера (даже с MEM_COMMIT) — это резервирование места в пейджфайле. Ну то есть система обещает нам дать столько, если мы попросим. Если не попросим — единственной «проблемой» будет увеличение циферки в таск менеджере, а если попросим — так какая разница (в разрезе количества памяти) «неблокирующая» операция или асинхронная? Есть и другой вариант: система пообещала что даст нам некое количество памяти, но на самом деле оставляет за собой право убить любого кто на самом деле запросит эту память (оверкоммит и оомкиллер).

в линуксе есть vmsplice/splice

Zero-copy был назван как один из бенефитов асинхронных операций (и, повторюсь, я полностью осознаю, что TOE находится вне контекста данного обсуждения, как не помогающий в решении «проблемы» c10k — у него совершенно другие задачи) и невозможен без полного оффлоада протокола. Stateless оффлоады (как впрочем и отсутствие оффлоадов вообще) могут помочь только доставить в память (по DMA естественно) сетевой фрейм. Далее эти фреймы нужно пересобрать (с отрезанием заголовков и упорядочиванием) — для этого необходимо копирование. Даже если бы Линукс поддерживал TOE, при неблокирующем вводе/выводе, лучшее на что можно надеяться это прямая запись в буфер драйвера транспорта (TCP) — копирование из этого буфера в пользовательский все еще необходимо. vmsplice/splice здесь не помощник.

забыли упомянуть про то что нужен будет раундтрип для того чтобы снова сделать WSARead

На latency это никак не отразится: мы же запостим два буфера, так? И пока один закомплитится и наполняется второй — мы запостим третий.

Хотя в случае когда будем использовать нулевые буфферы для уведомления, а потом неблокирующее чтение — получим три раундтрип

Оставляя в стороне саму возможность работоспособности подобного решения (мне лень тестить и я не нашел ничего подобного в MSDN, но «мне кажется», что WSARecv просто упадет если подсунуть ему нулевой буфер), у Вас есть хоть один пример использования подобного подхода в «дикой природе»?
>Для асинхронного I/O. Точка.
какой же вы упрямый :)
— это асинхронное IO даже не будет работать со всеми ФС в асинхронном режиме, только в тех в которых реализована поддержка
— открывать файл нужно будет с флагом O_DIRECT, а это значит что забываем о кэшировании, встроеном в ядро
— на пайпах вроде будет выкидывать ошибку
— в случае с сокетами не будет работать в асинхронном режиме

>Позволю себе процитировать источник, который, как Вы успели заметить, мне не нравится, что не освобождает Линукс от необходимости следования ему
мои линуксовые ман файлы говорят: «SO_RCVBUF Sets or gets the maximum socket receive buffer in bytes.»
Чтобы добраться до минимальных значений, нужно идти в tcp_rmem ;) Но нужно помнить что этот минимум в случае с виндовым aio будет являться максимумом(если хотим zero-copy), последствия которого можно будет быстро ощутить.

>Поведение WSARecv с нулевым буфером неопределено, и у меня есть серьезные подозрения, что это закончится AV
Опять ваши догадки, я вам говорю как работает на реальных приложениях, а вы опять про какие-то подозрения.

>Даже если бы Линукс…
Netmap
NetChannels
итд…
Конечно это всё не существует внутри оф. ядра линукса. Хотя нетмап уже вроде попал в фрибсд.

>у Вас есть хоть один пример использования подобного подхода в «дикой природе»?
Ну из публичных мест, где говорится об этом, гугление сразу выдаёт antirobotrobot.tumblr.com/post/5094268092/wsarecv
stackoverflow.com/questions/4988168/wsarecv-and-wsabuf-questions
— это асинхронное IO даже не будет работать со всеми ФС в асинхронном режиме, только в тех в которых реализована поддержка

Да, я заметил, что для aio выделены отдельные слоты в vfs. Тем не менее, я все еще прошу цитату из стандарта, что aio предназначен только и исключительно для файловых операций.

мои линуксовые ман файлы говорят: «SO_RCVBUF Sets or gets the maximum socket receive buffer in bytes.»

Да, я сверился как с манами, так и с net/core/sock.c перед тем как постить ответ. Моя цитата — из POSIX:2008.

Опять ваши догадки, я вам говорю как работает на реальных приложениях, а вы опять про какие-то подозрения.

Ну да, догадки. Вы ж так туманно говорите (в случае с WSARead — я «догадался», что Вы имеете в виду, а вот в случае «нулевым буфером» — уж извините, сразу подумал на NULL, а не на zero-length). И да, «в реальных» приложениях так никто не делает. Увеличивать latency почти в два раза (на дополнительный системный вызов) только для того, чтоб «сэкономить» полгигабайта виртуальной памяти (гигабайт места на диске стоит до 30 центов и то только из-за наводнения и если нужен самый дорогой да еще и в ретейле) — причем я исхожу из оценки, которая дана там же. На самом же деле, при том, что максимальный размер TCP пакета 64k — MTU в большинстве реальных условий будет 1.5к (вернее 1500) или 9к. Более того, даже если Вы напишете сервер из расчета на максимальный размер фрейма и этот максимальный размер фрейма Вам таки придет, то максимум, что Вы теряете от того, что не получите весь буфер сразу — это то самое дополнительное копирование из буфера драйвера транспорта в Ваш буфер. Короче, если хочется снижать именно затраты памяти, то постить более двух буферов по 9к не имеет смысла и уже потом можно динамически наращивать исходя из реальной ситуации.

Netmap
NetChannels
итд…

Штуки достаточно интересные, но вообще никаким боком к zero-copy TCP. В одном случае — вообще отказ от TCP, а в другом — оптимизация существующего TCP (что несомненно хорошо и правильно, но лучше использовать В СОЧЕТАНИИ с другими техниками). Без поддержки со стороны железа, zero-copy на прием не сделаешь.
>Тем не менее, я все еще прошу цитату из стандарта, что aio предназначен только и исключительно для файловых операций.
Приложение не работает со стандартами, оно работает с тем что реально реализовано. Я говорю о том как есть и с чем приходится работать, а вы о каких-то стандартах.

>так и с net/core/sock.c перед тем как постить ответ
идём в net/core/sock.c и видим, что SO_RCVBUF влияет на sock.sk_rcvbuf, который отвечает за максимальный размер буффера.

>Увеличивать latency почти в два раза (на дополнительный системный вызов)
не знал что latency увеличится почти в два раза из-за дополнительного системного вызова :) Очень хотелось бы в это поверить, но всё же не смогу. Наверное у нас просто расхождение в понимании latency, я про эту latency

>И да, «в реальных» приложениях так никто не делает.
stackoverflow.com/questions/4988168/wsarecv-and-wsabuf-questions
смотрим профиль человека, который написал ответ в этой теме: «I have developed a highly scalable, high performance, Windows, I/O completion port based client/server networking framework: www.serverframework.com.»
И что-то мне подсказывает, что у этого человека громадный опыт в разработке реальных серверов под винду, а не теоретических.
И вообще не пойму о чём речь, открыл я документацию по винде, и что я вижу:
1. Хотим zero-copy, нужно выделять locked память, которая не будет никогда сброшена в swap
2. Хотим zero-copy, нужно чтобы висело больше одного запроса на чтение(ещё больше физ памяти), иначе AFD.SYS использует внутрению буфферизацию и потом делает обычное копирование в поступивший запрос на recv.
3. Лимит на locked память — 1/8 физической памяти
В случае без zero-copy, получаем просто громадный оверхед на виртуальную память и значительную потерю производительности из-за этого(цпу кэши, все дела).

Или если речь не про винду, то хотелось бы посмотреть у кого это AIO сделано так как надо, на кого стоит равняться.
И вообще не пойму о чём речь, открыл я документацию по винде, и что я вижу
Я вот не понимаю, зачем Вы обсуждаете, то в чем, скажем там, не совсем хорошо разбираетесь?

1. Сюрприз!!!111 Все сетевые карты уже минимум лет 10 (может и раньше — не знаю) используют DMA для получения и отсылки пакетов. Так что физическая память ВСЕГДА лочится (DMA, как ни странно, умеет работать только с физической памятью). Другое дело, что для того, чтобы пересобрать пакеты в, собственно, TCP поток по-любому нужно одно копирование. Оно может происходить или прямиком в пользовательский буфер или в буфер драйвера транспортного протокола.
2. Afd.sys не держит буферов. Буферы держит tcpip.sys, а afd.sys — это адаптер инерфейса WinSock2 к TDI/WSK. В любом случае, эти буферы не обязаны быть физическими. Как я уже сказал, физические буферы нужны только NDIS драйверу для получения/отправки данных из/в физической среды. И вот здесь да, при наличии TOE мы можем получать (или отсылать) уже готовый TCP поток прямиком в пользовательский буфер и ВОТ ЭТОТ буфер — должен быть заблокирован в физической памяти, потому что работа с ним идет напрямую через DMA. Да, здесь видим обычный space/time tradeoff, но это, повторюсь, относится к async vs non-blocking I/O только в том плане, что из-за того, что асинхронный ввод/вывод позволяет избавиться от копирования из буфера транспортного протокола в пользовательский буфер, а TOE — от копирования из буферов драйвера сетевой карты в буфер драйвера транспортного протокола, то мы выходим на zero-copy, а в случае неблокирующего ввода/вывода zero-copy невозможен даже если бы Линукс поддерживал TOE.
3. MmProbeAndLockPages не говорит ничего о лимитах. Но это не важно в контексте zero-copy, потому что максимальное количество TOE состояний, которые можно офлоадить из виденных лично мной — 1024. Транспортный протокол использует хитрую эвристику, чтоб аплоадить только длительные соединения, по которым передается много данных (скачивание/закачивание файлов, стриминг медиа и пр.)

В случае без zero-copy, получаем просто громадный оверхед на виртуальную память и значительную потерю производительности из-за этого(цпу кэши, все дела).

Нет, не получаем. Буферы транспортных протоколов (и буферы, которые препостятся приложением при асинхронных операциях) используют demand paging и фактически «не существуют» до момента первого обращения к ним. Место в пейджфайле резервируется, но единственные дисковые операции, которые 100% происходят — это увеличение пейджфайла, если в нем не хватает места.

Или если речь не про винду, то хотелось бы посмотреть у кого это AIO сделано так как надо, на кого стоит равняться.

Да используйте хоть POSIX-овый AIO — все лучше чем «неблокирующие» операции (концептуально — я действительно не знаю, насколько хорошо они РЕАЛИЗОВАНЫ). Кроме того, вот Солярис внедрил IOCP, которые очень близки к виндовым
Чего то я напортачил с цитированием — ну да ладно, главное что осталось относительно понятно, что к чему
Это уже несерьезно. Вот статья того же автора, в которой он рассказывает как синхронизировать несколько асинхронных операций, которые могут завершиться out-of-order. Это что-то доказывает?

Если уж речь зашла о «теоретическом» опыте написания серверов, то, надеюсь, Вас успокоит, что у меня есть чисто практический опыт написания распределенного нагрузочного сетевого теста. Более того, проблема c10k передо мной даже не стояла, потому что была проблема c64k — столько портов доступно на одной системе. Здесь мы можем обсудить преимущества и недостатки самописного менеджмента портов на нескольких IP адресах одного интерфейса против использования wildcard bind в сочетании с SO_PORT_SCALABILITY, а также поговорить о влиянии TIME_WAIT таймаута на количество доступных портов (среди прочего).
После этого, я встану в позу «о чем вообще можно говорить с человеком, который не может с нескольких попыток правильно написать WSARecv» и демонстративно «выйду из дискуссии». Но ведь это же будет контрпродуктивно, не так ли?

не знал что latency увеличится почти в два раза из-за дополнительного системного вызова

И почему тогда Вы обсуждаете то, в чем не разбираетесь?
Latency — это задержка между каким-нибудь событием (в данном случае получением данных) и ответом на него. То, что Вы описали — это задержка самой физической среды, я говорю о задержке уже на самом сервере между получением данных и их обработкой. То есть вместо: «пришли данные — начали обрабатывать» у нас происходит «получили сообщение о том, что пришли данные — запросили данные — начали обрабатывать». Вот это дополнительное «запросили данные» и дает задержку. К тому же там еще и дополнительное копирование, что довольно сильно сказывается уже на 1Gb NIC-ах (у нас есть 10-30 тактов на байт и часть этого времени уже «съедена» другими частями стека, так что тратить оставшиеся такты на ненужное копирование — крайне расточительно, кроме того, тратить лишнюю ~тысячу тактов на каждый пакет — тоже совершенно излишняя роскошь), не говоря уже о 10Gb.

Попробую вернуть обсуждение в оригинальное русло. Чтобы не осталось непонимания, ответьте, пожалуйста, на следующие вопросы:
1. Согласны ли Вы, что если препостить буфер, то можно избежать одного дополнительного копирования и одного дополнительного раундтрипа в ядро на критическом пути (собственно, пути обработки входящих данных)?
2. Если ответ на (1) да, то согласны ли Вы, что это снижает задержку на обработку входящих данных?
3. Согласны ли Вы, что на большинстве современных сетевых карт, достаточно изначально препостить два буфера по 9000 байт (можно даже захардкодить, но лучше спросить драйвер о его MTU) для того, чтобы избежать вышеупомянутого копирования (в то время, как люди перемещают базы данных размером в сотни гигабайт целиком в память, мы пытаемся сэкономить сотню мегабайт даже не физической памяти — а места на диске, задерживая почти в два раза обработку данных, ну да ладно — если это проблема, то ее тоже можно решить)?
4. Согласны ли Вы, что даже если препостить буферы меньшего размера (скажем по 1500 байт), то мы В ХУДШЕМ СЛУЧАЕ выходим на производительность неблокирующего ввода/вывода, при этом в большинстве случаев выигрывая в производительности?
Ну и дополнительный вопрос (с которого все началось):
5. Считаете ли Вы что select, prefork и poll более достойны обсуждения, чем async I/O?
То, что Вы описали — это задержка самой физической среды, я говорю о задержке уже на самом сервере между получением данных и их обработкой.

вы говорите о теоретическом увеличении задержки в почти два раза

1. Согласны ли Вы, что если препостить буфер, то можно избежать одного дополнительного копирования и одного дополнительного раундтрипа в ядро на критическом пути (собственно, пути обработки входящих данных)?

согласен, но zero-copy можно и без aio, см splice/vmsplice :)

2. Если ответ на (1) да, то согласны ли Вы, что это снижает задержку на обработку входящих данных?

на микробенче с парой соединений возможно снизит задержку, в условии когда будут тысячи соединений, и будет происходить _обработка_ входящих данных — хз, поверю только если увижу какие-то бенчи.

3. Согласны ли Вы, что на большинстве современных сетевых карт, достаточно изначально препостить два буфера по 9000 байт (можно даже захардкодить, но лучше спросить драйвер о его MTU) для того, чтобы избежать вышеупомянутого копирования

1. не согласен
2. mtu тут вообще не в тему
3. два буфера по 9000байт не сможем, тк нужно выравнивать по размерам страниц :)

4. Согласны ли Вы, что даже если препостить буферы меньшего размера (скажем по 1500 байт), то мы В ХУДШЕМ СЛУЧАЕ выходим на производительность неблокирующего ввода/вывода, при этом в большинстве случаев выигрывая в производительности?

В лучшем случае вспомним времена как было хорошо на модемах :)

5. Считаете ли Вы что select, prefork и poll более достойны обсуждения, чем async I/O?

да, считаю

Afd.sys не держит буферов. Буферы держит tcpip.sys, а afd.sys — это адаптер инерфейса WinSock2 к TDI/WSK. В любом случае, эти буферы не обязаны быть физическими. Как я уже сказал, физические буферы нужны только NDIS драйверу для получения/отправки данных из/в физической среды.

Читаем что пишут разработчики Microsoft
++ Winsock DLL (user mode)
++ == afd.sys (kernel mode)
++ ==== tcpip.sys
++ ====== ndis
++ ======== network interface (hal)
At the AFD level, there is a buffer, generally between 8K and 64K, but with Vista (and beyond), it can also scale up. This setting can also be controlled by a registry setting(HKLM\SYSTEM\CurrentControlSet\services\AFD\Parameters).

In addition, the tcpip.sys also has a buffer, that is similar to AFD's buffer. I believe *SO_RCVBUF* setting passed when opening the socket can change this too.

Essentially, when you are receiving data, tcpip.sys on your behalf keeps getting data, and keeps the telling the sender that it got the data (ACK's), and does so until its buffers are full. But at the same time, afd.sys is clearing tcpip.sys buffers by asking it for the data (which it then copies into its own buffer), so tcpip.sys can fill more data from the sender.

когда хотим zero-copy(из кернелспэйса в юзерспэйс), то виндовое апи требует выделять физ. память, которая будет использоваться в afd.sys, а не ndis драйвере.
вы говорите о теоретическом увеличении задержки в почти два раза

Нет, я говорю о совершенно реальной разнице между одним копированием (если принимать в рассчет общий случай — без TOE) и одним раундтрипом в ядро на критическом пути вместо двух копирований и двух райндтрипов.
На моем ноутбуке двухлетней давности стоит сетевая карта с поддержкой 1 Gig/sec (к слову, стандарта почти пятнадцатилетней давности). Практически все серверные сетевые карты последнего десятилетия поддерживают такую скорость. Внедрение 10 гбит (не говоря уж о 40 или 100) несколько осложняется тем, что медь такие скорости уже не выдерживает и нужна совершенно другая инфраструктура (начиная от сетевых карт и свитчей и заканчивая сетевыми кабелями и «обжимным» инструментом). Так вот, 1 гигабит в секунду — это 125 мегабайт в секунду (Ваш К.О.). У какого нибудь Xeon-а на 3.6 гигагерцах есть всего 29 тактов на байт, чтобы все обработать (параллелится все только на уровне клиентов — критический путь строго последовательный). Пусть даже все контрольные суммы были посчитаны самой картой (TCP Checksum Offload поддерживается даже low-end картами), нам необходимо как минимум одно копирование из буфера сетевой карты в собранный TCP поток. Задержка обращения к памяти может достигать 60 наносекунд (или 200+ тактов нашего 3.6 ГГц процессора). Один раундтрип в ядро — около 1000 тактов. Я не совсем понимаю почему Вам все еще кажется, что лишнее копирование на таких скоростях обмена это так — ничего особенного. Естественно, если на самом верху стека ожидает какой нибудь питон или пэхапэ, то эти микросекунды в общем то и не важны, но даже если не говорить о хайлоаде — зачем делать лишнюю работу, если ее можно не делать?

согласен, но zero-copy можно и без aio, см splice/vmsplice :)

Хм, я уже устал повторять. Одно копирование будет при пересборке пакетов в TCP поток (кроме случаев, когда пересборка происходит аппаратно и в память пишется готовый TCP поток): оно может происходить либо в буфер транспорта либо наприямую в пользовательский буфер ЕСЛИ ОН ЕСТЬ. Второе копирование будет необходимо если этого буфера нет (или Вы предлагаете держать буферы драйвера сетевой карты с пакетами до тех пор, пока не появится запрос на чтение?).
splice/vmsplice поможет только в случае если есть этот самый пользовательский буфер, ожидающий данных. Он может быть либо при синхронном блокирующем чтении либо при асинхронном. При неблокирующем чтении это второе копирование всегда необходимо. Я объяснил это уже три или четыре раза — лень пересчитывать. Если Вы видите, где я неправ — прошу показать каким образом Вы сможете избежать двух копирований при помощи splice/vmsplice и в условиях неблокирующего чтения.

1. не согласен
2. mtu тут вообще не в тему
3. два буфера по 9000байт не сможем, тк нужно выравнивать по размерам страниц :)

MTU при том, что для того, чтобы избежать лишнего копирования, нам достаточно доставки данных из одного пакета — а дальше можно и принять решение о том, как продолжать. Кроме того, зачем нам здесь выравнивание (в не-TOE случае, нас даже DMA не касается, и даже если бы касалось, DMA alignment у большинства устройств значительно меньше страницы)?
Я еще раз посмотрел интерфейс WSK и должен признать, что речь действительно идет о физической памяти (WSK_BUF работает с MDL-ами).

В лучшем случае вспомним времена как было хорошо на модемах :)

Э-э-э-э. Это Вы ушли от ответа или действительно хотите что то сказать?

5. Считаете ли Вы что select, prefork и poll более достойны обсуждения, чем async I/O?

да, считаю

Почему?

Читаем что пишут разработчики Microsoft

Разработчик из Бинга, возраст — 26 лет. То есть стаж — не более 4 лет и при этом не связанный ни с ядром винды вообще, ни с сетевым стеком в частности. Но даже не принимая это во внимание — совершенно неважно, что делает AFD когда нет ожидающего запроса на чтение. Когда такой запрос есть — он уходит напрямую в транспорт.

когда хотим zero-copy(из кернелспэйса в юзерспэйс), то виндовое апи требует выделять физ. память

В общем случае это неверно. Есть три типа обмена: Buffered IO, Direct IO и Neither (type 3) IO. Только один из них (Direct IO) требует физической памяти — собственно для того и создавался (отсюда и название). Это самый эффективный по скорости, но самый «расточительный» способ и именно он используется как TDI так и WSK.

, которая будет использоваться в afd.sys, а не ndis драйвере.

Во первых, afd.sys не нужно выделять никаких дополнительных буферов, если пользователь запостил собственный буфер, но таки да tcpip.sys тоже лочит страницы (для избежания копирования). Собственно наша дискуссия свелась к вопросу о том, что лучше: В ХУДШЕМ СЛУЧАЕ потратить сотню мегабайт физической памяти (в случае с префорками Апача и то зачастую тратится больше) или В ЛЮБОМ СЛУЧАЕ потратить впустую несколько микросекунд на обработку каждого пакета (причем пакеты все равно будут в физической памяти иначе речь пойдет о задержках в десятки-сотни миллисекунд).
Хочу продолжения банкета.
Где вторая сторона? :)
Если отгуглите хоть одно подтверждение слов сторонника виндового aio, то могу продоложить банкет :) Я не занимаюсь разработкой под виндой, поэтому у меня нет глубоких познаний в том как всё на винде, но я не поленился ради этой беседы выкачать с торрентов книги по внутренностям, пробежался по ссылкам, которые гуглятся на первых страницах по этой теме и судя по всему весь интернет ошибается :)
Ну а такие цитаты как «Согласны ли Вы, что даже если препостить буферы меньшего размера (скажем по 1500 байт), то мы В ХУДШЕМ СЛУЧАЕ выходим на производительность неблокирующего ввода/вывода, при этом в большинстве случаев выигрывая в производительности?» нам как бы намекают об уровне познаний, и становится непонятно о чём вести диалог вообще ;)
Ну мой уровень познаний в этой области явно недостаточен, чтобы аргументированно участвовать в обсуждении. Но почитать было вполне интересно.

Правда ваше непонимание термина latency нам не то чтобы намекает, а прямо таки кричит о лично вашем уровне познаний.
Но это мое субъективное мнение в данном конкретном случае.

Резюме: Ваш оппонент мне показался более аргументированным. И более, того в отличии от вас, он производит впечатление человека, который понимает и то, что вы говорите, и собственную точку зрения.
Для вас же доступно только одно.
>Правда ваше непонимание термина latency
Боюсь что в контексте сетевого приложения говорить про latency и иметь в виду затраты на дополнительный сискол, а не увеличение задержки, которое увидит реальный сетевой клиент — это достаточно сомнительная затея. Причём такие громкие слова как увеличение в _два_ раза(был один сискол, стало два), будто кроме сискола в системе больше ничего не происходит, ну что за бред.

>он производит впечатление человека, который понимает и то, что вы говорите, и собственную точку зрения.
если вы обратите внимание на начало дискуссии, то увидите что этот человек забежал и начал утверждать то в чём вообще не разбирается, рассказывая про линуксовое aio. Причём старался делать это так убедительно, ссылаясь даже на исходники, но слава богу в случае с линуксом не проблема заглянуть в исходники ;)
А в случае с виндой получается что то что пишут на msdn/stackoverflow сотрудники microsoft оказывается неверным, а то что знает он — так работает всё на самом деле. И после утверждений про линуксы, верить в какие-то утверждения про винду, не подкреплёные никакими ссылками, я не буду.

>Для вас же доступно только одно.
Я на самом деле до сих пор не понимаю о чём речь в случае с виндой, потомучто то поведение сетевого стэка на винде, которое тут было описано — невозможно нигде нагуглить, я честно старался это делать :)
Везде пишут что когда делается запрос на zero-copy чтение, страницы должны быть залочены, но нам тут опять втирают о том что это не обязательно :) Потом идёт просто чудеснейшая бредятина с тем что юзерспэйс приложение высчитывает mtu и отправляет запросы на чтение, в зависимости от этого параметра, как будто у нас происходят мгновеные контекст свитчи как только прилетают 1500байт и такой конвеер с 1500байтами работал бы идеально, ну это феерический бред.
Боюсь что в контексте сетевого приложения говорить про latency и иметь в виду затраты на дополнительный сискол, а не увеличение задержки, которое увидит реальный сетевой клиент — это достаточно сомнительная затея.

Боюсь, что класс «сетевых приложений» не ограничивается «хомяками на похапэ». Более того, я за Вас посчитал количество тактов на байт даже на middle-end-овых 1GbE NIC и high-end-овом процессоре (идеальный для Вас случай). Разделить на 10 (40? 100?), надеюсь, сможете сами (да, кстати, оказывается уже несколько лет внедряют 10GbE инфраструктуру на витой паре — каюсь, некоторое время не интересовался конкретно этим вопросом).

Причем речь идет не о задержке между запросом удаленной стороны и ответом на нее сервера, речь идет о задержках между приемом пакета сетевой картой и возможностью приложения увидеть данные из этого пакета, а сетевому стеку — продвинуть окно. То есть о задержках ВНУТРИ ПОТОКА. Как Вам понравится если Ваш 1 гигабит ВНЕЗАПНО превратится в 500 (среднепотолочное значение — можете не придириаться) мегабит? А если в те же 500 мегабит превратится 10 гигабит (процессор то не ускоряется с «ускорением» сетевой карты)?

А в случае с виндой получается что то что пишут на msdn/stackoverflow сотрудники microsoft оказывается неверным, а то что знает он — так работает всё на самом деле

Интересно, где Вы увидели, что написанное «сотрудником Майкрософта оказывается неверным»? Я же вполне ясно сказал, что оно просто не имеет отношения к предмету обсуждения. Точно так же как и Ваши ссылки на «zero-copy» оптимизации в Линуксе. Вообще, подобное разбрасывание ссылками на не относящиеся к обсуждаемому вопросу статьи (авось что-то да «прилипнет») — это довольно явный маркер недостаточной уверенности в этом самом «обсуждаемом вопросе». Хотя чего уж там, Вы же и сами признаете, что знаний в винде у Вас нет, а по Вашему категорическому неприятию концепции асинхронного ввода/вывода (даже prefork/select/poll лучше), смею предположить, что конкретно с ним у Вас тоже опыта нет (хотя опыт сетевого программирования у Вас со всей очевидностью есть).

если вы обратите внимание на начало дискуссии, то увидите что этот человек забежал и начал утверждать то в чём вообще не разбирается, рассказывая про линуксовое aio.

Если Вы обратите внимания, я говорил об асинхронном вводе/выводе, который (НАКОНЕЦ-ТО) появился даже в линуксе (и позиксе). Более того, несколько раз по ходу дискуссии говорил о том, что даже если реализация асинхронного ввода/вывода в линуксе «сосет» (там много чего «сосет» — чего ж теперь от этого всего отказываться?) — это не значит, что асинхронный ввод/вывод «сосет».

Потом идёт просто чудеснейшая бредятина с тем что юзерспэйс приложение высчитывает mtu и отправляет запросы на чтение, в зависимости от этого параметра, как будто у нас происходят мгновеные контекст свитчи как только прилетают 1500байт и такой конвеер с 1500байтами работал бы идеально, ну это феерический бред.

Э-э-э-э, уважаемый, Вы бы лучше хоть раз на пальцах объяснили, как Вам splice/vmsplice поможет избавиться от двух (Вы же сами утверждали что будет «zero-copy») копирований при «неблокирующем» вводе/выводе. Что же до переключения контекстов и «феерическом бреде», во-первых, Вы очень удачно «не заметили», что я говорил о двух буферах размером с MTU, то есть пока заполняется второй — у нас есть время запостить следующий еще ДО НАЧАЛА обработки данных — просто по факту того, что один буфер уже использован. В нашем случае с 1GbE NIC + 3.6GHz Xeon, имеем почти 50к тактов на все про все. Вполне достаточно чтоб запостить DPC, найти по заголовку пакета нужный сокет нужный сокет, перевести его в сигнализированное (signalled) состояние, запостить в KQUEUE, ассоциированную с этим сокетом (и принадлежащую объекту-IOCP) «событие» о переходе в сигнализированное состояние с переводом в сигнализированное состояние самого объекта IOCP, найти в очереди IOCP WAIT_BLOCK одного из рабочих потоков, ожидающих на этом IOCP и переключить процессор на KTHREAD этого потока. Все операции, кроме поиска нужного сокета (ассоциативный массив из 10k объектов) и, собственно переключения, — тривиальны и примитивны. И, заметьте, нужны В ЛЮБОМ СЛУЧАЕ — хоть в неблокирующем, хоть в асинхронном режиме. Просто в асинхронном режиме операций нужно гораздо меньше. Поиск в ассоциативном массиве — либо константная амортизированная сложность, либо логарифмическая для худшего случая — зависит от используемой структуры данных. Переключение конктекстов — действительно самая дорогостоящая операция (опять таки гарантированно необходимая только раз в отличие от). Сброс TLB кешей заставляет несколько раз сходить в память (повторюсь, это в любом случае, в отличие от совершенно ненужных «походов в память» для перекладывания байтиков из одного буфера в другой), но ситуацию спасает тот факт, что рабочие потоки IOCP выбираются по принципу FIFO — то есть поток, который последним заблокировался на IOCP (скорее всего сразу после обработки предыдущего запроса) будет «разбужен» первым и есть немалая вероятность, что все необходимые ему данные будут в кешах разных уровней (опять таки, сравните, с неблокирующим доступом — где планировщику приходится действовать «наугад» и кучу памяти придется доставать прямиком из RAM — и у меня есть какие то смутные подозрения, что все может накрыться еще серьезнее на NUMA серверах)

Да, странным образом, я в Вас вижу грамотного (хотя и не совсем сдержанного) собеседника, Вы же все норовите перейти на личности (лично я себе такое, к моему сожалению, все еще позволяю, но только если вижу, что технического разговора все равно не получится). А вообще, у меня к Вам вопрос: у Вас такой пиетет перед всеми «молодыми специалистами» из Microsoft или только перед теми, кто пишет на stackoverflow
выбираются по принципу FIFO

LIFO, идиот!!!1111
> Я же вполне ясно сказал, что оно просто не имеет отношения к предмету обсуждения.
«Afd.sys не держит буферов. Буферы держит tcpip.sys, а afd.sys — это адаптер инерфейса WinSock2 к TDI/WSK. В любом случае, эти буферы не обязаны быть физическими. Как я уже сказал, физические буферы нужны только NDIS драйверу для получения/отправки данных из/в физической среды.»
Ну давайте ещё раз :) Если хотим zero-copy recv из кернелспэйса в юзерспэйс, то нам нужно выделить память, которая может быть сброшена в своп или нет?
Если всё же эта память не может быть сброшена в своп, то забиваем на эту ветку дискуссии :) И вообще я тогда не понимаю зачем вы расписывали тут столько про виртуальную память )

>а по Вашему категорическому неприятию концепции асинхронного ввода/вывода
не имею ничего против асинхронного ввода/вывода при работе с сетью, но тк на данный момент не вижу никакой реализации ни в одной системе, которая бы эффективно справлялась с задачей по обслуживанию сотен тысяч соединений, то не вижу смысла обсуждать её в этом контексте.
а тк на юниксах достаточно часто встречается select/poll/prefork итд, то эти знания гораздо важнее с исторической точки зрения, чем рассуждения о каких-то несуществующих техниках, в этом случае уж лучше было бы рассказывать про netchannels, реализации которого имеются и которые показывают впечатляющие результаты.

>Вы очень удачно «не заметили», что я говорил о двух буферах размером с MTU
Покажите хоть одну ссылку где можно прочитать про использование подобной техники. Ещё лучше если будут какие-то бенчи.
Интересно взглянуть на то как будет справляться такая техника на сервере, к которому подключены сотни тысяч клиентов, которые с различной периодичностью отправляют мессэджы размером в 1кб-64кб и при этом чтоб шла нагрузка по максимуму.

>А вообще, у меня к Вам вопрос: у Вас такой пиетет перед всеми «молодыми специалистами» из Microsoft или только перед теми, кто пишет на stackoverflow
msdn.microsoft.com/en-us/magazine/cc302334.aspx
Anthony Jones and Amol Deshpande work in the Microsoft Windows 2000 Networking group. Anthony is coauthor of Network Programming for Microsoft Windows (Microsoft Press, 1999).
А чтобы было всё совсем просто, то вот моя упрощённая позиция:
1. виндовое aio без zero-copy не даёт никаких преимуществ, только оверхед на виртуальную память.
2. виндовое aio+zero-copy с использованием двух оверлаппед буфферов по 4кб будет работать неэффективно на сервере, к которому подключено сотня тысяч соединений, которые отправляют мессэджы по 1-64кб и при этом идёт высокая нагрузка. Тк когда мы не будем успевать закинуть новый буффер, то zero-copy будет пропадать, и будет использоваться внутреняя буфферизация(судя по статье на msdn'е, и там советуют использовать не два на приём, а несколько). В итоге будет просто много системных вызовов с чтением по 4кб, вместо одного большого неблокирующего readv.
3. виндовое aio+zero-copy с использованием двух буфферов по ~64кб даст большой оверхед на физ.память из-за которого в итоге у всей системы упадёт производительность, что скорее всего никакого профита от zero-copy не получим, а оверхед на физ.память будет присутствовать.

p.s. Всё это не относится ко всяким серверам, к которым подключено 10-20 клиентов, которые перегоняют большие объёмы данных итд.
Мне показалось, что в aio будет проблема с сигналами, так как сообщения от ядра идут именно через сигналы. Не много ли вызовов будет?
Грубо, на один вызов select/poll/epoll мы можем получить сотню дескрипторов и их обработать. В случае aio это будет сотня сигналов.
Более того, мы в сигнале не можем особенно делать системные вызовы, поэтому мы всё равно сделаем свою очередь, которую будем пополнять в обработчике сигналов, а читать в общем цикле.

В aio круто то, что можно послать большой буфер в fd одним вызовом, но мы упираемся в память сервера на 10k больших буферов. Если же шлём контент из файла, то sendfile ничем не хуже.

Если бы не сигналы, а возможность выгребать события из очереди…
>Если бы не сигналы, а возможность выгребать события из очереди…
signalfd
3 года назад эксплуатировал самописный сервер под Windows 2003. В пике было до 300к подключений и 1.5гбит/сек трафика с двух сетевых карт. Софт был написан под IOCP модель, число воркеров = cpu*2.
Пара интересных фактов:
В свое время был удивлен, что select в fd_set для индексации использует номер сокета сиречь файлового дескриптора, т.е. если открыть сначала 512 файлов, то под сокеты останется также 512 бит из fd_set. Кроме того
подправив __FD_SETSIZE в kernel/include/linux/posix_types.h и пересборав ядро это ограничение можно приподнять.

И ещё факт про epoll. Буква e — появилась от сочетания «edge triggered» срабатывание по фронту — антоним «level triggered» срабатывание по уровню. Poll срабатывает «по уровню» — по наличию данных. Epoll — по добавлению данных. Если зациклить poll и не вычитывать данные — получится бесконечный цикл, в случае с epoll, такого не получится. Если новых поступлений нет — epoll не разблокируется. Т.е. epoll позволяет перенести этап копирования данных(kernel space ->user space) из мультиплексирующей нити в нити обработчики.
Насколько я помню apache довольно активно использует dup3 для перемещения сокетных дескрипторов «вниз» и всех остальных — «вверх».
Спасибо за развернутое обсуждение. К сожалению, мы не могли ответить в выходные, поэтому отвечаем сейчас всем участникам дискуссии.
Асинхронный ввод-вывод не был рассмотрен в лекции по двум причинам:
1). Он используется в Linux крайне редко, и вообще плохо реализован в Linux для сетевых соединений (см. ниже).
2). У автора нет опыта написания программ в этом стиле.

Теоретически, асинхронный ввод-вывод может быть основой для программы, обрабатывающей одновременно много соединений. Под Windows IOCP – это и есть асинхронный ввод-вывод, и он действительно используется в сетевых приложениях. Под Linux это остается теорией, поскольку в ядре AIO работает только на блочных устройствах, а userspace-реализация в glibc основана на потоках и поэтому не может быть лучше решения, использующего потоки напрямую.
Спасибо за ответ.
Небольшая поправка (думаю, Вы это и так знаете, но просто уточню для тех, кто будет читать). «Чистый» без примесей асинхронный ввод/вывод в Win32 имеет название overlapped (и использует структуру OVERLAPPED). IOCP — это не совсем асинхронный ввод/вывод. Строго говоря — они вообще «ортогональны» асинхронности ввода/вывода и занимаются в точности описанной в данном посте проблемой. Просто использование IOCP в паре с асинхронным вводом/выводом является наиболее эффективным и в то же время уступающим по удобству только полностью синхронному вводу/выводу без всяких изысков.
Sign up to leave a comment.