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

maksimus1210
maksimus1210

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

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

Вы можете помочь в развитии, если попробуете текущий прототип https://github.com/chriskohlhoff/networking-ts-impl и скажете, чего вам в нём не хватает, какие ошибки нашли.
yndx-antoshkka
h4tred
yndx-antoshkka, лично мне, очень бы хотелось иметь оторванное от асинхронности API для более абстрактной работы с сетью. А данный API базируется на Asio и, по сути, прогибается под Windows IOCP, навязывая проактора на тех платформах, на которых его нет, т.е. на всех, кроме Windows (если ошибаюсь - поправьте).
h4tred
yndx-antoshkka
h4tred, никто ни под кого не прогибается. Исследуйте пожалуйста внимательно документ http://www.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." (цитата из http://www.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

Есть даже пример в оффициальной документации: http://www.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
Marat Abrarov

yndx-antoshkka,

> будет очень круто, если кто-то напишет статью на Хабр о том, как выкрутить производительность ASIO на максимум. Если понадобится помощь - пишите.

Есть Million RPS Battle и есть идеи того, что в теории можно рекомендовать при использовании Asio на *nix и на Windows (например, как соотносится Why does one NGINX worker take all the load? с Asio). К сожалению, нет железа (включая быструю сеть), где можно было бы проверить, что из теории действительно работает, а что можно опустить в угоду более простому коду.

Понятно, что "заточенное" решение на том же C & epoll "уделает" Asio, отягощенную некоторыми абстракциями и необходимостью обеспечивать гарантии, которые эти самые абстракции предполагают (для удобства пользователей Asio – за удобство рано или поздно приходится платить).

Непонятно, насколько большой overhead дает Asio (по сравнению с C & epoll) – стоит ли он этих самых удобств (относительная кроссплатформенность)?

Насколько я помню список рассылки Asio и release notes – кажется, что автор пытался улучшить производительность Asio на Linux и уменьшил кол-во мест, где используются блокировки. Рецепт io_service with BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO + io_service instance per worker thread должен работать. Где бы проверить…

Marat Abrarov
Обновлено 
yndx-antoshkka

> Есть Million RPS Battle

Там, как и в подавляющем количестве примеров в интеренете, не оптимальная работа с ASIO: в этот класс надо добавить slab аллокатор и использовать его в асинхронных методах; надо нормально настраивать сокеты (например добавить no_delay(true) при первом получении сокета), да и плюс там UB при использовании acceptor.

 

yndx-antoshkka
Marat Abrarov

> надо добавить slab аллокатор

Во-первых, непонятно, как это поможет производительности на практике (тот же Yandex использует что-то вроде tcmalloc и ему хватает производительности... хотя там это вызвано характером нагрузки и trade-off между сложностью и теоретической эффективностью).

Во-вторых, есть https://github.com/virtan/mrps/pull/3 (что там не так с acceptor? можно и это поправить) и мои локальные тесты не показывают выигрыша больше погрешности, когда используется Asio custom memory allocation (в собственным ma_echo_server я реализовал еще и параллельные чтение и запись, но и это не помогло в моих локальных тестах). Думаю, можно еще преаалоцировать сокеты для входящих соединений, хотя я уже выключил mutex на io_service (за счет concurrency hint), который используется для per-io_service-socket-implementation-registry, так что overhead от Asio уже сведен почти на нет.

Насчет TCP_NODELAY на стороне сервера - эта опция включена на стороне тестового клиента (один для всех тестов и написан тоже на Asio), а на стороне сервера, даже в конкурирующих решениях эта опция вроде как не включается (явно). Значит, "нечестно" включать TCP_NODELAY и в сервере на Asio (c++-virtan)

Marat Abrarov
yndx-antoshkka

>> надо добавить slab аллокатор

> Во-первых, непонятно, как это поможет производительности на практике

На старом glibc это ускоряло сервер приблизительно в три раза.

> Во-вторых, есть https://github.com/virtan/mrps/pull/3

Супер! Что с производительностью, по сравнению с epoll / libev ?

yndx-antoshkka
Marat Abrarov

> На старом glibc это ускоряло сервер приблизительно в три раза.

Вот тут (см. комментарии) пишут о "в 2,5 раза" при Ubuntu-11.04/AMD Phenom 9650/4096Gb/gcc-4.5.2/boost-1.47.0 и "при полностью съеденных 4ех ядрах"

> Что с производительностью, по сравнению с epoll / libev ?

Еще не дошел до такого сравнения, но тот же c++-virtan, что до pull request, что после показывает приблизительно одинаковые результаты. Разница в пределах погрешности. Полагаю, что мои условия тестирования - VMware Workstation на Windows 10 + Docker + запуск и клиента, и сервера на одном и том же железе - не позволяют "раскрыться" тестируемому образцу.

P.S. Что-то не приходят уведомления, хотя я подписан на комментарии. Не сигнал ли это о том, что пора перевести данную дискуссию в другой формат и на другую площадку (GitHub, email)? Хотя бы до появления результатов ("кто-то напишет статью на Хабр о том, как выкрутить производительность ASIO на максимум")

Marat Abrarov
Hare76
Полностью поддерживаю идею, что хотелось бы иметь классы для работы с сетью. Но если в них будет навязана асинхронность из буста, то лично мое мнение, что это будет только очередным камнем в огород очередного стандарта C++. Зачем смешивать одно с другим? Нет необходимости выходить на какие-то непонятные высокие уровни абстракции. Программирование на С++ как раз подразумевает более глубокое знание предмета и умение его использовать. Попытку переписать сервер с epoll на asio я очень быстро прекратил. Я сам, при необходимости, сделаю где нужно асинхронный код или распаралелю его по потокам (с пулом или без) и буду знать как он работает. Например, те же потоки в С++11 (threads) сделаны очень просто и сердито - по-спартански. Нет ничего лишнего. Многое вынесено в отдельные классы, другое выкинуто в принципе. И в итоге получилось, на мой взгляд, то, что нужно.

Ну а что касается того момента, что в asio идет "прогиб" под IOCP - не соглашусь. Используя IOCP можно написать код практически идентичный в своей парадигме epoll. Сам подход у них очень идентичен по своей сути. Но вот если к IOCP начинать лепить дополнительно асинхронное winapi - вот здесь уже и получается каша, которая не нужна в стандарте.
Hare76
Другие идеи
Группа создана, чтобы собирать предложения к стандарту C++, организовывать их внутренние обсуждения, помогать готовить их для отправки в комитет и защищать на общих собраниях в рабочей группе по С++ Международной организации по стандартизации (ISO).
Все предложения