RabbitMQ

Когда веб-приложение перестает быть просто коллекцией скриптов, генерирующих HTML, встает вопрос о взаимодействии различных компонентов системы. Есть два основных подхода:

  • обращение напрямую посредством протоколов вроде Thrift или Protocol Buffers;
  • либо посредством брокера сообщений, посредника, берущего на себя вопросы их маршрутизации и доставки одному или нескольким получателям, даже в случае сбоев оборудования и недоступности сетевого соединения.

Сегодня я хотел бы рассказать об одной из лучших, на мой взгляд, реализаций брокера сообщений, RabbitMQ. Хотите узнать почему я так считаю? - Дочитайте до конца :)

Основные понятия

Слоганом RabbitMQ является "обмен сообщениями, который просто работает". Отчасти с этим утверждением можно согласиться, для того чтобы сервис обмена сообщениями "просто заработал" достаточно простой команды aptitude install rabbitmq-server или аналога для операционных систем, не основанных на Debian. Но кому этого будет достаточно? Как минимум нужно научить свой проект эти сообщения отправлять и принимать, а как максимум - обрабатывать десятки и сотни тысяч сообщений в секунду, но обо всем по порядку.

В основе RabbitMQ лежит протокол AMQP, который вводит три основных понятия:

  • Сообщение(message) - единица информации, которая передается от отправителя к получателю(ям); состоит из набора заголовков и содержания, которое брокером никак не интерпретируются.
  • Точка обмена(exchange) - распределяет отправленные сообщения между одной или несколькими очередями в соответствии с их заголовками.
  • Очередь(queue) - место, где хранятся сообщения до тех пор, пока их не заберет получатель.

Базовые механизмы взаимодействия с брокером очень просты:

  • Отправить сообщение(publish) - сообщение сериализуется в определенный формат, при необходимости снабжается маршрутной меткой (routing key) и передается в RabbitMQ;
  • Получать сообщение(consume или subscribe) - приложение регистрируется в RabbitMQ с указанием какие именно сообщения оно готово получать и обрабатывать, после чего ожидает их доставки.

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

  • Отправка всем(fanout) - как следует из названия, каждое сообщение получат все очереди, связанные с данной точкой обмена, типичная публикация-подписка (publish-subscribe).
  • Прямая(direct) - сообщение получит только та очередь, которая имеет название, соответствующее маршрутной метке сообщения, типичная очередь сообщений (message queue).
  • Тематическая(topic) - очереди при регистрации указывают паттерн маршрутных меток сообщений, которые они хотели бы получать. Этот механизм позволяет наиболее гибко управлять маршрутизацией сообщений и строить нетривиальные схемы доставки. Вместо регулярных выражений используется очень простая схема: метки в виде слов, разделенных точками; в паттерне * заменяет ровно одно слово, # - ноль или больше; при отсутствии этих символов работает как прямая точка обмена.

Если Вашему приложению достаточно простых подписки-публикации или очереди сообщений, а также нет необходимости гарантировать доставку сообщений или обрабатывать потоки сообщений, превышающие возможности одного сервера, то можно рассмотреть более простые в эксплуатации решения, не основанные на AMQP. В такой ситуации я рекомендовал бы первым делом взглянуть на Redis. Если это не про Вас, то продолжаем разбираться с RabbitMQ.

Типичные сценарии

Выполнение длительных операций

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

По-старинке эти операции делают последовательно в том же обработчике запроса, который и принял от пользователя файл. В схеме с брокером же после принятия файла он отправляет сообщение, в содержании которого будет, вероятно, ссылка на файла-оригинал, после чего он возвращает браузеру сообщение об успешной загрузке файла. Для отправки таких сообщений используют прямую точку обмена, с какой-то стандартной маршрутной меткой и соответствующим именем очереди, например process_video или create_thumbnails. Процессы, реализующие совершенно независимый сервис по выполнению этих длительных операций, будут по очереди забирать сообщения с "заданиями" из брокера, позволяя легко создавать любое количество исполнителей c балансировкой нагрузки, что обеспечит горизонтальное масштабирование этой подсистемы.

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

Удаленный вызов (RPC)

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

Для этого предусмотрено два заголовка сообщений:

  • Обратный адрес(reply to) - исполнитель должен отправить результат в очередь с указанным именем; отравитель сразу же после передачи сообщения-запроса брокеру начинает получать сообщения из указанной в этом заголовке очереди.
  • Идентификатор запроса(correlation id) - должен быть уникальным среди запросов, чтобы отправитель мог сопоставить результаты с запросами.

Сообщения пользователям

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

В этом примере очень важно использовать режим постоянных сообщений (persistant, путем установки заголовка delivery_mode=2), так как получатель сообщения может появиться очень не скоро и важно чтобы сообщения "переживали" даже полный перезапуск брокера сообщений. Для более короткоживущих сообщений это менее критично, но тоже порой актуально, особенно как еще одна мера для обеспечения отказоустойчивости.

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

Двустороннее соединение с браузером

Пожалуй, самый "вкусный" пример, хоть и лежащий на поверхности. На многих крупных интернет-проектах, особенно социальной направленности можно увидеть уведомления в реальном времени о событиях на сайте - кто-то что-то написал, поставил +1, проголосовал и т.п.

Реализация этого функционала требует довольно серьезной работы как на стороне браузера, так и на серверной стороне. Браузерный вопрос выходит за рамки этой статьи (хотя тут у меня тоже есть что рассказать, отдельным постом когда-нибудь обязательно напишу), а вот на серверной стороне брокер сообщений окажется очень даже кстати, особенно в реализации RabbitMQ.

На серверной части эта задача делится на две части:

  • Поддерживать постоянное соединение со всеми пользователями, кто находится онлайн - здесь на помощь обычно приходит либо Erlang, либо неблокирующий сервер на epoll. Оба варианта очень неплохие, выбирайте сами.
  • Дальше нужно как-то организовать доставку сообщений (информацию о событиях в системе) между пользователями, где и вступает в игру брокер. Обработчик соединения подписывается на сообщения о публичных событиях (точка обмена "отправить всем"), и туда же отправляет информацию о действиях пользователя-владельца.

Чем больше пользователей онлайн, тем больше сообщений в единицу времени будет проходить через брокер. Один сервер перестанет справляться довольно быстро, так что следующий раздел статьи окажется очень кстати.

Кластеризация

Многое из вышеизложенного справедливо и для других реализаций AMQP, но в вопросе кластеризации RabbitMQ предстает во всей красе. Залогом этого в первую очередь является использование Erlang, не знаю почему я до сих пор не написал статью про этот язык программирования, здесь достаточно было бы на нее сослаться и все стало бы ясно.

Если вкратце, то в Erlang реализована внутренняя система легковесных процессов, не имеющая общего состояния и взаимодействующая друг с другом исключительно посредством обменом сообщений. При этом с точки разработчика отправка сообщений другому процессу на том же физическом сервером и на удаленном выглядит одинаково, и даже является одним из операторов языка - "!", наравне с "=", "+" и.т.п. Этот факт позволяет приложениям или их частям взаимодействовать по сети так же легко, как и в рамках одного сервера.

Чтобы определить разрешено ли разным Erlang-сервера взаимодействовать друг с другом, они обмениваются хэшем пароля (который правда называют cookie, хотя с одноименным механизмом браузеров он ничего общего не имеет) и продолжают работу только если он совпал. Он должен быть одинаковым на всех узлах и хранится в файле ~/.erlang.cookie, для RabbitMQ это обычно /var/lib/rabbitmq/.erlang.cookie - первым делом нужно решить этот вопрос, а также убедиться, что используется нестандартное значение.

Узлы в RabbitMQ кластере могут быть двух типов: работающие только в памяти и сохраняющие данные на диск. Так как состояние системы реплицируется между узлами кластера, в большинстве случаев достаточно иметь лишь 2-3 дисковых узла, а остальные избавить от необходимости работать с дисковой подсистемой для увеличения производительности.

Важно понимать, что под состоянием системы здесь имеются ввиду лишь привязки и настройки брокеров, каждая же очередь и хранящиеся в ней сообщения располагаются на одном конкретном узле, что приведет к потери части сообщений при сбое одного из серверов. Этот вопрос можно решить и средствами операционной системы, но чаще всего правильнее выделить критически-важные для системы очереди сообщений и включить их репликацию средствами RabbitMQ, этот механизм называется зеркальные очереди(mirrored queues).  Репликация происходит по принципу мастер-слуга(master-slave), как и в реляционных СУБД: все операции осуществляются на основном сервере (мастере), он транслирует их на один или несколько вторичных серверов (слуги), при каком-либо сбое на основном один из слуг "повышается" до статуса мастера и берет на себя его функции. Очереди могут быть объявлены зеркальными только при создании, но новые узлы в роли слуг могут добавляться и позже, в таком случае новый слуга начнет получать входящие сообщения и рано или поздно начнет полностью отражать его состояние, механизма синхронизации при подключении дополнительного слуги не предусмотрено. Последним шагом для гарантированной доставки сообщений, не упоминавшимся ранее, является механизм уведомления отправителя об успешной записи сообщения в очередь (на все сервера для зеркальных).

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

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

Подводим итоги

Используя брокер сообщений при технической реализации интернет-проекта, можно перевести его на совершенно новый уровень с точек зрения отказоустойчивости и горизонтальной масштабируемости. Во многих случаях он становится "сердцем" приложения, без которого его существование было бы немыслимо, но в то же время благодаря кластеризации не становится единственной точкой отказа(single point of failure).

Хоть многое из упомянутого в статье можно реализовать и с помощью других технологий, RabbitMQ является наиболее приспособленной к реалиям современного Интернета реализацией брокера сообщений и AMQP в частности, в первую очередь благодаря распределенной природе Erlang и качественно спроектированной архитектуре этого продукта.

В комментариях с удовольствием обсудил бы применение RabbitMQ и других брокеров сообщения в различных практических ситуациях; еще можно подискутировать по поводу его преимуществ и недостатков по сравнению с альтернативами, в каких ситуациях это проявляется.

Жду Вас среди постоянных читателей Insight IT, число которых недавно перевалило за 14 тысяч :)

10 марта 2012 |  Иван Блинков  |  Erlang