Выбор MQ для высоконагруженного проекта


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

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

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

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

Проверенное решение

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

Лидером по популярности у разработчиков является RabbitMQ. Это проверенное временем решение класса Enterprise с гарантиями доставки, гибкой системой маршрутизации и поддержкой всевозможных стандартов. Руководители проектов любят его, как в начале 80х покупатели компьютеров любили IBM PC. Эта любовь наиболее точно выражается фразой «Nobody ever got fired for buying IBM.»

Нам RabbitMQ не подошел потому, что он медленный и дорогой в обслуживании.

Производительность RabbitMQ не превышает десятки тысяч сообщений в секунду. Это хороший результат для многих применений, но совершенно неудовлетворительный для нашего случая.

Конфигурировать кластер RabbitMQ непросто, это отнимает ценные ресурсы devops. Кроме того, мы краем уха слышали о нареканиях по работе кластера — он не умеет сливать очереди с конфликтами, возникшими в ситуации «split brain» (когда вследствие разрыва сети образуются два изолированных узла, каждый из которых считает, что он главный).

Очередь на базе распределенного лога

Мы посмотрели на Apache Kafka, которая родилась внутри компании LinkedIn как система агрегации логов. Kafka умеет выжимать бОльшую производительность из дисковой подсистемы, чем RabbitMQ, поскольку она пишет данные последовательно (sequential I/O), а не случайно (random I/O). Но никаких гарантий, что запись на диск всегда будет происходит последовательно, получить нельзя.

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

Кроме того, для управления кластером Kafka требуется отдельный сервис (zookeeper), что опять же усложняет обслуживание и нагружает devops.

Мы не готовы рисковать в production потерей производительности, поэтому продолжили поиск.

«Гарантированная» доставка сообщений

Есть замечательная табличка от Jeff Dean, ветерана Google (работает там с 1999 года):

Latency Comparison Numbers
--------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy             3,000   ns        3 us
Send 1K bytes over 1 Gbps network       10,000   ns       10 us
Read 4K randomly from SSD*             150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from disk    20,000,000   ns   20,000 us   20 ms  80x memory, 20X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms

Видно, что запись на диск в 15 раз медленнее отправки по сети.

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

«Понятное дело, это нужно для обеспечения гарантии доставки,» — скажете вы. — «Ведь если адресат получит сообщение, но, не успев его обработать, упадет из-за отказа железа, очередь должна доставить его повторно.»

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

Высокопроизводительные очереди

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

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

Очередь на СУБД?

В какой-то момент мы почти отчаялись и нам показалось, что проще уже будет написать очередь самим поверх СУБД. Это может быть SQL-база данных или одно из многочисленных NoSQL-решений.

К примеру, у Redis есть специальные функции для реализации очередей. Поскольку Redis хранит данные в памяти, производительность прекрасная. Этот вариант был разумным, но смущало, что надстройка Sentinel, предназначенная для объединения нескольких узлов Redis в кластер, выглядела несколько искусственно, будто приделанной сбоку.

При использовании классической СУБД для получения сообщений пришлось бы использовать технику «long polling». Это некрасиво и чревато задержками в доставке. Да и не хотелось писать на коленке.

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

Решение найдено: NATS

NATS — относительно молодой проект, созданный Derek Collison, за плечами которого более 20 лет работы над распределенными очередями сообщений.

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

По производительности NATS опережает все очереди с «гарантированной доставкой». NATS написан на языке Go, но имеет клиентские библиотеки для всех популярных языков. Кроме того, клиенты NATS также знают топологию кластера и способны самостоятельно переподключаться в случае потери связи со своим узлом.

Результаты использования в production

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

Сервисы-отправители должны в случае ошибок повторять попытку отправки сообщения. (Впрочем, это не специфично для NATS и так требуется делать при работе с любой очередью).

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

Мы установили процесс NATS на все виртуальные машины с микросервисами. По результатам наблюдения в течение 2 месяцев NATS не потерял ни одного сообщения.

Мы довольны своим выбором. Надеемся, наш опыт окажется полезным вам.

Комментарии (5)

  • 19 апреля 2017 в 16:58

    +1

    Как-то маловато про NATS написано. Раз вы его выбрали, и, что более важно, он подошёл лучше, чем Кролик и Redis, хотелось бы более подробной информации о нём. Просто, к слову, я лично о нём до этого момента не слышал. И если он действительно настолько хорош, то возможно утащим себе в прод его.
  • 19 апреля 2017 в 17:02

    0

    Кстати, сколько там у Apache систем MQ? Четыре или уже больше?
  • 19 апреля 2017 в 17:08

    +2

    >> По результатам наблюдения в течение 2 месяцев NATS не потерял ни одного сообщения.
    Или вы не знаете о том что что-то потеряли)) Не могли бы вы рассказать о том что вы делаете в случае выхода из строя нескольких микросервисов (ситуация когда умер и получатели и отправители), спасет ли ваше логирование? Как реализован мерж после сплитбрейна? Какую пропускную способность получили?
  • 19 апреля 2017 в 17:27

    0

    Отсутствие данных используемых в сравнении MQ не дает уверенности в честности тестов.
    Так, NATS не гарантирует доставку, за доставку с гарантией отвечает NATS Streaming.
    Есть ощущение, что тесты производительности используют NATS, вместо NATS Streaming.

    • 19 апреля 2017 в 17:28 (комментарий был изменён)

      0

      Да, в тестах сравнивается именно NATS.

© Habrahabr.ru