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 не нашел.
Но проект быстро развивается, уже много где используется, будем надеятся и верить.

Замечания в комментариях приветствуются!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

    0
    — ZeroMQ давно форкнут в более активно развивающийся проект Crossroads и официально почти мертв (идет поддержка, но не развитие)
    — для C# есть версия на .NET — NetMQ, которая не требует носить за проектом р̶̶̶а̶̶̶с̶̶̶п̶̶̶р̶̶̶о̶̶̶к̶̶̶л̶̶̶я̶̶̶т̶̶̶у̶̶̶ю̶̶̶ ̶̶̶.̶d̶l̶l̶ библиотеку (да, нужно таскать .dll-сборку, но ее при желании проще засунуть в ресурсы/код проекта, чем нативную .dll)
      +2
      Crossroad I/O был попыткой сделать улучшенный ZeroMQ, но не вышло и проект заброшен (https://github.com/crossroads-io/libxs последний коммит почти год назад). Как верно сказали дальше — сейчас активно развивается nanomsg в котором решили многие из существенных проблем ZeroMQ. мы сейчас как раз переводим часть системы от ZMQ на nanomsg именно из-за этих сложностей. Выглядит очень мощно, хотя некоторые из фич zmq там нет.

      Однак и хоронить ZMQ рано — активно развивается, хоть и без Мартина, уже и 4 стабильная и пилится дальше.
        0
        Вот что бывает, когда перестаешь следить за апдейтами; а впрочем, мой коммент все равно на пару лет актуальнее статьи.
        +2
        Crossroads вроде как тоже уже мертв (и еще). А ZeroMQ перезагружают и переписывают на С (проект nanomsg).
          0
          Дополню, для java есть JNI-обертка для zmq — jzmq, которая упоминается в посте, но она не очень удобна (иногда требует пересборки при обновлении java).

          Также есть jeromq на чистой java, которая чуть менее производительная и не поддерживает протоколы ipc:// и pgm://. Первый не поддерживается, т. к. в java не поддерживает unix-domain socket'ы (и между jeromq эмулируется через tcp), второй — т. к. автор не нашел соответствующей библиотеки под java.
          0
          Про обмен через файлы в наши годы уже стыдно говорить, но и такое случается.
          А что хранит файлы лучше, чем файловая система (возможно, распределенная)? Хранить, например, относительно тяжелые файлы (скажем, со средним размером 100к) в БД уже накладно. Особенно, если с ними работают как с файлами. Не говоря уже про всякие развлечения типа mmap'а.
            0
            Про хранение — спору нет. Речь про методы обмена между приложениями или его частями.
            Работаю в сфере где все очень консервативно и направлено на максимально быстрое написание кода, зачастую это как раз через файлы. Одно приложение положило сообщение в файл, другое — сканирует каталог и забирает сообщения из файлов. Вот так вот, в 21-м веке :)
              0
              Тут стоит вспомнить особо энтерпрайзные bash-скрипты.

              Сложно не согласиться, что в качестве границы обмена между разными системами при интеграции это может быть наиболее дешево, сердито и надежно. Как с точки зрения технологической (и на этой, и на той стороне интеграции могут быть идиоты), так и с административной (количество бумажного геморроя для подключения к какой-нибудь базе/esb в большой организации может быть довольно большим).
                0
                Дешево и сердито — да. Насчет надежности вопрос спорный. Там уже вопрос упирается в тип сетевой файловой системы. Было много негативного опыта, когда админы настраивали NFS, не удосужившись даже слегка погрузиться в настройки. А между тем по-умолчанию процесс, обращающийся к точке монтирования NFS в режиме hard в случае потери связи с файл-сервером тупо виснет, а в случае если выключена опция intr, виснет очень хорошо. С Samba и CIFS тоже немало глюков. Плюс сложности с резервированием.
                Вобщем эта экономия приносит немало бед эксплуатирующему персоналу.
            +1
            Автор ZeroMQ сейчас занимается разработкой nanomsg, которая, как я понимаю, есть «ZeroMQ done right». Вот здесь рассказывается об отличиях nanomsg и ZeroMQ.
              0
              Автор, а чем данный подход превосходит подход с использованием шины данных? то есть в случае использования rabbitMQ(как пример) получаем из коробки гибкую возможность масштабирования, конфигурирования, управления очередями, гарантии доставки тд. Я понимаю что в этом случае уровень абстракции гораздо выше, но и плюшек гораздо больше он привносит.
                +2
                В большинстве случаев не нужен Ворд, чтобы написать записку соседке по парте :)
                Пока мной RabbitMQ толком не изучен, поэтому и писать нечего.
                  +2
                  Всё просто — для раббита и аналогичных MQ вам нужно запускать отдельный демон, вероятно, даже на отдельной машине. Соответственно, там в комплекте есть куча возможностей, как вы уже заметили. Но это ещё одно звено в вашем приложении.

                  ZeroMQ же (и nanomsg, и другие последователи) — это «сокеты на стероидах», тонкий слой абстракции поверх BSD-сокетов. Дополнительные (сверх обычных сокетов) возможности там довольно низкоуровневые, но для ZeroMQ не требуется дополнительных программ. Вся функциональность линкуется в виде библиотеки (довольно маленькой, кстати) в вашу программу.

                  Пример, зачем оно в принципе нужно. Есть такая среда, IPython, которая, помимо всего прочего, предоставляет возможность создания красивых фронтендов к ней, типа веб-приложения в браузере. Общение между BE и FE ведётся с помощью ZMQ. Настоящий брокер сообщений, как можно себе представить, был бы здесь просто overkill'ом.
                    0
                    Следует помнить, что MQ имеет преимущества в виде хранения данных в своей очереди, а в случае падения вашего приложения данные в ZeroMQ будут утеряны. Опять таки уровень абстракции немного пугает тем что неясно как выяснить в ZMQ: кто подключился, он все еще подключен и т.д. Насчет nanomsg существует мнение, что он еще не готов для промышленного использования. Опять такие сложность протокола своидит на нет ZMQ против например очередей в том же Redis. Одним словом в копилку, но хотелось бы от статьи ответов на поставленные в ней же вопросы.
                      +2
                      Просто ZeroMQ — это не MQ, несмотря на его название. Это просто слой абстракций над сокетами, применяемый там, где сокеты — это слишком низкоуровнево, а полноценные очереди сообщений и аналогичные системы — уже overkill. Их некорректно сравнивать, это совершенно разные инструменты.
                    0
                    Скорость передачи, например, где latency критична.
                    0
                    del

                    Only users with full accounts can post comments. Log in, please.