2014-08-24

О ZeroMQ

Ну вот я и добрался до мимимишной ØMQ.


Сразу отмазываюсь. Хороший наезд на ØMQ на CodeFest 2014 был вполне справедлив. ØMQ — это лишь транспорт, она не способна решить вопросы вашей сервисно-ориентированной архитектуры в одиночку. Мартин Сустрик, один из первоначальных авторов ØMQ, действительно форкнул её, и не один раз. Но мы все же взялись использовать эту чудесную библиотеку, ибо она хороша, и нам подходит.

ØMQ — это сокеты на стероидах. ØMQ — это очередь сообщений без очереди сообщений (ну ладно, с очередями в памяти, но точно без брокера). Позволю себе перевести «Сотню слов о ØMQ» из официального руководства:

ØMQ (также известная как ZeroMQ, 0MQ или zmq) выглядит как встраиваемая сетевая библиотека, но работает как фреймворк для многопоточного программирования. Она дает вам сокеты, которые атомарно передают сообщения через различные транспорты внутри процесса, между процессами, по TCP или мультикастом. Вы можете соединять N к N сокетов согласно таким паттернам как распределение сообщений, публикация и подписка, распределение задач, запрос-ответ. Она достаточно быстра, чтобы строить на ней кластерные продукты. Её асинхронная модель ввода-вывода дает вам масштабируемые многоядерные приложения, построенные из асинхронных задач, обрабатывающих сообщения. Она имеет API на многих языках и работает на большинстве операционных систем. ØMQ вышла из iMatix и она открыта под LGPLv3.

Ну и невозможно не перевести «Как оно начиналось» из того же руководства. (Это руководство полно подобного юмора, замечательное чтиво).

Мы взяли обычные TCP сокеты, примешали радиоактивные изотопы, украденные из секретного советского атомного исследовательского проекта, обработали космическими лучами из 1950-х годов, дали в руки обкуренному автору комиксов, откровенно дико помешанному покрывать вздутые мускулы спандексом. Да, ØMQ сокеты — это мироспасательные супергерои мира сетей.

fig1.png

Меня пока не сильно интересует асинхронная многопоточная модель ØMQ. Хотя обещания строить сложные системы из компонент, обменивающихся сообщениями, сначала из потоков одного процесса, потом разных процессов, потом процессов на разных узлах, объединенных сетью, я где-то уже слышал. Кажется, в Erlang. Может, это и есть главный недостаток ØMQ? В смешении сетевых вещей и работы с потоками.

Ну так чем отличается ØMQ сокет от TCP сокета?

Направление подключения не зависит от роли компонент. В TCP как правило клиент подключается к серверу: сервер слушает порт, а клиент подключается к нему. В ØMQ можно все наоборот: клиент слушает порт, а сервер подключается к нему. К тому же можно подключаться сразу к нескольким слушающим сокетам, тогда сообщения будут распределены между ними, чаще всего поочередно (round-robin). Поэтому для слушания выбираем не тот компонент, который сервер, а тот, чей сетевой адрес будет реже меняться или будет более известен.

PlantUML diagram

ØMQ сама заботится о TCP подключении и переподключении. Даже можно «подключиться» и начать посылать сообщения, когда TCP связности между узлами еще нет. Тут у нас возникают таки очереди в памяти. Впрочем, ØMQ предпочитает в случаях невозможности доставки сообщений (или переполнения очереди) молча уничтожать эти самые сообщения. Об этом нужно помнить. И, в общем, тут есть своя логика.

Кстати, сообщения. Если TCP «доставляет» потоки, то ØMQ доставляет сообщения. Это особенность и ограничение любой очереди сообщений. Сообщения доставляются атомарно. Это значит, что если на принимающей стороне API сообщило вам, что пришло сообщение, то это сообщение уже полностью присутствует в памяти этой самой принимающей стороны. А если в процессе передачи сообщения произошли какие-то проблемы, то принимающая сторона никогда об этом не узнает. Сообщение просто не дойдет. Гарантированной доставки нет. О гарантированной доставке нужно заботиться самостоятельно с помощью нумерации сообщений, дополнительных запросов, таймаутов и т.д и т.п.

ØMQ сообщения — просто наборы байт. Но ØMQ сообщения могут еще делиться на фрагменты. Фрагмент — это часть сообщения, как её выделила отправляющая сторона. Фрагменты — это просто удобный способ разбиения сообщения на части. В HTTP для разделения заголовков от тела запроса используется пустая строка. В ØMQ для подобного разделения не нужно изобретать специальных разделителей, достаточно использовать фрагменты. Внутри себя ØMQ использует фрагменты для добавления адреса отправителя к сообщению в ROUTER сокетах. Также на фрагментах можно строить протоколы.


Почему, собственно «нулевая очередь сообщений»? Потому что «обычные» очереди сообщений (на том же «проклятом» создателями AMQP) требуют выделенного брокера. Это такой компонент, который отвечает за хранение очередей и передачу сообщений. Который, для пущей надежности, еще и хранит все сообщения. Который имеет свои собственные проблемы с масштабируемостью. Который еще надо развернуть и сопровождать.

PlantUML diagram

Если в том же AMQP есть и сообщения, и очереди, и точки обмена, все как отдельные явные сущности, то в ØMQ есть только сообщения и сокеты. Сокеты бывают разных типов и эти типы должны использоваться совместно, чтобы получились определенные паттерны передачи сообщений.

Простейший и тупейший вариант — пара сокетов REQ (request) и REP (reply). Клиент посылает сообщение через REQ сокет и блокируется в ожидании ответа. Сервер блокируется в ожидании поступлении сообщения через REP сокет, получает сообщение и отсылает ответ через тот же сокет. Как уже упоминалось ранее, слушания и коннекты ортогональны к направлению этого диалога. А тупейший это вариант, потому что это единственный синхронный паттерн в ØMQ. Блокировка, завязанная на порядок поступления сообщений, может в грустных случаях длиться вечно.

fig2.png

Сокеты PUSH и PULL предназначены для параллельного пропихивания сообщений. Можно из одного PUSH слать сообщения нескольким PULL (поочередно, round-robin). А можно из нескольких PUSH собирать сообщения в один PULL.

fig5.png

Если нужно слать сообщения одновременно нескольким получателям нужны PUB (publish) и SUB (subscribe) сокеты. На SUB стороне нужно обязательно подписаться на интересующие сообщения. Подписаться можно на префикс сообщения. Т.е. указать некую последовательность байт, и на этот сокет будут приходить только сообщения, начинающиеся с этой последовательности байт. Можно подписаться на несколько префиксов. В частном случае можно указать пустой префикс и получать все сообщения. Ну а в PUB сообщения просто посылаются. И если нет ни одного подключенного и подписанного получателя, сообщение просто уничтожается. Но зато по сети пересылаются только те сообщения, в которых заинтересован данный подписчик.

fig4.png

Пара сокетов DEALER и ROUTER — это такой асинхронный вариант REQ и REP. Без тупых блокировок. Еще их можно соединять один ко многим. А ROUTER еще и может отсылать ответы конкретному получателю. Когда мы читаем сообщение из ROUTER сокета, к началу сообщения добавляется еще один фрейм, содержащий идентификатор (identity) отправителя сообщения. Этот идентификатор либо назначается ØMQ самостоятельно (но будет уникальным для каждого подключившегося к данному ROUTER сокету), либо отправитель (REQ или DEALER) может установить его явно. Идентификатор — обычный фрейм, т.е. произвольная последовательность байт.

И теперь когда мы отсылаем назад сообщение через ROUTER, мы должны добавить этот идентификатор первым фреймом сообщения. И сообщение дойдет не абы куда (как в других сокетах), а конкретному получателю (собственно, некоему конкретному отправителю ранее полученного через ROUTER сообщения). Если получателя с данным идентификатором нет (не подключен), то сообщение молча уничтожается (впрочем, при желании можно поймать тут ексепшен).

На роутере и дилере можно построить брокер, аналогичный выделенному брокеру «классических» очередей сообщений. Даже, при желании, нарисовать и сохранение сообщений.


Вот на роутерах и дилерах мы и построили одну маленькую подсистему. Есть несколько роутеров, которые перенаправляют сообщения. Типа масштабируемое ядро маршрутизации сообщений. Есть различные компоненты, которые одновременно подключаются ко всем этим роутерам. Эти компоненты выполняют определенные роли (о которых знают роутеры) и устанавливают явный идентификатор (identity), одинаковый для всех подключений к роутерам. Ну а роутеры, зная все идентификаторы и роли, и руководствуясь некоторыми правилами, маршрутизируют сообщения.

PlantUML diagram

Получилось вполне удобно и надежно. Еще раз, у нас уже были компоненты и выделены роли. Нам не хватало удобного транспорта. У REST слишком много накладных расходов, и через него плохо передавать уведомления. C голым TCP слишком много морок при асинхронном обмене, да и адресная доставка и переподключения требуют много ручной работы. В общем, пока ØMQ в роли универсального и мощного транспорта мне нравится.