Comments 58
UFO landed and left these words here

Динамическую (неоднородную) нагрузку, скорее всего, покажу в статье про I/O проактор, где и сравню его с обычным I/O реактором. Тут проблема ещё в том, что вместе с нашим кодом мы будем измерять производительность библиотеки TLS, базы данных, логгера и т.д.

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

Реализуете все это на С, и удивитесь, что какой-нибудь .Net Core из коробки работает быстее.

Веб-сервера так и меряются производительностью.
.Net нет причин быть медленнее, как ниже заметили, сейчас все сервера так работают.

>Реализуете все это на С, и удивитесь, что какой-нибудь .Net Core из коробки работает быстее.

Написал как-то микросервис на С для контроля контроллеров =), libressl, простейший парсинг, реактор модель. Там всего с пяток страниц кода.
Ради интереса сделал нагрузочный тест. Сопоставим по скорости обработки на 2-3 тыс конкурентных запросов был только минимальный koa.js на node.js, ну так там libuv под капотом.
Net Core и рядом не стоял.
Как это прекрасно сравнивать код 5 страниц с полнофункциональным веб-фреймворком. Если все отрезать — node.js медленнее dotnetcore
>Как это прекрасно сравнивать код 5 страниц с полнофункциональным веб-фреймворком.

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

Мне продолжать или вы поняли, где тут проблема?

П.С. Это шутка, если что.
>Мне продолжать или вы поняли, где тут проблема?

проблемы не наблюдаю. Есть цели, есть время-бюджет-качество.

В отличие от приведенного вами сказочного примера мой код с реактор паттерном работает в предприятии уже 4 года, и работает как швейцарские часы.
Его я использовал повторно пару раз, но уже под менее критичные цели.

Зачем перегруженный код с кучей если не косяков, то всяких эффектов, если достаточно обычного пула c простейшим парсером и обработчиком на С?

Я начал сначала писать на базе boost asio, потом уперся в его непонятный глюк, и написал на голом С с pthreads & kqueue. Фактически, получился очень упрощенный вариант libuv, но с понимаемым поведением. =)
Таки real time типа.

Код первиной обработки и упаковки данных с контроллеров отдельно, это еще на пяток станиц.

Да, ради хохмы я прицеплял postgreql, с разными простыми и не очень запросами.
Ну, время на ответ увеличилось на время обработки транзакции.
Надо совсем больше клиентов — сделаем больше thread pool.
Среда-то контролируемая. Но я пробовал на 1000-1500 одновременных потоков — куда уже больше для микросервиса, где и 10 за глаза хватит?

Код, извините, не могу опубликовать, из элементарных соображений.
Да, я мог и могу написать с-модуль для perl/ruby/node и к нему приписать web, но зачем городить огород? KISS его, мазефака. =)

Хорошо, я отмечу, где тут проблема.

Фреймворк общего назначения позволяет решать проблемы в комплексе, за счет снижения общей производительности. Весь поинт в том, что за 5 страниц кода вы никаким боком не покроете и 10% того, что идет из коробки в каком .Net Core.
Я нисколько не спорю, что кастомный код может покрывать потребность предприятия на 100%, однако писать его дольше, уровень знаний нуже выше, а повторно использовать его за пределами узкого направления — нельзя. При этом, как правило, производительность существующих решений более чем достаточна.
Если нужен конечно real-time, то тут и требования иные.
>Хорошо, я отмечу, где тут проблема.…

И далее пересказ «маленькие по три, но маленькие. большие по пять! но большие!» =)

Еще раз: при планировании проекта учитываются требования и ограничения.
Ограничения: real time, C интерфейс контролеров, ограниченные средства разработки.
Все. Все node, python, perl, ruby,… идут лесом.

>кастомный код может покрывать потребности
>производительность существующих решений более чем достаточна.

открою секрет, на автоматизированном предприятии всегда дофига кастомного кода.
потому что существующих решений есть только на 90-95% от кода.

>уровень знаний нуже выше

простите, не моя печаль.

> повторно использовать его за пределами узкого направления — нельзя.

ну вот зрасте. Я тут давеча этот же код реактора к контролеру батареи на ком прикрутил. И уже успел дать в ухо тому кто залез внутрь и отвалить разьем =)

У автора как раз очень интересная задача на простом примере показать, из чего состоит I/O Reactor. Это не про тестирование на динамической нагрузке, а про алгоритмы и upper bounds.

Тем не менее, результаты тестов производительности сферического коня в ваакуме от нагрузок из реального мира сильно отличаются. Думаю, что стоило или сделать тест со сторонними компонентами (БД, логгер, etc), или не делать тестов вовсе.


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

нет. Тест с БД проверит в основном производительность БД. В случае postgres, например, там вообще process-per-connection. Так что и сама БД тоже должна быть реактивной в таком случае, монга какая-нибудь.

Для нагрузки в миллионы запросов в секунду, наверно, подойдет бинарный протокол, а не HTTP, чтобы не тратить время на парсинг.

добавив работу с БД, мы померяем производительность БД, а не сервера.
В моем понимании вся магия в reactor_run()
while (true) {
...
    int nfds =
        epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed);
    switch (nfds) {
...
        case event:
            callback(event);
...
    }
}

Ну вроде, да, так работают вообще все современные (epoll/kqueue) сeрверы под капотом и это всем известно. В async фреймворках и/или специализированных языках этот цикл просто обмазан слоем абстракций разной толщины для удобства пользования. Или что я упускаю?

В целом, да. Разве что в I/O проакторе блок с вызовом событий заменяется хитрым планировщиком с пулом потоков для исполнения обработчиков событий.

А мне понравилось, как лабораторная работа по epoll.
Понятно, что в настоящем сервере нужно ещё много всего, но надо же с чего-то начинать.
И ни одного упоминания nginx ;)

Если добавить планировщик для таймеров (priority queue, из которой берётся следующий тайм-аут на epoll), то получится вполне рабочее ядро для простенького однопоточного сервера.

Боюсь, что вы в этом понимаете больше, чем я, но я бы посоветовал сделать следующее. Запустите в одной консоли тест, а в другой помониторьте состояние сокетов (количество по статусу).

Может быть у вас сокеты подвисают в состоянии ожидания закрытия, и поэтому увеличение кол-ва приводит к наблюдаемому эффект падения производительности.

Последний раз я тюнил сетевой код на Java два года назад, поэтому пишу без подробностей, испытывая ностальгические чувства)
Судя по результату — тут нет закрытия сокетов (keep-alive), иначе производительность была бы в 10-100 раз меньше.
Есть важная деталь — пользовательский код тоже должен быть неблокируемым. И если он получает данные с диска (сюрприз) — то в линукс это сделать было до последнего времени нельзя. Только костыли в виде тех же тредпулов, которые тоже не спасают от полного вставания колом из-за одного тупящего или перегруженного диска, даже nginx. Как отстоят дела с асинхронным дисковым i/o в последних ядрах? Почему разработчики ядра упорно игнорируют то, что давно есть во FreeBSD и даже в Windows?
UFO landed and left these words here
UFO landed and left these words here
Довольно поверхностный и типичный подход. Уж извините.
1. Добавьте для сокетов SOCK_CLOEXEC опцию
2. Хендлите ситуацию закрытия\подвисания сокета (EPOLLRDHUP | EPOLLHUP)
3. Почитайте про edge\level triggered, дайте другим сокетам право на жизнь избавясь от лишнего вызова while(read(...)) => EAGAIN || EWOULDBLOCK
4. ну и надеется на то что в вашем http сервере, все запросы\ответы будут ограниченной длины в 1024 байт — наивно. Тут у вас и появятся вопросы с аллоцированием\реалоцированием памяти, просадки
5. http сервер, не ограничивается только приемом данных, данные нужно как-то обработать, выше писали, про тот же парсинг и т.д.

N. А есть медленные клиенты, почитайте про TCP_KEEPALIVE
N+1. позже вы дойдете, до того, что не все клиенты честные, они откроют десятки тысяч соединений и на вашем сервере закончатся сокеты. Подумайте над этим когда будете развивать ваш проект
  1. Я пробовал с EPOLLET делать (секция «Второй заход»), это ощутимых результатов не дало (но должно проявится с парсером, БД и т.д.). Level-Triggered идёт по-умолчанию в epoll, поэтому этот цикл чтения убрать можно.

Спасибо аз советы.

3. Если сокет создается с NONBLOCKING, тогда можно и в цикле и в Edge-Triggered. Вроде он быстрее.

Можете привести пример использования? Из списка публичных функций пока неясно как Вашу библиотеку можно использовать.

Вот пример как хостить простой todomvc приложение.

> make
> bin/file-server samples/todomvc/
# open browser and navigate to http://localhost:8080

Идея состоит в том что на голом C пишете код вроде

while (condition) {

	io_stream_read(stream, buffer, sizeof(buffer), 0);
	io_stream_write(stream, RESPONSE, sizeof(RESPONSE) - 1);
}

и этот код будет работат в этквиваленте следующего кода на C# или JavaScript

while (condition) {

	await ReadAsync(stream, buffer, sizeof(buffer), 0);
	await WriteAsync(stream, RESPONSE, sizeof(RESPONSE) - 1);
}

без каких либо препроцессоров или других модификаторов кода.
Requests/sec: 665789.26
Вам удалось выяснить во что упирается производительность? Сколько CPU используется (при 80% и 100% производительности)?
Я производил подобный тест — на максимуме производительности используются не все ресурсы, поэтому пришел к выводу, что упирается в сетевой стек, может локи внутри epoll или т.п.

Ещё я заметил, что более 60% CPU уходит на отправку (send), есть идея вынести recv и send по разным epoll, что может дать ещё производительности.

Как вариант, вместо SO_REUSEPORT, можно использовать один (отдельный) поток/epoll для приема всех соединений, и потом уже передавать в менее нагруженный epoll.
Вам удалось выяснить во что упирается производительность? Сколько CPU используется (при 80% и 100% производительности)?

top показывает числа в районе 400% нагрузки ЦПУ (с wrk и http_server_multithreaded). Как версия с прибитыми к ядрам потоками, так и простой http_server_multithreaded.


Я производил подобный тест — на максимуме производительности используются не все ресурсы, поэтому пришел к выводу, что упирается в сетевой стек, может локи внутри epoll или т.п.

Я тоже думаю, что упирается в сетевой стек Linux.


Как вариант, вместо SO_REUSEPORT, можно использовать один (отдельный) поток/epoll для приема всех соединений, и потом уже передавать в менее нагруженный epoll.

Идея, но это уже будет нечто похожее на проактор.

Модифицированная версия выделяет фиксированное число потоков (thread pool), тем самым не позволяя системе аварийно прекратить исполнение, но вместе с тем привносит новую проблему: если в данный момент времени пул потоков блокируют продолжительные операции чтения, то другие сокеты, которые уже в состоянии принять данные, не смогут этого сделать.

Тут есть ещё одна проблема (или другая сторона той же проблемы): число потоков для такой архитектуры подобрать трудно. Если потоков будет слишком мало, то пул часто будет истощаться из-за того, что они ожидают завершения блокирующих операций ввода/вывода. Если много, то избыточное число активных потоков приведет к потерям на частое переключение между ними.
Для эффективной реализации парадигмы пула потоков нужен механизм в ОС, который, даже при наличии задач в очереди обработки, реализует запуск на обработку только ограниченного (например, равного числу ядер в системе) числа активных потоков, меньшего полного числа потоков в пуле. Такой механизм должен отслеживать число активных (не заблокированных на вводе/выводе) потоков, и если их становится мало, запукать обработку в дополнительных потоках из пула. При наличии такого механизма истощение пула потоков становится менее вероятным, при том что потери на конкуренцию между активными потоками остаются на приемлемом уровне, т.к. число активных потоков привышает желательное только на короткое время.
В Windows такой механизм — IO сompletion port — есть уже довольно давно. Но вот в часто используемом для создания веб-серверов Linux с таким механизмом, насколько мне известно(если я ошибаюсь — просьба поправить), есть (или были до недавнего времени) проблемы, а потому подход с пулом потоков в Linux работает (или работал) сильно хуже событийно-ориентированного неблокирующего подхода (как в этой статье).

PS А ещё меня в очередной раз умилил пример «проектирования на основе паттернов» (наиболее заметной частью которого IMHO является назначение красивых имен давно известным приемам программирования) из названия статьи: в данном случае красивым словом Реактор(Reactor) названо давно и хорошо известное (в частности, GUI Windows реализован именно в этой парадигме) событийно-ориентированное программирование. С одной стороны, конечно, не могу отрицать полезность паттернов — красивые имена повзоляют легче запоминать приемы (действительно полезные), но вот с другой стороны не умеющему «в паттерны» трудно сразу понять, что речь на самом деле идет о чем-то хорошо ему знакомом.

Ну нет, реактор — это не СОП. Всё-таки надо различать общий принцип и обособленную часть программы.

Объясните, пожалуйста, что вы считаете в данном случае обособленной частью программы.
В данном случае вы заблуждаетесь. Reactor pattern(та самая ссылка, которая в статье) — это именно общий принцип, «паттерн»(шаблон проектирования, если по-русски), «повторяемая архитектурная конструкция, представляющая собой решение проблемы»(из Википедии).
А частью программы (реализацией этого общего принципа), причем, в данном случае — коренной, составляющей основную её логику, являются struct reactor и работающие с ними функции в программе автора.
В более сложных программах разные её части могут реализовывать разные общие принципы. Например, в одной и той же программе для современных (в смысле не 3.x) версий Windows может быть, например, как графическая часть на основе событийно-ориентированного программирования, так и логика обработки подключений клиентов по сети с использованием Winsock, использующая другую парадигму. Например — выделенные потоки для каждого клиента, такие программы можно легко и просто клепать в Delphi: там графические и сетевые компоненты для накидывания на форму берутся из одной кучи.
А частью программы (реализацией этого общего принципа), причем, в данном случае — коренной, составляющей основную её логику, являются struct reactor
Ну, понятно: получается, что под словом «реактор» мы просто имели в виду разные вещи.

Вот неистово плюсую. В нашу молодость это никакими "реакторами" не называлось, да и вообще никак особо не называлось, а было просто нормой. Хоть классическую библию Стивенса по сетевому программированию взять, просто неблокирующиеся сокеты, да и всё (и кстати, в статье гвоздями прибито к epoll, не лучшей реализации, хотя на картинке их было больше — почему не libevent было рассмотреть хотя б?). Складывается впечатление, что чем больше людей и проектов в отрасли, тем больше им нужно как-то оправдывать своё существование громкими словами.

А какая разница? Главное, что он кроссплатформенный, и абстрагирует еще ряд нужных в процессе полезных вещей.

Вот неистово плюсую. В нашу молодость это никакими "реакторами" не называлось, да и вообще никак особо не называлось, а было просто нормой. Хоть классическую библию Стивенса по сетевому программированию взять, просто неблокирующиеся сокеты, да и всё

Мне больше нравятся термины "однопоточный цикл событий" и "I/O реактор". Второй легче звучит и проще запомнить. Неблокирующие сокеты, ИМХО, неполностью описывают саму суть подхода, т.к. можно пользоваться неблокирующими сокетами и без самого цикла событий.


и кстати, в статье гвоздями прибито к epoll, не лучшей реализации, хотя на картинке их было больше — почему не libevent было рассмотреть хотя б?

Потому что libevent — это реализация I/O реактора, а не системный селектор, и тоже далеко не самая лучшая.

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


т.к. можно пользоваться неблокирующими сокетами и без самого цикла событий.

Ну, попытайтесь =) Неизбежно будет некое подобие такого цикла в итоге. Или можно было сразу прочитать Стивенса и знать, что и как имеется, и какие подводные камни.


тоже далеко не самая лучшая.

Потому и "хотя б", только это не имеет особого значения уже.

>CallbackData *callback = g_hash_table_lookup(reactor->table, &fd);

Не лучше ли сразу держать указатель на коллбэк в events[i].data.ptr и обойтись без лишнего поиска в хэше?
UFO landed and left these words here
По сути разница только в том, что вместо поиска CallbackData* в хеше ты будешь получать этот указатель из epoll_event.data.ptr.

Как я его буду получать, например, если я хочу уничтожить реактор?

UFO landed and left these words here
Зато поиска по хешу не будет.

Хм, а в чём проблема поиска по хешу? Вы про случай, когда два объекта имеют одинаковый хеш?

Говоря про недостатки событийного подхода, думаю имеет смысл упомянуть те, которые поднимались в академической среде в конце прошлого/начале текущего столетия и описаны в работе «Why Events Are A Bad Idea (for high-concurrency servers)»
Это stack ripping и inversion of control

Альтернатива — сопрограммы и легкие потоки, реализованные на их основе.
Они дают удобство написания thread-based подхода и скорость работы за счёт асинхронности «под капотом» и той самой популярной ныне кооперативной многозадачности, которая многим нравится в golang, kotlin и т.д.
В них можно полностью отказаться от привычных примитивов синхронизации типа мьютексов и условных переменных.
Хорошо описано в цикле статей «Асинхронность: назад в будущее» Григория Демченко
Only those users with full accounts are able to leave comments. Log in, please.