В последнее время знакомился с разными инструментами, поддерживающими модель publish/subscribe (в первую очередь речь идет о MQTT и DDS). Подумалось, что в каких-то вещах начинает наблюдаться принцип "если у тебя в руках молоток, то все вокруг выглядит как гвозди". Т.е. модель pub/sub начинают использовать не совсем так, как это нужно.
Где-то pub/sub выглядит вполне естественно. Допустим, есть какой-то датчик температуры воздуха, который раз в 10 минут отсылает в сеть свое текущее значение. Тогда создается тема с именем, например, building-1/floor-2/temp-sensor-3, в которую датчик отсылает свои замеры. Тот, кто хочет читать значения именно этого датчика, тот подписывается именно на эту тему. Тот, кто хочет читать значения всех датчиков температуры с этого этажа, подписывается на несколько тем сразу, скажем, вот так: building-1/floor-2/temp-sensor-#. И таких подписчиков может быть несколько: один собирает информацию для управления климатом (например, когда температура выходит за заданные пределы, включается система кондиционирования), второй архивирует эту информацию для дальнейшей обработки (например, демонстрации графиков-трендов).
Совсем другое дело, когда pub/sub пытаются задействовать для организации взаимодействия друг с другом нескольких компонентов по принципу "запрос-ответ". Предположим, что есть состоящий из нескольких компонентов SMTP-сервер. Компонент-читатель получает очередной email и перед дальнейшей отправкой полученного письма хочет проверить, а не спам ли это. Для этого нужно сделать запрос к компоненту-анализатору-спама. Каким образом? Просто опубликовав запрос в теме spam-checking/request. Анализатор подписан на эту тему и, поэтому, рано или поздно получит этот запрос. Выполнив проверку компонент-анализатор опубликует ответ в теме spam-checking/response.
В принципе, мне уже здесь начинает казаться, что что-то идет не так. Подозрения усиливаются, если предположить, что для увеличения пропускной способности в нашем SMTP-сервере есть несколько компонентов-читателей, каждый из которых принимает email-ы и нуждается в их проверке. Вопрос в том, каким образом компонентам-читателям определяет, кому из них предназначался ответ в spam-checking/response?
Тут напрашивается сразу несколько решений:
- при публикации сообщения в теме spam-checking/response можно снабдить это сообщение каким-то мета-атрибутом, позволяющим однозначно определить получателя. По этому мета-атрибуту каждый из компонентов-читателей настраивает фильтры для темы spam-checking/response и, благодаря фильтрам, получает только свой трафик. Этот подход требует, чтобы pub/sub-система поддерживала мета-атрибуты (а разработчики знали про этот механизм и могли его использовать);
- для ответов создается не одна тема, а несколько, по одной на каждый компонент-читатель. Например, spam-checking/response-1, spam-checking/response-2 и т.д. Отсылая запрос компонент-читатель сообщает свой номер и ответ публикуется в той теме, которая создана персонально для этого читателя;
- каждый компонент-читатель создает свою собственную тему, например, receiver-1/spam-checking-result, receiver-2/spam-checking-result и т.д. Компонент-анализатор должен публиковать свои ответы в соответствующей теме.
По сути, все эти способы -- это все одни и те же яйца, только в профиль. Причем все способы требуют передачи вместе с запросом какого-то эквивалента обратного адреса. Т.е. все это, на самом-то деле, реализация peer-to-peer взаимодействия, только посредством того инструмента, который есть в наличии (отсюда и ощущение про молоток и гвозди).
Думаю так же, что если мы отходим от pub/sub в сторону простых очередей сообщений, то результат, на мой взгляд, получается более логичным. Т.е., если мы строим SMTP-сервер из компонентов на основе message queues, то у каждого компонента появляется своя очередь входящих сообщений. Например, receiver-1-queue, receiver-2-queue, spam-analyzer-queue и т.д. Нужно отправить запрос на анализ очередного письма? Просто пишем его в spam-analyzer-queue, а в качестве обратного адреса указываем имя очереди, в которую нужно отослать ответ (например, receiver-2-queue).
Однако, если у нас появляется такое peer-to-peer взаимодействие, то манипуляции с очередями начинают казаться чем-то низкоуровневым. Ну действительно, зачем нам знать про какие-то очереди? Нам нужно знать идентификатор/адрес получателя. А уже способ доставки сообщения до него -- пусть определяет промежуточный слой.
Т.е. приходим к чему-то вроде "шины данных" (пишу "вроде", т.к. термин этот может быть слишком перегружен): есть какая-то магистраль, по которой туда-сюда снуют сообщения. Компоненты подключаются к этой магистрали, для чего им нужны уникальные идентификаторы или адреса. После чего общение компонентов друг с другом осуществляется путем отсылки сообщения на адрес получателя. Все остальное определяется реализацией самой шины: будут ли это очереди на стороне отправителя или же получателя, или же это будет какое-то централизованное хранилище на главном брокере (или совокупность таких хранилищ), и т.д, и т.п.
Имхо, такая модель peer-to-peer взаимодействия компонентов через шину данных хорошо подходит для многостадийной обработки информации. Например, в сложных Web-приложениях, при обработке SMTP- или SMPP-трафика, при обслуживании платежных транзакций и т.д. Гораздо лучше, чем использование publish/subscribe моделей и инструментов. Хотя, у меня сложилось впечатление, что вендоры DDS-решений так не думают :)
PS. В принципе, для peer-to-peer взаимодействия можно использовать и простой синхронный RPC. Но это, на мой взгляд, и масштабируется хуже, и, со временем, могут всплыть вопросы с версионностью данных (расширять сообщения, как показывает практика, проще, чем синхронные интерфейсы, базирующиеся на чем-то вроде CORBA).
PPS. А еще мне кажется, что для задач, с которыми приходилось сталкиваться, нужно иметь промежуточное ПО, которое поддерживает сразу два способа взаимодействия -- и peer-to-peer через шину данных, и publish/subscribe. А местами и data-flow :)