Брокер сообщений для сервисной архитектуры на базе 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] [-v] -n [-u ] [--] [--version] [-h] Where: -b , --backend Backend binding for proxy -v, --verbose Show much more output -n , --name (required) Balancing service name -u , --url 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] [--threads ] [-p ] [-v] [-u ] [--] [--version] [-h] Where: -t , --timeout Request timeout without `timeout` param. -1 - infinity --threads HTTP max threads -p , --port HTTP binding port -v, --verbose Show much more output -u , --url 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
Почему не boost/libevent или еще что? Потому что Poco это очень тонкая обертка над системными вызовами, без лишнего обвеса, собирающееся с минимальными усилиями и отличной документацией. И просто она мне значительно понятнее чем boost.
Как сделать хорошо администратору и просто человеку, которому неохота программировать?
Делаем демона, который вызывает любую другую программу, передает ей на вход поля сообщения как аргументы и интерпретирует вывод как ответ. Упрощенный такой CGI.
USAGE: waha-service-script [-s <script>] [--no-stdout] [-e] [-r] -n[-v] [-u ] [--] [--version] [-h] ... 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 (required) Balancing service name -v, --verbose Show much more output -u , --url 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. (accepted multiple times) Predefined args for script Broker script service
Код здесь на GitHub
Как сделать еще лучше?
Сделать доступ ко всем сервисам через консоль. С plain/hex/base64/json вводом и выводом.
USAGE: waha-cli [-t] -n [-v] [-u ] [-p] [-e ] [--out-sep ] [-o ] [-d ] [--in-sep ] [-i ] [--] [--version] [-h] ... Where: -t , --timeout Request timeout. -1 - infinity -n , --name (required) Remote service name -v, --verbose Show much more output -u , --url Broker url -p, --pretty Pretty JSON output for 'json' -e , --encoder Output encoder for 'delimited' output --out-sep Output separator -o , --output Output mode in CLI mode -d , --decoder Input decoder for 'delimited' input --in-sep Input separator -i , --input 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. (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. Дабы код не пропадал, выложил его в общий доступ.
Если кому нужно это — обращайтесь проконсультирую.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
