Работа с сетью

maksimus1210
maksimus1210

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

34
рейтинг
12 комментариев
yndx-antoshkka
Люди уже работают над этим, в этом году надеюсь что будет имплементация в нескольких компиляторах: open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4626.pdf

Вы можете помочь в развитии, если попробуете текущий прототип github.com/chriskohlhoff/networking-ts-impl и скажете, чего вам в нём не хватает, какие ошибки нашли.
yndx-antoshkka
h4tred
yndx-antoshkka, лично мне, очень бы хотелось иметь оторванное от асинхронности API для более абстрактной работы с сетью. А данный API базируется на Asio и, по сути, прогибается под Windows IOCP, навязывая проактора на тех платформах, на которых его нет, т.е. на всех, кроме Windows (если ошибаюсь - поправьте).
h4tred
yndx-antoshkka
h4tred, никто ни под кого не прогибается. Исследуйте пожалуйста внимательно документ cs.wustl.edu/~schmidt/PDF/proactor.pdf

Синхронный интерфейс в n4626.pdf так же присутствует.
yndx-antoshkka
h4tred
yndx-antoshkka, детально ещё изучу, но даже беглый просмотр:
> On real-time POSIX platforms, the asynchronous I/O functions are provided by the aio family of APIs [9].

Posix AIO на том же Linux реализован на User-level в виде пула потоков (про Linux AIO/libaio слышал, но там свои нюансы). Не знаю как покажет себя на больших нагрузках. На macOS вроде только для файлов, не для сокетов. Можно делать эмуляцию поверх epoll, не сложно, но тоже снижает эффективность: вытянутые native_handle() из сокетов Asio, подсунутые в libev + синхронный интерфейс (с nowait) показали куда лучший результат, чем так же логика, полностью реализованная средствами Asio (асинхронный интерфейс). Тестировалось на небольшом HTTP 1.1 сервере. Ещё смутило обилие аллокаций памяти: они случаются на каждый асинхронный вызов (про свои аллокаторы знаю, но это другой вопрос).

В любом случае, AIO не используется в Asio, используется дополнительный слой абстракции поверх существующих механизмов. Если грубо: асинхронный интерфейс Asio нативно ложится на Windows IOCP, но требует дополнительных затрат ресурсов и менее эффективен поверх epoll/kqueue и иже с ними. Потом и ляпунул про "прогиб". Хотя, ЕМНИП, Скрис изначально Asio начинал на Windows писать, это может несколько объяснить выбор интерфейса библиотеки...

Как минимум, в данной схеме трудно реализовать подход: дождаться события на сокете, пусть будет чтения, запустить обработчик, узнать количество данных (не портируемо, но kqueue эту информацию может передать вместе с эвентом, в Linux - дополнительный сискол), после чего выделить блок памяти нужного размера и за один присест прочитать все данные. Или выделить дополнительный блок памяти и прочитать при помощи readv (не портируемо, но оптимально). Или переиспользовать текущие свободные буфера. В Windows IOCP и Asio ты передаёшь блок памяти сразу в обработчик и если данных больше, чем блок, нужно снова запускать асинхронную операцию. В контексте Asio это ещё и аллокация памяти на вызов. В kqueue ещё можно и чтенеие не делать, если есть ошибка и/или данных ноль - эта информация тоже есть в эвенте.

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

PS в этом отношении интерфейс Java NIO мне больше нравится.
h4tred
h4tred
Опечатался в имени: Крис.
h4tred
maksimus1210
Давно пользуюсь библиотеками Qt, на мой субъективный взгляд это лучшее решение для С++ на сегодня, их сокетами пользоваться очень легко, почему бы не взять их за основу.
maksimus1210
yndx-antoshkka
h4tred, могу сказать что Крис и другие разработчики из WG21 в полной мере осознают какой платформой большинство людей пользуется для написания серверов: "we talked about the "ultra-low latency, high throughput" systems that are typically encountered in the capital markets space and stated that such systems make heavy use of techniques such as coroutines in order to meet their demands. Several of the authors of this paper have extensive experience in that space. One aspect of those systems is that, by far, the most common deployment platform is Linux." (цитата из open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0158r0.html)

> Как минимум, в данной схеме трудно реализовать подход: дождаться события на сокете.

Мне кажется мы расходимся в терминологии. Давайте разберем на примере: у вас 100 сокетов, и сразу произошло 16 событий. Как вы предлагаете их обрабатывать в вашей схеме?
yndx-antoshkka
h4tred
yndx-antoshkka,

> Мне кажется мы расходимся в терминологии. Давайте разберем на примере: у вас 100 сокетов, и сразу произошло 16 событий. Как вы предлагаете их обрабатывать в вашей схеме?

Небольшое допущение: сервер однопоточный и используем epoll.

0) регистрируем сокеты в epoll при помощи epoll_ctl() (тоже допущение: сокеты уже откуда-то взялись)
1) epoll_wait выходит из ожидания, и возвращает число событий - 16
2) _последовательно_ просматриваются события в структуре events, переданной epoll_wait и запускаются обработчики
3) в обработчике для каждого сокета сделать то, что я описал выше: определение размера, вычитывание всех данных.

Собственно первый пункт - это и есть "дождаться события на сокете".

В случае Asio, если заглянуть во внутрь, различия начинаются с 3го пункта:
3) запускается внутренний обработчик (пусть будет чтение из сокета) и читает указанное число байт в указанный пользователем буфер
4) запускается пользовательский обработчик, куда передаётся (по памяти) число реально прочитанных байт и код ошибки.

Так как буфер мы должны передать до возникновения события, мы реально не можем узнать сколько данных в буфере ядра, и не можем выполнить:
1) переаллокацию буфера под нужный размер (не сильно оптимально)
2) взять ещё один буфер для readv (оптимально, если есть пул буферов)

и прочитать данные за _один_ системный вызов read/readv.

Если мы не угадали с размером буфера, нам придётся делать ещё одну операцию асинхронного чтения, как я писал выше - плюс аллокация памяти.

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

Ну и слово _последовательно_ в пункте 2) я тоже не просто так выделил. Дабы не возникло ощущения, что в Asio заполнение буффера данными будет происходить на уровне ядра и одновременно (или почти). Это верно (насколько хватает моих знаний) только для Windows. В любом случае, в однопоточном сервере, колбеки будут вызываться последовательно.
h4tred
h4tred
yndx-antoshkka, я тут подумал... по сути, мало что мешает в текущей реализации Asio добавить интерфейс для асинхронного чтения (для записи и так буфер передавать нужно), в котором не нужно передавать буфер, а только факт запроса готовности сокета и соответствующий колбек. Правда опять: это не решит проблему с дополнительными аллокациями на запрос.
h4tred
yndx-antoshkka
> Небольшое допущение: сервер однопоточный и используем epoll.

Это очень необычные требования, переводящие ваше серверное приложение в разряд экзотических.

Обычно серверные приложения многопоточны и тогда проактор - идеальная архитектура, которая даёт возможность наиболее быстро, с минимальными задержками и равномерной нагрузкой обрабатывать запросы. Типичный пример - http сервер. Вы как разработчик заинтересованы в том, чтобы как можно скорее обработать запросы с как можно меньшими задержками. Когда пользователь запрашивает страницу, браузер может открыть несколько соединений и запрашивать разные ресурсы - поэтому ситуация, когда у вас сразу 16 событий случается на сервере - норма. Если вы будете обрабатывать каждое событие по пол секунды последовательно, как в акторе - пользователь прождёт 8 секунд и будет недоволен. Если примените проактор и обработаете в 16 потоков - пользователь прождёт пол секунды.

НО при всё при этом, реализовать на ASIO предложенную вами схему можно, и достаточно просто:
* async_read_some в null_buffers()
* std::size_t bytes_readable = socket.available();
* аллоцируете буферы, или комбинируете их например в boost::array<mutable_buffer, 3> bufs и вызваете sock.read_some(bufs); который прочтёт их за один системный вызов readv

Есть даже пример в оффициальной документации: boost.org/doc/libs/1_63_0/doc/html/boost_asio.html#boost_asio.overview.core.reactor

P.S.: Подобный подход c null_buffers() (но многопоточный) я давным давно использовал для асинхронной работы с PostgreSQL: оправлял запрос, получал нативный сокет соединённый с базой данных, ждал когда появится нужноe количество байт ответа (асинхронно читал в null_buffers()) , после чего из сокета читал через API Postgres.
yndx-antoshkka
h4tred
yndx-antoshkka,
> Это очень необычные требования, переводящие ваше серверное приложение в разряд экзотических.

Это допущение. Всё это примерно одинаково делается и в несколько потоков (обычно по числу ядер). Зачем усложнять объяснение?

> Если вы будете обрабатывать каждое событие по пол секунды последовательно, как в акторе - пользователь прождёт 8 секунд и будет недоволен. Если примените проактор и обработаете в 16 потоков - пользователь прождёт пол секунды.

Так, опять про разные вещи говорим. Проактор в однопоточном режиме будет так же выполнять запросы последовательно. Ровно как актор вполне может их обслуживать в несколько потоков. Повторюсь, в Asio на Linux используется epoll, который актор по своей природе.

> НО при всё при этом, реализовать на ASIO предложенную вами схему можно, и достаточно просто:

А вот этот момент я как-то упустил. Спасибо. Нужно будет погонять на пробном сервере (http, http_parser от nginx). На данный момент по числу RPS такой расклад был (сервер - многопоточный):
1) libev (C++ интерфейс) + нативные сокеты - самый быстрый, получилось около 540k RPS при 4х потоках сервера, 4 потоках клиента и 100 единовременных подключений. На 10k - около 300k.
2) libev + Asio абстракция и вытащенные native_handler - немногим хуже предыдущего пункта, но удобнее в плане абстракций над сокетами. При условиях выше показывал где-то на 50k запросов меньше.
3) Asio (я standalone использовал) - в базовой реализации вообще сдулся (10-20k запросов), после включения трюка с TCP_NODELAY ретировался, но показал себя где-то раза в два медленнее.

Понятно, что выкладки выше - вода без конкретных цифирь, но сейчас их найти не могу (точнее остались несуразные выкладки для разных условий тестирования). Плюс запускалось на localhost, а не в реальной сети. Попробую перепроверить в ближайшее время, тем более, что повод появился:
> Есть даже пример в оффициальной документации: boost.org/doc/libs/1_63_0/doc/html/boost_asio.html#boost_asio.overview.core.reactor

Так что пока у меня остаётся только претензия на аллокации памяти при каждом асинхронном запросе.
h4tred
yndx-antoshkka
h4tred,

> 3) Asio (я standalone использовал) - в базовой реализации вообще сдулся (10-20k запросов), после включения трюка с TCP_NODELAY ретировался, но показал себя где-то раза в два медленнее.

Скорее всего у вас есть класс, который отвечает за работу с установленным соединением и содержит сокет. Добавьте в этот класс небольшой массив slab и перенаправляйте все аллокации в этот класс, чтобы он аллоцировал из slab если может. Держите пул инстансов, чтобы устанавливать соединения быстрее. При таком подходе производительность возрастёт где-то на порядок или на два.

P.S.: будет очень круто, если кто-то напишет статью на Хабр о том, как выкрутить производительность ASIO на максимум. Если понадобится помощь - пишите.
yndx-antoshkka
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).