Брокер сообщений для сервисной архитектуры на базе 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
-->
