Сложные системы состоят из множества компонент, между которыми возникает необходимость организовывать взаимодействие, часто сводящееся к передаче сообщений. Разные команды разработчиков решали эту задачу по-разному. Итак, как же в рамках экосистемы Ruby можно решить эту задачу?
- Если посмотреть на наш постер с Open Cirrus 2011, то можно заметить, что мы в первой черновой реализации нашего сервиса использовали фреймворк Gearman и официальный гем gearman-ruby. Gearman создан ребятами из LiveJournal для связывания всех сервисов своей платформы в единое целое. К сожалению, архитектура «клиент → сервер задач → обработчик» не всегда адекватна задаче: например, просто так связать точку А с точкой Б не получится.
- Ребята из GitHub сочинили протокол BERT на основе терминов (terms) языка Erlang и связали свои сервисы при помощи фреймворка Ernie, написанного на Ruby и Erlang, и реализующего протокол BERT-RPC. Достаточно посмотреть, как работает этот инструмент, чтобы назвать статью “Fuck You, Erlang!”, как это было сделано ранее и повлекло за собой кучу справедливого непонимания со стороны читателей. Erlang — отличный язык, OTP — крутая платформа. Ernie, построенный на их основе, кажется ужасно неадекватным решением.
- На фоне существующих решений выделяется библиотека ØMQ: The Intelligent Transport Layer. Несмотря на наличие суффика “mq” в названии, ØMQ не является очередью сообщений в привичном для нас виде. Кстати, авторы ØMQ (iMatix Corporation) в своё время разработали протокол AMQP и сейчас на основе предыдущего опыта постарались максимально приблизиться к удобству работы с сообщениями в Erlang/OTP.
ØMQ — очень эффективная и навороченная библиотека для организации обмена сообщениями, не привязанная к конкретному транспортному уровню. Внутри одного процесса можно устроить обмен сообщениями между потоками поверх UNIX-сокетов или внутрипроцессового IPC, а удалённые приложения могут «разговаривать» друг с другом поверх протоколов TCP или UDP. Более того, ØMQ ориентирована на верхний уровень модели OSI, поэтому даже сообщение в полмегабайта переданное из пункта А в пункт Б придёт в целости и сохранности.
Внутри ØMQ реализованы традиционные паттерны построения сетевых приложений: парадигма «запрос-ответ», «издатель-подписчик», механизмы маршрутизации сообщений, построения отказоустойчивых систем и многое другое, описанное в гайде.
Итак, главное для нас — сообщения. Сообщениями обмениваются изолированные приложения при помощи специальных сокетов в контексте ØMQ.
Рассмотрим более-менее прикладную задачу. Пусть имеется Web-приложение, при помощи которого пользователи обрабатывают что-то большое. Это самое «что-то большое» должно обрабатываться и отдаваться клиентам, не вынуждая систему отказывать им в обслуживании из-за высокой нагруженности. Логично разделить приложение на две части (как показано на рисунке):
- frontend: Web-интерфейс и трекер заявок;
- backend: обработчик заявок.

- Frontend работает с конечным пользователем по протоколу HTTP. С точки зрения механизма взаимодействия это очевидный «запрос-ответ», причём не имеющий отношения к ØMQ.
- Общение Frontend и Backend ведётся посредством двух UNIX-сокетов:
pub.sockиpull.sock. - Когда Frontend получает заявку, он передаёт её в Backend с использованием механизма push-pull, то есть Backend постоянно слушает данные, поступающие от Frontend и немедленно реагирует на их появление.
- Как только Backend заканчивает обрабатывать заявку, он сигнализирует об этом Frontend при помощи механизма publish-subscribe, то есть заранее подключенный Frontend (подписчик) «висит» на Pub-сокете и через некоторое время получает данные от Backend.
Возьмём Sinatra и биндинги к ØMQ. Кстати, из-за особенностей реализации потоков в MRI, следует использовать версию Ruby 1.9 и неофициальный гем ffi-rzmq. Официальный гем zmq «подвешивает» интерпретатор при завершении работы.
В качестве нагрузки будем вычислять CRC32-хэш переданной строки и вызывать sleep() на несколько секунд внутри бэкэнда, чтобы дать видимость сложной вычислительной задачи. Обмениваться данными будем в формате JSON при помощи библиотеки Yajl.
Итак, код бэкэнда. Вообще, я старался комментировать код, хотя проблем при чтении быть не должно: всё очень просто.
loop do
begin
logger.info(recv = pull.recv_string)
data = Yajl.load(recv).first
# imagine that we're processing this piece of crap
crc32 = Zlib.crc32(data)
sleep 2
rep = [ data, crc32 ].to_json
pub.send_string rep
logger.info(rep)
rescue Interrupt
break
end
end
Теперь код фронтэнда. Это вполне себе обычное приложение на микрофреймворке Sinatra.
get '/' do
# index template should be saved at views/index.erb
erb :index
end
post '/' do
data = params['data'] || ''
if data.strip.empty?
return redirect '/'
end
$queries[data] = nil
# send query to backend
push.send_string [data].to_json
redirect '/'
end
Кстати, не забываем положить файл с ERb-шаблоном страницы по адресу views/index.erb относительно остальных файлов.
Итак, для начала запускаем backend.rb, который забивает на себя UNIX-сокеты и готовится обрабатывать данные. После этого запускаем frontend.rb и заходим браузером по адресу http://localhost:4567.
Как видно, если добавить в очередь несколько заявок подряд, то они выполнятся последовательно. Таким образом, всю работу по доставке данных от фронтэнда к бэкэнду и обратно выполнила ØMQ, за что ей большое спасибо. У меня это выглядит примерно вот так:

В качестве домашнего задания можно прочесть гайд и попробовать добавить к фронтэнду возможность работы с несколькими бэкэндами, балансировки нагрузки между ними, в результате получив функциональный аналог Gearman.
Приведённый в статье пример может показаться слишком простым. Это действительно так, и он не показывает даже верхушку айсберга возможностей ØMQ, но исключительно базовые вещи. В качестве материала для дальнейшего чтения могу категорически порекомендовать две шикарные статьи Ilya Grigorik [1,2].