ZeroMQ: сокеты по-новому

в 9:41, , рубрики: sockets, zeromq, клиент-сервер, Программирование, сервисы, Сетевые технологии, сокеты

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

Многие разработчики решают идти по пути наименьшего сопротивления, возложив эту задачу, например, на СУБД. Скажем, один процесс положил данные в БД, второй прочитал, обработал — положил еще и так далее.
Про обмен через файлы в наши годы уже стыдно говорить, но и такое случается.
Другие же программисты пытаются создать какое-то свое, специализированное решение и, как правило, выбирают сокеты.

Задача проектирования и разработки архитектуры приложения крайне интересная, но это отдельная тема. В данном посте хотел бы поделиться своим первым впечатлением от знакомства с библиотекой ZeroMQ.

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

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

Непосредственно описание ZeroMQ, его API и кучу другой полезной информации можно найти на официальном сайте ZeroMQ.

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

Мы же займемся решением типовой задачи и сравним решение на основе традиционных сокетов и «сокетов ZeroMQ».

Итак, задача

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

Далее нужно определиться с форматом обмена.
Традиционный сокет работает с последовательностью байтов, что для приложения, которое обменивается некоторой структурированной информацией, не есть хорошо. Поэтому нам придется создать некий «пакет» с данными, для простоты у пакета будет один атрибут — длина. То есть сначала передаем длину пакета, затем сами данные указанной длины. При приеме соответственно буферизируем принятую последовательность байт и разбираем ее на «пакеты».
Внутрь самого «пакета» мы можем запихнуть что угодно: бинарную структуру, текст, JSON, BSON, XML, и т.д.

Для простоты, сервер у нас будет принимать и передавать данные в одном потоке.
А вот обработка данных на сервере должна происходить в несколько потоков (будем называть их worker-ами).

Решение

В качестве решения создал два исходника, один с обычными сокетами, другой с ZeroMQ.
Не буду публиковать исходный код в самом посте, для просмотра пройдите по ссылкам:
1) Традиционные сокеты (19 Kb)
2) Сокеты ZeroMQ (11,74 Kb)

Подробнее о тестах

Каждый файл с исходным кодом — это готовый тест, при запуске которого стартует и сервер, и клиенты (в одном процессе, но в разных потоках).
Тест работает несколько секунд и выдает результаты работы каждого клиента: сколько пакетов и байт получил, а также среднюю скорость получения пакетов.
При старте потока клиента происходит передача одного или нескольких пакетов с данными, а при получении каждого пакета — он передается обратно.
Параметры теста можно изменить, они заданы в #define-ах в каждом файле.

Как видно, ZeroMQ сократил объем кода примерно в 2 раза, читабельность улучшилась.
Теперь посмотрим, сколько мы за это заплатили.

На моей машине при исходных параметрах тест выдал примерно следующие результаты:

1) 400 пакетов в секунду (традиционные сокеты);
2) 500 пакетов в секунду (ZeroMQ).
* Примечание: по-умолчанию в тесте 10 клиентских потоков и 2 worker-а, размер пакета — 1Кб, время «обработки» (имитируем usleep-ом) одного пакета сервером — 2мс.

Сразу оговорюсь, что если бы обработка данных у нас шла в один поток, вместе с приемом и передачей, то ZeroMQ проиграл бы обычным сокетам в 2-4 раза. Проверено также на подобном тесте, однако публиковать его я пока не буду, т.к. однопоточный сервер, который обрабатывает одновременно только один запрос, а остальные клиенты ждут — это не наш случай.

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

Основная причина, конечно же, кроется в исходном коде самого теста. Обработка данных в несколько потоков на обычных сокетах — задача довольно сложная. В моем тесте она реализована далеко не оптимальным способом:

1) нет никакой очереди задач и принятых пакетов, мы банально не принимаем данные, если не можем их обработать;
2) когда worker закончил обработку запроса — он впустую спит, пока основной поток не запишет ему в буфер следующую задачу;
3) основной поток в случае занятости worker-ов вхолостую проходит основной цикл, пока worker не освободится (или не появятся события ввода-вывода);
4) при записи результата обработки запроса worker-ом в буфер передачи клиента, блокируется основной поток (либо worker ждет пока основной поток пройдет основной цикл).

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

Теперь давайте обратимся к варианту с ZeroMQ.

Исходный код более читабелен, а главное — лишен каких-либо блокировок (mutex-ов, как в задаче с обычными сокетами). Это основное преимущество ZeroMQ.

В традиционном асинхронном программировании блокировки неизбежны, с увеличением объема кода вы обязательно где-то поставите лишнюю блокировку, а где-то забудете поставить нужную. Затем появятся вложенные блокировки, которые в итоге приведут к deadlock-ам и различным race condition. Если ошибки будут происходить в редких случаях, на приложении в production вы замучаетесь их искать. А эффект будет потрясающий — ваш сервис намертво зависнет, несохраненные данные будут потеряны, а клиенты отключатся.

ZeroMQ решает эту проблему просто — процессы и потоки лишь обмениваются сообщениями. При этом нужно сделать оговорку, что не рекомендуется расшаривать никакие общие данные между потоками и использовать блокировки. ZeroMQ позволяет не делить между потоками данные о сокетах и их буферы, однако данные самого приложения остаются головной болью разработчика.
Внутри процесса между потоками также может происходить обмен сообщениями, и не обязательно через TCP. Достаточно передать функциям zmq_bind/zmq_connect вместо «tcp://127.0.0.1:1010» что-то вроде «ipc://mysock» — и ваш обмен уже работает через UNIX-сокеты, а поставите «inproc://mysock» — и обмен пойдет через внутреннюю память процесса. Это значительно быстрее и экономичнее сокетов.
В качестве примера возьмите исходник теста.
Поток, который производит обработку данных (worker) — это такой же клиент, но только внутренний. Он подключается к основному потоку через указанный сокет (эффективнее всего inproc://) и получает задание, выполнив которое отправляет результат обратно основному потоку. Последний уже переадресовывает результат внешнему клиенту.
ZeroMQ позволяет не заботиться о распределении задач и поиске свободного worker-а. В данном примере он автоматически ставит пакет в очередь на обработку (отправку worker-у).

Несомненно, и у ZeroMQ есть довольно весомые минусы. Хоть эта библиотека и берет на себя кучу забот, она не обеспечивает гарантии доставки и сохранности ваших сообщений. Это отдается на откуп разработчика, что совершенно правильно, на мой взгляд.

Пройдемся по нескольким, наиболее важным аспектам работы с ZeroMQ.

Соединения

Плюсы:
+ ZeroMQ автоматически восстанавливает исходящие соединения. В приложении вы можете и не заметить разрыва соединения, если, конечно, специально не будете отслеживать это событие (см.zmq_socket_monitor())

Минусы:
— Я пока не догадался, как узнать настоящий IP-адрес, имя хоста или хотя бы дескриптор клиента, от которого пришло сообщение. Максимум что дает ZeroMQ — это некий идентификатор клиента (для сокета типа ZMQ_ROUTER), который может быть как назначен ZeroMQ автоматически, так и задан клиентом самостоятельно перед установкой соединения.
— Опять же, я пока не догадался как принудительно отключить клиента (допустим, не авторизовался вовремя). А это чревато накапливанием ненужных соединений.

Очереди

Плюсы:
+ отправляемые в ZeroMQ сообщения попадают во внутреннюю очередь, что позволяет не дожидаться окончания отправки, а в случае исходящего соединения — не имеет значения, установлено оно или нет. Размер очереди может меняться.
+ существует также очередь на прием, из-за чего реализуется стратегия т.н. «справедливой очереди». В случае входящего соединения, вы получаете сообщения из общей очереди на прием для всех клиентов.

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

Сообщения

Плюсы:
+ В ZeroMQ вы работаете не с потоком байт, а с отдельными сообщениями, длина которых известна.
+ Сообщение в ZeroMQ состоит из одного или нескольких т.н. «фреймов», что довольно удобно — можно по мере прохождения сообщения по узлам добавлять/удалять фреймы с метаинформацией, не трогая фрейма с данными. Такой подход, в частности, используется в сокете типа ZMQ_ROUTER — ZeroMQ при приеме сообщения автоматически добавляет первым фреймом идентификатор клиента, от которого оно получено.
+ Каждое сообщение атомарно, т.е. всегда будет получено или передано полностью, включая все фреймы.

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

Лирическое отступление

В ZeroMQ, помимо различных видов транспорта (tcp, ipc, inproc и т.д.), существует несколько типов сокетов: REQ, REP, ROUTER, DEALER, PUB, SUB, и т.д.
Советую ознакомиться с ними по документации внимательно. От типа сокета на обоих концах зависит его поведение. В некоторых типах сокетов используются дополнительные обязательные фреймы.
Упомянутый выше Guide вполне неплохо на примерах ознакомит вас с основными типами сокетов.

Вывод

Если вы только начинаете проектировать свое приложение, либо какие-то его отдельные простые части, модули и подзадачи, то очень рекомендую присмотреться к ZeroMQ.
В реальном приложении с асинхронной обработкой данных ZeroMQ обеспечит не только сокращение объема кода, но и некоторое увеличение производительности.
Бинды данной библиотеки есть для множества языков программирования: C++, C#, CL, Delphi, Erlang, F#, Felix, Haskell, Java, Objective-C, Ruby, Ada, Basic, Clojure, Go, Haxe, Node.js, ooc, Perl, Scala.
Библиотека кросс-платформенная, т.е. можно использовать как в Linux, так и под Windows. Правда, к сожалению, пока официальной версии под MinGW не нашел.
Но проект быстро развивается, уже много где используется, будем надеятся и верить.

Замечания в комментариях приветствуются!

Автор: nitro2005

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js