Брокер сообщений для сервисной архитектуры на базе ZMQ — или отдых разработчика



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



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

    Предисловие



    Сделано just-for-fun за 5 дней на борту судна со средней скоростью 7 узлов.

    Введение



    Профессиональное счастье программиста довольно простое — писать на своем любимом языке интересные задачи и получать за это деньги (желательно не маленькие, хотя денег всегда мало). Подобные желания привели к тому, что родился целый подход в виде отдельностоящих приложений и процесса обмена между ними: SOA (в частности SOAP/WSDL/XML-RPC/JSON-RPC т.п.), REST, микросервисная архитектура. Суть в том, что следуя заветам Unix, отдельный функционал выделяется в приложения, а обмен данными между ними специфицируется отдельно.
    Одно из моих хобби связанно с работой распределенной сети мелких модулей: умный дом, система вычислений и другие схожие задачи. Для коммуникации между ними удобно использовать центральный брокер сообщений. Типовое решение: RabbitMQ, Redis, ActiveMQ и другие схожие решения. Из монстров индустрии можно отметить IBM Broker, IBM MQ, Tibco.
    Но что-то мне в них не нравилось

    «Фатальные недостатки» существующих решений



    • Apache Active MQ и прочие Java-based брокеры. Минимум 100МБ на старте — злость берет даже при наличии 16Гб оперативы. Ничего против Java не имею, но все же…
    • IBM, Tibco и остальные. Было бы столько денег — потратил бы на что-нибудь еще.
    • Rabbit MQ, Redis. Возможно самый правильный вариант, но я же на отдыхе, а значит это не наш путь.

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

    Компоненты



    Коммуникационный слой в виде ZeroMQ для простоты обмена сообщениями.
    С языком программирования возникли сложности. JVM-based, Python, Ruby отметаем по причине виртуальных машин, а значит избыточного потребления ресурсов. Хотел Rust, но после начала реализации понял, что надо ждать дальнейшей стабилизации стандартной библиотеки. Go — было бы отлично, если бы была родная реализация zmq. В итоге C++/C.

    Первичная реализация



    Брокер состоит из:
    • сервисов — именных клиентов (ZMQ_DEALER), работающих в режиме ответ-на-запрос
    • клиентов — анонимных клиентов (ZMQ_REQ), выполняющих запрос к сервису через брокер и ожидающих ответа.


    Точкой подключения к брокеру является сокет вида ROUTER. При приеме данных от REQ сокета (от клиентов), генерируется сообщение, содержащее в себе уникальный код клиента. Далее создается пакет для запроса в сервис, состоящий из названия сервиса, номера клиента и остальных данных. Схематически это можно представить так:



    Код можно посмотреть на GitHub.

    Вторичная реализация



    Реализация показалась очень простой. Надо было придумать и решить еще часть проблем.

    Как использовать несколько экземпляров сервисов с одинаковым именем?


    Допустим для балансировки нагрузки и отказоустойчивости.
    Дело в том, что если используется одинаковое имя соединения в режиме DEALER-to-ROUTER, то использоваться будет только последнее подключение.
    Решение — отдельный балансировщик нагрузки для каждого сервиса, подключающегося к брокеру и создающий отдельную точку соединения в режиме DEALER-to-DEALER. В этом случае, для сервисов достаточно лишь поменять адрес подключения на балансировщик вместо брокера.

    Параметры
    USAGE: 
    
       waha-proxy-balance  [-b <zmq bind>] [-v] -n <name> [-u <zmq bind>] [--]
                           [--version] [-h]
    
    
    Where: 
    
       -b <zmq bind>,  --backend <zmq bind>
         Backend binding for proxy
    
       -v,  --verbose
         Show much more output
    
       -n <name>,  --name <name>
         (required)  Balancing service name
    
       -u <zmq bind>,  --url <zmq bind>
         Broker url
    
       --,  --ignore_rest
         Ignores the rest of the labeled arguments following this flag.
    
       --version
         Displays version information and exits.
    
       -h,  --help
         Displays usage information and exits.
    
    
       Broker balance module
    



    Код здесь на GitHub

    Как предоставить доступ к сервисам через HTTP?



    Учитывая массовую любовь народа к HTTP интерфейсу, что, учитывая возможность работы хоть через умный тостер, не лишено смысла, надо это сделать правильно: json/plain вывод, поддержка как GET так и POST запросов, ну и JSONP на всякий пожарный.
    С помощью библиотек Poco получилось реализовать в виде отдельного демона.

    • Параметр args в GET запросе или тело POST должно содержать JSON массив.
    • Опциональный параметр timeout — максимальное время ожидания ответа в мс.
    • Опциональный параметр type — тип возвращаемых данных — plain или json
    • Опциональный параметр jsonp или callaback — имя функции для JSON-P запроса


    Параметры
    USAGE: 
    
       waha-proxy-http  [-t <ms>] [--threads <int>] [-p <int>] [-v] [-u <zmq
                        bind>] [--] [--version] [-h]
    
    
    Where: 
    
       -t <ms>,  --timeout <ms>
         Request timeout  without `timeout` param. -1 - infinity
    
       --threads <int>
         HTTP max threads
    
       -p <int>,  --port <int>
         HTTP binding port
    
       -v,  --verbose
         Show much more output
    
       -u <zmq bind>,  --url <zmq bind>
         Broker url
    
       --,  --ignore_rest
         Ignores the rest of the labeled arguments following this flag.
    
       --version
         Displays version information and exits.
    
       -h,  --help
         Displays usage information and exits.
    
    
       Broker HTTP proxy (REST like) module
    



    Код здесь на GitHub

    Почему Poco?
    Почему не boost/libevent или еще что? Потому что Poco это очень тонкая обертка над системными вызовами, без лишнего обвеса, собирающееся с минимальными усилиями и отличной документацией. И просто она мне значительно понятнее чем boost.


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



    Делаем демона, который вызывает любую другую программу, передает ей на вход поля сообщения как аргументы и интерпретирует вывод как ответ. Упрощенный такой CGI.

    Параметры
    USAGE: 
    
       waha-service-script  [-s <script>] [--no-stdout] [-e] [-r] -n <name>
                            [-v] [-u <zmq bind>] [--] [--version] [-h] <string>
                            ...
    
    
    Where: 
    
       -s <script>,  --script <script>
         Script path for 'script' mode
    
       --no-stdout
         Do not use stdout as message
    
       -e,  --stderr
         Append script stderr output
    
       -r,  --ret-code
         Append script return code as last field in message
    
       -n <name>,  --name <name>
         (required)  Balancing service name
    
       -v,  --verbose
         Show much more output
    
       -u <zmq bind>,  --url <zmq bind>
         Broker url
    
       --,  --ignore_rest
         Ignores the rest of the labeled arguments following this flag.
    
       --version
         Displays version information and exits.
    
       -h,  --help
         Displays usage information and exits.
    
       <string>  (accepted multiple times)
         Predefined args for script
    
    
       Broker script service
    



    Код здесь на GitHub

    Как сделать еще лучше?



    Сделать доступ ко всем сервисам через консоль. С plain/hex/base64/json вводом и выводом.

    Параметры
    USAGE: 
    
       waha-cli  [-t <ms>] -n <name> [-v] [-u <zmq bind>] [-p] [-e <plain
                 |base64|hex>] [--out-sep <char>] [-o <empty|plain|delimited
                 |json>] [-d <plain|base64|hex>] [--in-sep <char>] [-i <args
                 |plain|delimited|json>] [--] [--version] [-h] <string> ...
    
    
    Where: 
    
       -t <ms>,  --timeout <ms>
         Request timeout. -1 - infinity
    
       -n <name>,  --name <name>
         (required)  Remote service name
    
       -v,  --verbose
         Show much more output
    
       -u <zmq bind>,  --url <zmq bind>
         Broker url
    
       -p,  --pretty
         Pretty JSON output for 'json'
    
       -e <plain|base64|hex>,  --encoder <plain|base64|hex>
         Output encoder for 'delimited' output
    
       --out-sep <char>
         Output separator
    
       -o <empty|plain|delimited|json>,  --output <empty|plain|delimited|json>
         Output mode in CLI mode
    
       -d <plain|base64|hex>,  --decoder <plain|base64|hex>
         Input decoder for 'delimited' input
    
       --in-sep <char>
         Input separator
    
       -i <args|plain|delimited|json>,  --input <args|plain|delimited|json>
         Input mode in CLI mode
    
       --,  --ignore_rest
         Ignores the rest of the labeled arguments following this flag.
    
       --version
         Displays version information and exits.
    
       -h,  --help
         Displays usage information and exits.
    
       <string>  (accepted multiple times)
         Args in request message for for 'args' input
    
    
       ZMQ broker console client interface
    
    



    Код здесь GitHub

    Сборка всего



    GIT + CMake + make

    git clone https://github.com/reddec/waha.git && cd waha && mkdir build && cd build
    cmake ../  -DCMAKE_BUILD_TYPE=Release
    make && make package
    


    Пакет для Debian будет в waha-*.deb, для остальных waha-*.zip

    Пример использования



    Авторизация пользователей с большой нагрузкой на чтение и небольшой на запись.



    Запуск брокера (по умолчанию порт 10000)
    $ waha-broker
    


    Запуск балансировщика для чтения (по умолчанию порт 10001)
    $ waha-proxy-balance -n system.user.check
    


    Запуск httpasswd для проверки логин/пароля (запустить сколько угодно раз для распределения нагрузки). Выдает только код завершения работы. Все что идет после — - передается как аргументы скрипту.
    $ waha-service-script  -u tcp://127.0.0.1:10001 -n system.user.check -s htpasswd  --ret-code --no-stdout -- -bv users.passwd
    


    Запуск скрипта добавления пользователей (без балансировки) вместе с выводом
    $ waha-service-script -n system.user.add -s htpasswd --ret-code --stderr -- -b users.passwd
    


    Запуск HTTP интерфейса (порт по умолчанию 9001)

    $ waha-proxy-http
    


    пример запроса

    http://127.0.0.1:9001/system.user.check?args=["admin","admin"]
    http://127.0.0.1:9001/system.user.check?args=["admin","admin"]&callback=func123
    


    Использование командного интерфейса



    Вывод без форматирования:
    $ waha-cli -n system.user.check admin admin

    Вывод в JSON:
    $ waha-cli -o json -n system.user.check admin admin


    Итог



    Модульный, работающий брокер сообщений, написанный во время отдыха для отдыха и развлечения программистом just for fun. Дабы код не пропадал, выложил его в общий доступ.
    Если кому нужно это — обращайтесь проконсультирую.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Будете использовать?

    • 2,8%Да4
    • 59,6%Нет84
    • 37,6%Возможно53
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 8

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

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

        • Узнать состояние сооединения с клиентом/другим сервисом через API библиотеки невозможно. Эта информация скрыта от пользователя by design. Отлаживать проблемы связи между компонентами вашего приложения, особенно если оно немаленькое и компонентов сильно больше двух, становится задачей нетривиальной
        • Точно так же скрыта от разработчика информация об количестве сообщений в очередях получения/отправки IO-процессов. Благодаря этому даже мягко (gracefully) завершить процесс не получится, т.к. приложение не знает, все ли входящие сообщения уже обработаны и все ли исходящие отправлены. И если с входящей очередью можно разобраться алгоритмически, добавив пару-тройку сервисных сообщений между сервисами, то с исходящей очередью все плохо.
        • Продолжение предыдущего пункта — невозможно закрыть соединение с другими сервисами. Никак. ZMQ решает это за вас, и считает что при этом умнее. Как показывает практика, не всегда
        • А если соединение все-таки закрылось, даже самым похабным образом (exit(1), die, etc) — вторая сторона об этом никогда не узнает, и будет дальше слать сообщения (которые дропнутся на выходе из приложения-отправителя, молча — ZMQ умный же), или ждать ответа от помершего сервиса. Единственный метод решения — повальный heartbeat между сервисами.
        • ZMQ_IDENTITY. Еще одна замечательная опция от создателей ZMQ. Честно идентифицирует отправителя, но если вы решили использовать её для адреса сервиса в кластере, то это зря. Если ваш сервис по какой-то причине решит помереть, а потом восстать из пепла (банально — при обновлении версии софта), после чего представится старым идентом и будет ждать сообщений, то получит он шиш. Ибо ZMQ умный же, да. Единственное, что поможет в такой ситуации — рестарт всех сервисов, которые сотрудничали с перезапущенным, что в реальной жизни значит рестарт всего кластера.


        Что в итоге
        Библиотека просто отлична для прототипирования проекта и для быстрой разработки PoC-решений.
        В проде тоже, в общем-то, жизнеспособна, за вычетом того, что подводных камней количество немалое.

        Если опыта работы с ZMQ мало, а в прод пустить хочется, то готовьтесь к веселому дебагу по ночам, а лучше подумайте еще раз.
        Если опыт есть — почему бы и нет, зато хорошо прокачаете скилл построения протоколов без потерь сообщений; термины heartbeat, «нумерация пакетов», распределение нагрузки и масштабирование больше не будут пустыми звуками, а резюме будет выглядеть куда круче :)

        ЗЫ: Меня зовут Денис и я алкоголик с ZMQ в проде больше двух лет.
          0
          Полностью согласен. Посему и написал just-for-fun) На работе все-таки используется IBM/Tibco, а для своих проектов RabbitMQ (в котором тоже есть грабли, но решаемые с меньшими трудозатратами).

          Приятно видеть еще одного ZMQ'шника. С библиотекой я знаком более трех лет, но не профессионально. Может статью набросаете? А то в русскоязычном сегменте по ZMQ удручающе мало информации.
            0
            Что по вашему лучше использовать тогда?
              0
              Стоит приглядеться к nanomsg
                0
                Серебрянной пули нет.

                Приходится выбирать между высокой производительностью/пропускной способностью, которую дают brokerless решения вроде ZeroMQ и его детищ — nanomsg и crossroads, и стабильностью/надежностью/гарантией доставки, которые дают брокеры — RabbitMQ, ActiveMQ, Redis (с оговорками), NATS и остальные.

                Есть отличный материал со сравнением и тестами многих систем — Dissecting Message Queues

                Crossroads I/O и NanoMSG в какой-то степени детища ZeroMQ. Первое — форк, который сначала забросили, потом с помпой возобновили и опять забросили.
                Второе — детище одного из разработчиков ZeroMQ, которое исправляет многие недочеты прародителя, но при этом приносит свои и исповедует все ту же идеологию ZMQ — библиотека сама знает, как лучше отправлять, ибо умнее ее пользователей.

                Nanomsg, как и zmq, гарантирует атомарность доставленных сообщений и их порядок, при этом не гарантирует саму доставку. При потере части разделенного сообщения оно все будет дропнуто получателем, сообщения могут не доходить вообще (о чем я писал выше). При этом автор (Martin Sustrik) ясно дает понять, что чинить это никак не собирается:
                Guaranteed delivery is a myth. Nothing is 100% guaranteed. That’s the nature of the world we live in. What we should do instead is to build an internet-like system that is resilient in face of failures and routes around damage.


                Во многих аспектах nanomsg интереснее, модульнее и проще для разработчика, но с моей точки зрения это такой же черный ящик для мониторинга в проде. И если ZMQ проект уже устоявшийся, обладает большим комьюнити, которое большую часть «особенностей» уже выявило и опубликовало решения на том же StackOverflow, то Nanomsg штука свежая, комьюнити небольшое, и чего от него ждать при нагрузке в боевых условиях я не знаю.

                Ну и кому интересно, есть отличный обзор Nanomsg на том же ресурсе — A Look at Nanomsg and Scalability Protocols

                При этом стоит сказать, что я его в проде не использовал, и, может быть, еще просто не умею готовить :)

                Если подытожить:
                Надо быстро и допустимы потери: brokerless решения — ZeroMQ, NanoMSG, Crossroads I/O
                Надо надежно и можно подождать: брокеры — RabbitMQ, ActiveMQ, RATS, etc
                +1
                Очень интересно.
                Хорошо бы ваш такой богатый опыт оформить это в виде статьи «Как правильно готовить ZMQ», а то в zguide не так много таких нюансов описано.
                На счет идентии не знал, спасибо. Наверно по правилам надо перегенерить идентити при перезапуске. Тогда не должно быть проблем, и не нужно тормозить кластер(я правда не уверен. что тогда ликов конекшенов не будет)
                Хеартбит и уведобления о доставки — это да, нужно обязательно. тут разработчики не скрывают.
                На счет диагностики тоже согласен, это ужасно раздражает и напрягает. Я уже думаю — а не написать, ли пач zmq (или форк сделать) в котором была-бы прикручена диагностика. В принципе это не сложно реализовать.

                И если не секрет — а в проде клиенты и сервисы на чем бекенд написан?
                  0
                  У нас как IDENT используется UUID, при этом он не задействован нами в какой-то логики, а скорее как адрес сервиса для правильного роутинга пакета по кластеру. Если его не задавать самому вручную, то он будет сгенерен автоматически, при этом сталкивался с коллизиями идентов, созданных разными машинами.

                  У нас по большей части php (php-zmq), c++ (cppzmq) и java через jzmq

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое