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

e7fddb3a90d44fc6a9b619abcad706df.jpg

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

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

Сделано 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 сокета (от клиентов), генерируется сообщение, содержащее в себе уникальный код клиента. Далее создается пакет для запроса в сервис, состоящий из названия сервиса, номера клиента и остальных данных. Схематически это можно представить так:

99686faefe694a84938b0ed9aff5b2e7.png

Код можно посмотреть на 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

Почему Poco?

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

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

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

Параметры
USAGE: 

   waha-service-script  [-s