Ads
Comments 53
У меня диссонанс из-за различий в вашем нике и содержимым статьи )
За то и люблю хабр, что в ответ на одну крепкую статью запросто может прилететь еще более крепкая, с разоблачениями и уточнениями. Кондитеры ликуют!
Но это уже тема для другой статьи, если оно кому-либо интересно.


Об чём речь? Конечно интересно. И спасибо за статью, с удовольствием прочел.
Да, очень хорошая статья. В принципе, по содержанию очень похожа на мою, с упором в разные версии unix.
Как бы то ни было, одним вводом-выводом демоны не обходятся, а во время обработки запросов случаются намного более сложные «затыки».
Есть способ «миксовать» используя неблокирующий ввод-вывод для сети и мелких операций а «затыки» кидать в потоки.
Ещё всем интересующимся можно посоветовать почитать книги Стивенса и Снейдера
Что-то про асинхронный ввод/вывод вы высказались не очень хорошо. У начинающих может сложится впечатление, что это сложно и не надо. В своих демонах весь сетевой ввод/вывод осуществляю через libev (раньше использовал libevent), ничего сложного в этом нет.
Скажите, вы вручную включаете EVFLAG_SIGNALFD? Или пользуетесь дефолтными epoll/kqueue/whatever?
У меня как-то исторически сложилось так, что пользуюсь сигналами через sigaction и только для отлавливания SIGUSR1, SIGUSR2, SIGHUP, SIGINT и SIGTERM. В ранних версиях libevent пробовал использовать их реализацию обработки сигналов, но демон стал падать в сегфолт, вернулся к обратно :)
Все эти тесты и высказывание по производительности на nix и win системах ничего не доказывают без детального описания процесса тестирования и непосредственно самой программы. В тестах особо нет нагрузки и нет задач которые требуют относительно длительной обработки данных. И всё упирается в пределы железа и конкретной ОС, а на деле очень часто всё может быть совсем наоборот.

Ну если уже и делать сетевой демон под никсы, то использовать все возможности системы, а не только что-то одно. Если протокол общения работает по схеме: Клиент->Сервер->Клиент (что чаще всего бывает) то для начала желательно было бы применить Accept Filter, чтобы не обрабатывать лишние коннекты. А потом завязать всё систему на epoll(*nix) или kqueue (FreeBSD). Ну а далее обработка запросов через пул потоков. И даёт очень хорошие результаты. Но при этом практически во всех статьях и книгах по программированию про это ни слова, а больше всего делается упор на select и poll. А потом все и считают что select и poll — это самое классное что есть и пытаются впихнуть его куда надо и куда не надо.

Кстати, для Windows с помощью IOCP можно сделать не чуть не хуже по производительности приложения, вот тока более сложнее. Хотя на тестах почему-то при 300к TCP коннектах у Windows (8 ГБ ОЗУ + 8 ядер проц) случился крах TCP. Т.е. PING проходил, на UDP пакеты DNS сервер отвечал, но все TCP коннекты отваливались. Помог только ребут сервера.
Скажите, а вы пробовали epoll() или kqueue() в реальных проектах?
Я там ссылки не зря приводил, они какбе полезны. Вот описание epoll и kqueue от разработчиков libev.
Избранные места:
EPOLL
For few fds, this backend is a bit little slower than poll and select, but it scales phenomenally better. While poll and select usually scale like O(total_fds) where n is the total number of fds (or the highest fd), epoll scales either O(1) or O(active_fds).

The epoll mechanism deserves honorable mention as the most misdesigned of the more advanced event mechanisms: mere annoyances include silently dropping file descriptors, requiring a system call per change per file descriptor (and unnecessary guessing of parameters), problems with dup, returning before the timeout value, resulting in additional iterations (and only giving 5ms accuracy while select on the same platform gives 0.1ms) and so on. The biggest issue is fork races, however — if a program forks then both parent and child process have to recreate the epoll set, which can take considerable time (one syscall per file descriptor) and is of course hard to detect.

Epoll is also notoriously buggy — embedding epoll fds should work, but of course doesn't, and epoll just loves to report events for totally different file descriptors (even already closed ones, so one cannot even remove them from the set) than registered in the set (especially on SMP systems). Libev tries to counter these spurious notifications by employing an additional generation counter and comparing that against the events to filter out spurious ones, recreating the set when required. Last not least, it also refuses to work with some file descriptors which work perfectly fine with select (files, many character devices...).

Epoll is truly the train wreck analog among event poll mechanisms.

While nominally embeddable in other event loops, this feature is broken in all kernel versions tested so far.

KQUEUE:

Kqueue deserves special mention, as at the time of this writing, it was broken on all BSDs except NetBSD (usually it doesn't work reliably with anything but sockets and pipes, except on Darwin, where of course it's completely useless). Unlike epoll, however, whose brokenness is by design, these kqueue bugs can (and eventually will) be fixed without API changes to existing programs. For this reason it's not being «auto-detected» unless you explicitly specify it in the flags (i.e. using EVBACKEND_KQUEUE) or libev was compiled on a known-to-be-good (-enough) system like NetBSD.

You still can embed kqueue into a normal poll or select backend and use it only for sockets (after having made sure that sockets work with kqueue on the target platform).

It scales in the same way as the epoll backend, but the interface to the kernel is more efficient (which says nothing about its actual speed, of course). While stopping, setting and starting an I/O watcher does never cause an extra system call as with EPOLL, it still adds up to two event changes per incident. Support for fork () is very bad (but sane, unlike epoll) and it drops fds silently in similarly hard-to-detect cases

Я вряд ли смог бы написать лучше.
epoll пробовал и до сих пор пробую в проектах которые должны обрабатывать десятки тысяч единовременных коннектов. И пока не жалуюсь на них.
Ничего плохого именно к этой статье я не имею, я говорю в общем о существующем
Автор libev излишне суров по отношению к bsd системам (мне кажется, тут что-то субъективное). На самом деле все не так плохо с kqueue. Есть особенности, да :)
>Кстати, для Windows с помощью IOCP можно сделать не чуть не хуже по производительности приложения
у IOCP есть большой минус, нужно преалокэйтить буферы, отсюда плохая масштабируемость.

>практически во всех статьях и книгах по программированию про это ни слова, а больше всего делается упор на select и poll
Потомучто в большинстве приложений они более актуальны чем epoll итд. Меньше проблем с переносом на другие платформы, ничем не отличающаяся производительность, только масштабируются хуже. А те кто собирается писать сервер под >10к соединений, при первом же запросе в гугле найдут всю нужную инфу.
Автору спасибо, но есть несколько замечаний.

> Итак, первым делом любой демон вызывает системную функцию bind(), которая создает файл специального типа «слушающий сокет».
В корне неверно. Создаёт сокет сис. вызов socket(2). bind(2) связывает сокет с конкретным портом/сетевым интерфейсом, можно получить и клиентский (неслушающий) сокет с пом. bind. Вот listen(2) переводит сокет в слушающий режим и, если сокет ещё не связан с портом, то осуществляет принудительное связывание; после успешного вызова listen bind будет возвращать ошибку.

>Потоки — это максимально легкие «процессы»
Если вы говорите о UNIX, то необходимо упомянуть, что в Linux они никакие не лёгкие, структура в ядре для потоков и процессов та же самая, просто потоки имеют одно и то же адресное пространство. Т.к. все ключевые девелоперы ядра признают, что потоки в принципе — зло, то никто особо не чешется их оптимизировать.

Незаслуженно забыта модель FSM, на которой основан nginx:
https://groups.google.com/group/fido7.ru.unix.prog/browse_thread/thread/e8f8edf4f2f2447b/?hl=ru
Что касается socket, listen и пр. — они опущены для ясности. Да-да, и функции read() в современных версиях Windows тоже нет (есть _read и ReadFile). Извините, статья и так большая. Если указывать все функции, которые вызываются по 1-2 раза… ну вы поняли.

FSM не назван как FSM, но описан в части 4. Просто в современных реалиях имеется по 8-16-32 ядер в каждом сервере, и чистый однопоточный FSM неактуален. А несколько работающих параллельно и асинхронно автоматов… наверное, это сеть Маркова, но надо смотреть детали.
Хм, да, про FSM — слоника-то я и не заметил, извините :)
«Нечистый FSM» описан по ссылке, что я дал выше.
Никогда так не думал про связь процессов и прав почему то. Думал, что это чтоб при падении одного процесса не валилась вся программа.
Дал почитать приятелю статью, мол смотри как всё классно разжевали и в рот положили. А он в ответ говорит, что много лет назад для ввода-вывода предпочитал использовать как-раз unix select или WinSock2. В веб-программерской среде сейчас столько говорят про реализации серверов на неблокирующих сокетах и асинхронных задачах. А всё это новое есть ещё не позабытое старое.
Вот именно, не забытое. Я первый раз использовал select в 2002-ом. Просто сейчас все приложения выходят в интернет, соответственно всем так или иначе нужно организовывать серверные демоны. И тут вылезают всякие разные вопросы.
плюс высокие нагрузки заставляют людей заниматься оптимизацией и разбираться с системой
Ссылку добавил в список. Но имхо вы, батенька, как-то уж очень сурово сам с собою)
Но в большинстве статей не объясняется, почему именно такой способ используется в том же Apache.
Одна из причин указана Стивенсоном: процессы — более надежное звено нежели потоки, крах одного потока приведет к краху всего приложения.Разработчики группы Appache ставили в основу критерий — надежность WEB сервера
UFO landed and left these words here
UFO landed and left these words here
Ну, клинч есть устоявшийся в русском языке термин, пусть и из бокса. Дедлок пока нет.
Честно говоря, термин «клинч» как-то озадачил при прочтении (про бокс, кроме того что в нём есть «нокаут», «нокдаун» и «хук» ничего не знаю), а вот с термином «дедлок» встречался даже когда параллельным программированием даже не интересовался.
Почти полностью переписал раздел про асинхронный ввод/вывод, спасибо комментаторам.
Прекрасно описано, с огромным удовольствием прочитал.
Socket.Poll Method


+

Имеется в виду групповой poll(), см. man poll(2)


=

«эффективные реализации polling'а на сегодняшний день имеются лишь в *nix-системах»
Вряд ли когда нибудь уже займусь столь низкоуровневым программированием, но некоторые «холивары» на тему настройки веб-серверов и СУБД стали намного понятнее. Вернее появилось осмысление почему эмпирически полученные значения являются оптимальными для некоторых условий и не являются универсальными
Блин, забыл дописать последней строчкой «Спасибо» — исправляю склероз

Спасибо за статью!
> Исторически одной из первых ОС с поддержкой асинхронного ввода-вывода стала Windows 2000.
Насколько я помню, асинхронный ввод-вывод и OVERLAPPED появились ещё в Windows NT 4.0, но этот динозавр уже не упоминается в текущей версии MSDN.

Ну и немного поправок/уточнений по поводу асинхронного i/o в windows.

1) callback-функции, передаваемые в функции чтения/записи — это отдельный механизм, называемый APC (Asynchronous Procedure Calls). Его можно использовать независимо от i/o и callback-функции работают через него. APC работает в пределах одного потока: асинхронный вызов можно назначить только в контексте одного конкретного потока. В случае i/o вызов будет сделан в контексте потока, инициализировавшего i/o.

2) IOCP != OVERLAPPED. IOCP — это интегрированный с планировщиком потоков механизм, который позволяет организовать эффективный пул потоков для работы с OVERLAPPED i/o на многопроцессорных машинах. По сути это thread safe очередь, в которую можно помещать элементы, даже не обязательно связанные с i/o (PostQueuedCompletionStatus). Туда же помещаются результаты операций OVERLAPPED i/o. Несколько потоков в цикле получают элементы из очереди и обрабатывают эти элементы. При этом:

a) Если несколько потоков ждут очередного элемента, то приоритет имеет поток, который последним запросил элемент. Это позволяет не тратить время на усыпление/пробуждение потока в случае, когда элемент очереди появился в момент запроса работающим потоком.

b) Потоки, хоть раз запросившие элементы из очереди, считаются ассоциированными с этой очередью. Планировщик потоков работает с ними специальным образом. Если на машине N процессоров, а в пуле M потоков, и M > N (microsoft рекомендует делать M = N * 2), то не более N потоков будут пробуждены независимо от того, сколько элементов оказалось в очереди — это позволяет минимизировать количество переключений контекста потоков на одном процессоре. Если по каким-то причинам один из этих N потоков уснул не при запросе очередного элемента (например на событии), то планировщик пробуждает один из оставшихся потоков, ждущих очередного элемента очереди.
Всё это позволяет добиться практически максимальной производительности на данном железе.

Вообще, мужики из microsoft двигаются в правильном направлении. в win2k, например, не было функции ConnectEx, позволяющей асинхронно установить исходящее соединение, приходилось создавать отдельный поток. В xp/2003 эта функция появилась, плюс появилась DisconnectEx, позволяющая переиспользовать сокет для нескольких подключений и экономить на вызове socket. Была ещё одна проблема — получение адреса по имени хоста было только синхронным (getaddrinfo), в vista/2008 добавили асинхронную версию (GetAddrInfoEx). API постепенно улучшается и это радует.

P. S. А вообще, boost.asio решает проблемы с эффективными кросплатформенными сетевыми приложениями ;)
Не подскажете, как асинхронно использовать GetAddrInfoEx()? В MSDN сказано, что асинхронные параметры зарезервированы и не поддерживаются :(
Мда, ошибочка вышла. Параметры то есть, но не работают. Я, когда смотрел новшества API, описание параметров не посмотрел — думал и так всё понятно.
> Если в обычном, «блокирующем» режиме функция read читает из файла столько байт, сколько заказал программист

read() может прочитать меньше чем заказано.

man 2 read:

RETURN VALUE
On success, the number of bytes read is returned (zero indicates end of file), and the file position is advanced by this number. It is not an error if this number is smaller than the number of bytes requested; this may happen for example because fewer bytes are actually available right now [...]
Only those users with full accounts are able to leave comments. , please.