Современные масштабируемые системы состоят из микросервисов, каждый из которых отвечает за свою ограниченную задачу. Такая архитектура позволяет не допускать чрезмерного разрастания исходного кода и контролировать технический долг.
В нашем проекте десятки микросервисов, каждый из которых зарезервирован: две или более абсолютно идентичных копии сервиса установлены на разных физических серверах, и клиент (другой микросервис) может обращаться к любой из них независимо.
Если микросервис перестает отвечать на запросы в результате аварии, его клиенты должны быть мгновенно перенаправлены на резервный. Для управления потоком запросов часто используют так называемые очереди сообщений (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 не потерял ни одного сообщения.
Мы довольны своим выбором. Надеемся, наш опыт окажется полезным вам.
Автор: Pyrus