четверг, 14 июня 2018 г.

[prog.flame] Вещи, которые мне не понравились при знакомстве с ZeroMQ

После недели штудирования и обдумывания "0MQ - The Guide" попробую зафиксировать несколько вещей, которые мне не понравились в ZeroMQ. А в следующем посте я попробую рассказать о текущих соображениях на тему применения ZeroMQ для построения распределенных SObjectizer-приложений. Так что сегодняшний рассказ может быть интересен более широкому кругу читателей.

Все, что описывается ниже -- это мое субъективное мнение, никоим образом не претендующее на объективность. Более, того, это мнение возникло в результате попыток представить, как ZeroMQ может стать базой для некого абстрактного транспорта, не привязанного напрямую к какой-то конкретной прикладной задаче. Вполне возможно, если бы ZeroMQ рассматривался как механизм взаимодействия конкретных компонентов конкретного приложения, впечатления были бы несколько другими.

Вначале своего рассказа я попробую высказать некое общее впечатление (или даже несколько впечатлений) о ZeroMQ вообще, а затем перейду к перечислению вполне себе конкретных вещей, которые мы можем увидеть в ZeroMQ или вокруг ZeroMQ.

Disclaimer. Я не буду рассказывать об азах ZeroMQ. Поэтому, если вы со спецификой ZeroMQ не знакомы, то написанное ниже может быть для вас непонятно. Поэтому я могу посоветовать найти время и хотя бы поверхностно познакомиться с двумя первыми главами из "0MQ - The Guide" (под названиями "Chapter 1 - Basics" и "Chapter 2 - Sockets and Patterns", хотя может хватить и только первой главы).

Прежде всего, мне кажется, что мне удалось понять психологию авторов "0MQ - The Guide" и ZeroMQ. Ну или не психологию, но их взгляд на то, чем они занимаются. Это эдакие гуру от message-oriented middleware, создавшие AMQP и ZeroMQ, построившие на их основе ряд сложных решений... Короче, съевшие собаку на этой теме. И, посему, рассказывающие о каких-то вещах с высоты своего многолетнего опыта. А менее опытным разработчикам может быть непонятно, почему ZeroMQ нужно выбрать вместо какого-нибудь RabbitMQ или Kafka. Или почему имеет смысл делать persistence на базе обычных файлов, а не СУБД.

Думаю, что лучшей иллюстрацией, которая лично мне объясняет много в ZeroMQ, будет служить байка про мостостроителей, рассказанная в самой "0MQ - The Guide". Вот мой вольный ее пересказ:

Два инженера-мостостроителя рассказывают о самых запомнившихся в их карьере мостах.

Первый инженер говорит, что мол они захотели построить хороший мост. Долго исследовали берега и русло реки. Выбрали наиболее удобное для строительства место. Заказали проект в серьезной фирме. Наняли самых лучших подрядчиков. Подвели построенные по последнему слову техники коммуникации. В общем, постройка моста заняла не один год и съела кучу денег.

Второй инженер говорит, что мы как-то с другом нажрались водки, пошли на берег реки, там перекинули веревку на другой берег. А этой веревкой начали пользоваться жители деревень по обе стороны реки. Потом там сделали подвесной мост. Потом более надежный мостик. Потом нормальный мост с каменными опорам. Потом его несколько раз переделывали и сейчас это большой современный мост, но он стоит именно там, где мы перекинули когда-то веревку.

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

В общем-то, весь "0MQ - The Guide" пронизан мыслью итерационного подхода к разработке. Мол, возьмите и сделайте самый простой прототип. Поэкспериментируйте с ним. Сделайте где-то улучшения. Проверьте их. Сделайте еще улучшения и т.д.

Собственно говоря, итерационный подход и плавная эволюция -- это замечательно и это, пожалуй, единственный работающий способ создавать сложный, объемный и, главное, работающий софт. Но, к сожалению, когда фреймворки, вроде ZeroMQ, развиваются по такой же эволюционной схеме, то хвосты старых веревок будут торчать в разных местах. Вовсе не делая фреймворк лучше или проще.

Так что еще одно сильное ощущение от ZeroMQ таково: развитие ZeroMQ шло эволюционно от покрытия нескольких простых сценариев (которые и стали базовыми для ZeroMQ) до более сложных, в которых требуется творческое комбинирование базовых схем взаимодействия ZeroMQ.

С этим же связано и другое очень сильное ощущение. Наверное, если у вас есть какая-то простая задача, которая 1-в-1 ложится на ту или иную схему взаимодействия ZeroMQ (например, на REQ-REP, PUB-SUB или PUSH-PULL), то ZeroMQ дает вам в руки готовый инструмент, на котором вы быстренько лабаете решение своей задачи прямо на коленке. Но вот если вам нужно что-то посложнее, скажем, схему с асинхронными REQ-REP, а еще и с возможностью выдавать запросы в обоих направлениях, то тут уж вам придется разобраться с другими схемами (вроде DEALER, ROUTER, XPUB/XSUB) и придумать, как из этого всего добра построить именно то, что вам нужно.

При этом, рискну предположить, вы не раз и не два зададите себе вопрос: "А почему бы просто не взять готовый MQ-брокер?"

И, что важно, в 2018-ом году вам сложно будет найти нормальный и убедительный ответ на этот вопрос. Все-таки ситуация по сравнению с 2007-ым, когда ZeroMQ родилась, сильно поменялась.

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


Теперь давайте уйдем от возвышенной лирики к приземленной физике и поговорим о некоторых деталях, которые меня реально зацепили и которые для меня служат индикатором... Как бы это сказать... Индикатором того, что не все так ладно в Датском королевстве.

Штука первая. Возможно главная. В ZeroMQ взаимоувязаны физические и логические каналы. Т.е. вы создаете ZeroMQ-сокет конкретного типа и далее используете этот сокет для конкретного взаимодействия между сущностями. Если вам нужен другой тип взаимодействия между этими же сущностями, то вы должны создать еще один сокет.

Например, представьте, что у вас два компонента A и B, каждый из которых должен иметь возможность сделать запрос к другому компоненту. Так, A может отослать B запрос ReqX, тогда как B в это же время может отослать A запрос ReqY. Чтобы сделать это в рамках ZeroMQ простыми средствами, вам нужно:

  • открыть в A REP-сокет и привязать его к конкретному порту (читай: создать серверный TCP-сокет);
  • открыть в B REP-сокет и привязать его к другому конкретному порту (читай: создать собственный серверный TCP-сокет);
  • создать в A REQ-сокет и связать его с REP-сокетом компонента B (читай: создать клиентский TCP-сокет для подключения к серверному TCP-сокету компонента B);
  • создать в B REQ-сокет и связать его с REP-сокетом компонента A;
  • запросы ReqX должны отсылаться в REQ-сокет компонента A;
  • запросы ReqY должны отсылаться в REQ-сокет компонента B.

Получается, что компонентам B и A нужно два физических канала, хотя, по сути, достаточно всего одного.

Это может казаться не столь существенным, но мы взяли очень простой пример, когда есть только компоненты A и B. Попробуем добавить сюда еще компонент C, который захочет обмениваться запросами ReqZ с A и B и количество физических каналов станет еще больше.

ИМХО, физические каналы связи должны управляться отдельно, а логические связи -- отдельно. Тогда можно создать всего один физический канал между A и B, который будет обслуживать как ReqX, так и ReqY. По-моему, если бы в ZeroMQ сделал такое разделение, то конструирование более сложных сценариев общения программ через ZeroMQ было бы более простым и понятным делом, чем это есть сейчас. Но, полагаю, здесь сказалась плавная эволюция ZeroMQ "от простого к сложному": на простых сценариях тождество между физическим и логическим каналом вполне естественно, а дальше уже просто выкручиваемся на основе того, что есть.

Штука вторая. Составные сообщения.

В ZeroMQ есть возможность отсылать сообщения, состоящие из нескольких частей. Причем это принципиальная и, как я понимаю, архиважная фича, т.к. на ее использовании базируется работа более сложных схем взаимодействия компонентов (когда приходится задействовать ROUTER-ы и DEALER-ы).

Но вот механизм работы с этими составными сообщениями выглядит специфически. Оказывается, что ZeroMQ не отсылает составное сообщение на удаленную сторону пока пользователь не отошлет все части сообщения. И ZeroMQ на удаленной стороне не доставляет части сообщения до пользователя, пока он не примет все части сообщения.

По сути получается, что сообщение в ZeroMQ имеет вид чего-то вроде:

struct zmq_message {
  std::vector<std::string> parts_;
};

Но вот когда вы работаете с сообщениями в ZeroMQ, то вы работаете не с целым объектом, внутри которого находится несколько фрагментов данных, а вы работаете как бы с несколькими независимыми объектами, у которых выставлен признак "есть продолжение". Поэтому, вы читаете один фрагмент как отдельное сообщение. Затем проверяете, есть ли у фрагмента продолжение. Если есть, то читаете следующий фрагмент и т.д. Примеры можно посмотреть здесь

В принципе, эту низкоуровневую кухню можно обернуть какой-нибудь более высокоуровневой оберткой (вероятно, готовая C++ная обертка вокруг ZeroMQ именно так и поступает). Но, тем не менее, осадочек-то остается. Причем происходит это, полагаю, опять же из-за эволюционного развития ZeroMQ. Может быть в самом начале составных сообщений и не было вовсе. Но потом их добавили. Но так, чтобы не сломать чего-то важного (API или wire protocol). Поэтому и получилось так, как получилось.

Штука третья. Так же очень весомая. Общее положение дел с ZeroMQ.

Когда-то ZeroMQ, насколько я помню, была больше на слуху, чем сейчас. Плюс к тому, несколько лет назад была громкая история о том, что один из авторов ZeroMQ, Мартин Сустрик, с довольно большим шумом переключился с ZeroMQ на nanomsg. После чего nanomsg некоторое время развивался, но в итоге, заглох. И сейчас дело nanomsg продолжается в виде nanomsg-nng (которая, насколько можно судить, разрабатывается силами всего одного человека, который при этом ведет и порт nanomsg на Go под названием mangos).

Понятное дело, что за ZeroMQ стоит не один человек, а целое коммьюнити. Поэтому ZeroMQ жив и, возможно, здоров. Но какие перспективы у него в современном мире? Ведь перечень проблем с ZeroMQ, которые толкнули на разработку nanomsg/nanomsg-nng, никуда не делись. И ряд из них вполне себе обоснованы и сохраняют свою актуальность. Ознакомиться можно здесь (Differences between nanomsg and ZeroMQ) и здесь (Rationale: Or why am I bothering to rewrite nanomsg?).

В общем, нет у меня ощущения, что у ZeroMQ светлое будущее. Свою роль ZeroMQ, безусловно, сыграл. На нем базируется куча софта. Понятное дело, что ZeroMQ будет оставаться на плаву. Но вот тот же COBOL так же спокойно живет в своей нише, но мало кто захочет тыкать его даже трехметровой палкой ;) ИМХО, ZeroMQ повезло, что история с nanomsg закончилась настолько печально. Если бы nanomsg дошел до стабильной версии, то выбор между ZeroMQ и nanomsg, полагаю, во многих случаях был бы в пользу nanomsg.


Ну вот какие-то такие мысли по поводу самого ZeroMQ. Понятно дело, это не все мысли. Но не писать же целый трактат :)

Для себя я сделал такой вывод: в сторону ZeroMQ/nanomsg имеет смысл смотреть только если вы точно знаете, почему для вашей задачи не подходят какие-то другие решения. Для многих задач, полагаю, гораздо проще будет использовать готовые брокеры на базе MQTT, AMQP, Kafka и т.д. Или какие-то RPC-механизмы, вроде gRPC. Или же вы сможете использовать то, что уже встроено в инструменты вроде MPI и HPX. Или даже простой RESTful API может оказаться удобнее, чем ZeroMQ.

Комментариев нет: