19 апреля в 16:47

Выбор 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 не потерял ни одного сообщения.

Мы довольны своим выбором. Надеемся, наш опыт окажется полезным вам.
@Pyrus
карма
7,0
рейтинг 24,0
Пользователь
Самое читаемое Разработка

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

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

      Вот здесь можно какие-то впечатления прочитать:
      https://news.ycombinator.com/item?id=11284489
  • 0
    Кстати, сколько там у Apache систем MQ? Четыре или уже больше?
    • 0

      ActiveMQ, QPID (Java и C++), Kafka, и как минимум два развития ActiveMQ — Apollo и Artemis.


      Ну правда, два последних не совсем независимые проекты — скорее всего, выживет один из них, и заменит ActiveMQ.

  • +9
    >> По результатам наблюдения в течение 2 месяцев NATS не потерял ни одного сообщения.
    Или вы не знаете о том что что-то потеряли)) Не могли бы вы рассказать о том что вы делаете в случае выхода из строя нескольких микросервисов(ситуация когда умер и получатели и отправители), спасет ли ваше логирование? Как реализован мерж после сплитбрейна? Какую пропускную способность получили?
    • 0
      Спасибо за комментарий. Мы логируем время отправки и время окончания обработки каждого сообщения получателем, и в мониторинге висит триггер, если время окончания обработки все еще не заполнено через 10 минут после отправки.

      Некоторые сообщения мы можем безболезненно терять — например, push-уведомление на телефон с количеством новых задач. Обычно, через несколько минут будет новое такое сообщение, старое перестанет быть актуальным.

      Ситуация, когда умер хотя бы один сервис — исключительная. Это бывает при maintenance, минут 5-10 может. В крайнем случае — ночь может полежать (но всегда есть резерв). Конечно, 100% гарантия невозможна и между at-least-once / at-most-once delivery мы явно сделали выбор в пользу последнего. Нам важно, что мы не ожидаем сюрпризов от выбранного механизма MQ.

      Как я понимаю, каждый сервер кластера знает обо всех остальных и публикует темы (topic), на которые он подписан. Нет единого центра управления, никто не считается главным узлом, поэтому и split brain в классическом понимании (2 главных узла) невозможен. Полный peer-to-peer.

      Пропускную способность в production не измеряли, нам достаточно, что слой MQ незаметен и latency между разными физическими машинами в районе 1ms. Когда будет время, соберем статистику, напишем отдельно.
      • 0
        Спасибо за ответ. Хотел бы еще уточнить:

        >> Полный peer-to-peer.
        То есть у вас нет вероятности того, что кластер разделится на N подсетей? Допустим где-то маршрутизатор умер. И либо умерло все либо все живое?

        И вот интересный вопрос — сложность в настройке и поддержке, ну допустим по сравнению с кроликом сильно различается?

        P.S.

        >> Ситуация, когда умер хотя бы один сервис — исключительная
        Вот прям везет вам, что никогда оборудование не умирает)
        • 0
          Вот прям везет вам, что никогда оборудование не умирает)
          Ух, если бы только оборудование. У нас сервера используются на Azure и Soft Layer. И вот на Soft Layer довольно часто (чаще, чем раз в месяц) проблемы подобного рода, как правило «Emergency Maintenance». И в эти моменты может связь между серверами пропасть (сплит брейн), могут просто перезагрузить виртуалку. И т.п.
          Хорошо, если у вас относительно стейтлес сервисы, когда важно, чтоб хоть один жил, но если это база данных, к примеру, то выход из строя большинства нод из кворума весьма печальные последствия имеет =\
        • 0
          >> То есть у вас нет вероятности того, что кластер разделится на N подсетей? Допустим где-то маршрутизатор умер. И либо умерло все либо все живое?

          У нас полный набор всех сервисов в 2 независимых дата-центрах (и некоторые еще в третьем). Если упал маршрутизатор в одном, считаем, что упал весь дата-центр.

          Вероятность, что распадется на N подсетей или откажут оба дата-центра одновременно — есть. Но мы считаем ее меньше вероятности падения кирпича на голову.

          >> И вот интересный вопрос — сложность в настройке и поддержке, ну допустим по сравнению с кроликом сильно различается?

          Не можем подсказать — опыта его администрирования и использования в production у нас нет.

          >> Вот прям везет вам, что никогда оборудование не умирает)

          Умирает, просто мы считаем это штатной ситуацией.
          • 0
            Окей. Тогда если у вас будут какие-то statefull-сервисы и упадет один из DC, то потом миграция данных из одного в другой может быть жуткой болью.
  • +5

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

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

        Тогда это не честный тест, так как некоторые другие участники гарантируют доставку. Основной тратой времени обработки запроса как раз и является организацией этой гарантии.

        • 0
          Согласен. Мы сделали выбор в пользу производительности, пожертвовав условной гарантией доставки.
          • +1
            Ну тогда возможно уместнее было бы сравнивать производительность не NATS vs. RabbitMQ, Kafka, ZeroMQ или «очередь на СУБД», а NATS vs. «in-memory очередь за лоад-балансером»? Отмечу, что реализация in-memory очереди есть в стандартных библиотеках большинства ЯП.
            • +1

              Считаю, что такой вопрос целесообразнее задавать автору теста производительности, но в целом я соглашусь с вашим сравнением реализации:)

            • 0
              Компоненты могут быть написаны на разных ЯП. В стандартных in-memory очередях обычно нет логики сетевого переподключения к другому адресату, когда текущий адресат умер.

              Про производительность соглашусь с VlastV — вопрос к автору тестов. Мы же искали не самое производительное решение, а «good enough» для наших задач.
              • +5
                По-моему, имеет место некоторая путаница в показаниях.

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

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

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

                Хм… ну ладно, наверное отказоустойчивость все-таки была не самым главным критерием. Производительность важнее. Ок, возьмем in-memory очередь: максимальная производительность, нулевой оверхед…

                Мы же искали не самое производительное решение, а «good enough» для наших задач.

                Вот тут я уже реально запутался… Производительность теперь не главное, все должно быть «good enough». Почему NATS «good enough»? Почему вы сравнивали его с решениями из других классов (например, Kafka), но не сравнивали с решениями из примерно того же класса, например с Redis?
                • 0
                  По отказоустойчивости у нас было два требования:

                  1) автоматическое переключение маршрутизации на резервный сервис в случае умирания основного (или лучше — прозрачная поддержка пула consumers по каждому topic с маршрутизацией round-robin и автоматическим исключением из пула в случае умирания сервиса);

                  2) минимизация человеческого фактора при конфигурировании и расширении кластера.

                  C Redis сравнивали, об этом написано в посте, мы сочли, что он проигрывает NATS по второму критерию.

                  Все упомянутые решения достаточно производительны/надежны для большинства практических применений. Разница проявляется лишь в краевых случаях.
  • +4
    >> В Kafka данные делятся по разделам (partition) и чтобы соблюдать порядок доставки каждый получатель сообщений читает данные ровно из одного раздела

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

    >> Это может приводить к блокировке очереди в случае, когда получатель по каким-либо причинам обрабатывает сообщения медленнее обычного.

    каждая партиция независима, о какой блокировке всей очереди идет речь? так как каждая партиция полностью независима, то у вас просто будет затык в определенной партиции, как поступать в этом случае отдельный вопрос, но остальные партиции продолжат работать без проблем.

    а теперь реальные проблемы кафки если использовать как очередь запросов:

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

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

    Не знаю каким боком автор мерил производительность, но у меня в похожей ситуации и размере сообщений получалось из кафки 400k+ messages / second, а дальше упиралось в мой код.

    Поэтому пока это смотрится: а смотрите что мы на Go сумели написать, Go сейчас модный, так что вы должны выбрать нас.
    • 0
      Спасибо за комментарий, очень ценно услышать опыт реального использования kafka.

      >> каждая партиция независима, о какой блокировке всей очереди идет речь?

      вы правы, речь именно о блокировке одной partition, спасибо.

      >> Поэтому пока это смотрится: а смотрите что мы на Go сумели написать, Go сейчас модный, так что вы должны выбрать нас.

      Хуже того, они вначале на ruby написали (еще более модный), работало медленно, переписали на Go. :)
    • 0
      если у вас одну партицию разгребает за читателем пул потоков, то непонятно как комитить оффсет

      Сделать n читателей (по размеру пула, например) в рамках одной группы. Каждый из них будет читать 1 сообщение, обрабатывать, коммитить, потом читать следующее. При такой схеме не обязательно строго привязывать читателя к партишону в кафке.
      • 0
        >> Сделать n читателей (по размеру пула, например) в рамках одной группы

        нельзя сделать читателей больше чем количество партиций, 1 партиция — 1 читатель.
        архитектурное ограничение для ускорения работы, так как broker делает sendfile между файловым дескриптором и дескриптором на сокет. вернее создать можно, но читать реально будет только по количеству партиций, остальные будут стоять в очереди пока какая-либо партиция не освободится

        >> Каждый из них будет читать 1 сообщение, обрабатывать, коммитить, потом читать следующее.

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

        >> При такой схеме не обязательно строго привязывать читателя к партишону в кафке.

        нельзя не привязывать читателя, подключение нового читателя обычно проходит по схеме:
        1) подключился новый читатель в группу
        2) бросилось всем читателям в группе сообщение что сейчас будет ребалансинг, притормозите работу
        3) разослалась инфа о том кто какую партицию теперь будет обрабатывать

        то есть вычитать не из той которая к тебе привязана нельзя, а процесс перепривязки достаточно долгий.
        поэтому обычно читатель работает с 1+ партициями,

        при вашей схеме держа:
        M серверов по N потоков, P — количество партиций

        каждый поток имеет своего читателя,

        M*N > P — мы хоть и имеем несколько потоков которые курят, но зато каждый поток получает упорядоченные данные и нормально может комитить офсет.

        M*N < P — тут в один поток влетает перемешенные данные из 2х партиций, при желании можно организовать доп логику и тоже работать, просто нужно будет проверять с какой партиции пришло сообщение и отправлять commit offset не общий, а именно в эту партицию

        если же за 1 читателем находится пул потоков, то тут уже мешанина и async обработка неупорядоченная. проблему выше описал

        p.s. чтобы было понятно: кафка отправляет подтверждение не на отдельное сообщение, а offset в пределах очереди, как бы говоря «все что было до этого оффсета я обработала»
        • 0
          Вы написали много правильных вещей, я прокомментирую то, что непосредственно относится к предложенному мной варианту.

          если же за 1 читателем находится пул потоков, то тут уже мешанина и async обработка неупорядоченная. проблему выше описал

          Поэтому так и не надо делать.

          M*N > P — мы хоть и имеем несколько потоков которые курят, но зато каждый поток получает упорядоченные данные и нормально может комитить офсет.

          Нет особого смысла держать консьюмеров больше, чем количество партишонов в топике.

          M*N < P — тут в один поток влетает перемешенные данные из 2х партиций, при желании можно организовать доп логику и тоже работать, просто нужно будет проверять с какой партиции пришло сообщение и отправлять commit offset не общий, а именно в эту партицию

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

          Если придерживаться ваших обозначений, то я всегда использую схему M*N <= P, и никаких проблем с коммитом оффсетов для каждого консьюмера нет.
          • 0
            для вашего случая как уже сразу писалось нужно держать достаточно большое количество партиций, чтобы при необходимости масштабирования всем хватило работы. а устанавливать 100+ партиций не всегда оправдано из-за накладных расходов.

            вот только я не пойму схему работы на версиях до 0.9 включительно:

            1) когда вы делаете poll вам вернулся map с различных партиций или пачка сообщений даже из одной,
            2) если обрабатывать последовательно даже одним потоком и в самом конце делать commit, то все равно остается вопрос что делать если обработали несколько, а потом упали, оффсет незакомиченный, а данные обработаны

            согласен что на 0.10 данной проблемы нету так как можно прочитать ровно 1 сообщение, до 0.9 включительно нужно городить костыли и по мере обработки сообщения вызывать не просто commitSync()/commitAsync() (которые закомитят максимальный офсет из того что вам вернули по poll), а вариант с указанием где и что комитим

            void commitSync(Map<TopicPartition, OffsetAndMetadata> offsets);


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

            тут именно вопрос чтобы исключить двойную обработку отдельных сообщений
            комитить в конце обработки блока не проблема и на 0.9, но иногда это не вариант =\
            • 0
              1) когда вы делаете poll вам вернулся map с различных партиций или пачка сообщений

              Причем вместе с каждым сообщением есть еще его оффсет в рамках данного партишона. Т.е. и номер партишона, и оффсет известны для каждого сообщения.

              2) если обрабатывать последовательно даже одним потоком и в самом конце делать commit, то все равно остается вопрос что делать если обработали несколько, а потом упали, оффсет незакомиченный, а данные обработаны

              Все правильно, делать коммит после обработки всей пачки небезопасно. Поэтому надо коммитить оффсет после обработки каждого отдельного сообщения. Еще раз повторюсь, вся необходимая для этого информация, а именно, номер партишона и оффсет этого конкретного сообщения — доступна.
              • 0
                я согласен что есть =)

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

          >> нельзя сделать читателей больше чем количество партиций, 1 партиция — 1 читатель.

          здесь пошла какая-то путаница, неявно подразумевается что используется consumer group, зачем этот режим выбран и тут же обругивается непонятно
          • 0
            это уточнение было на то, что можно повесить читателей сколько хочешь, пока они в пределах группы.
            я уточнял что даже в пределах группы не более чем количество партиций.

            простейший пример:
            1) имеем топик на 10 партиций
            2) 20 читателей с одной consumer group

            вопрос:
            сколько читателей будет одновременно работать?
            10 будут читать, другие 10 будут висеть в воздухе ожидая пока кто-то из тех 10 отвалится.

            до тех пор пока вы продолжаете использовать «то я всегда использую схему M*N <= P» все действительно можно разрулить, вот только я и начал разговор с того что

            >> в общем представление партиции как упорядоченный лог накладывает свои ограничения, а держать 100500 партиций в пределах топика тоже не хочется, так как это дополнительные накладные расходы.

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

            то есть используя схему M*N <= P мы можем получить что все вычитали, сидят курят, а один читатель продолжает пыхтеть из-за неравномерности распределения сообщений по партициям. и никто из его группы ему помочь не может. (хотя признаю что на такое только раз или два нарывался)
  • +2
    Предполагаю, что когда вы искали очереди и наткнулись на NATS который написан на Go, должны были бы наткнуться и на NSQ, который тоже написан на Go. Смотрели ли вы в сторону NSQ и если да, то почему отказались от него?
    • –3
      NATS и NSQ выглядят похоже. Мы не глубоко изучали NSQ, но там вроде 2 процесса в каждом узле вместо одного (сложнее администрировать) + у NATS есть возможность добавить message persistence, если понадобится (надстройка NATS Streaming).
      • +2

        @Pyrus в NSQ 1 процесс nsqd, если нужен cluster, то два nsqd и nsqlookupd, второй выполняет роль service discovery.
        В NSQ гарантия доставки из коробки.


        @dos мы в одном из проектов выбрали NSQ, выбирали так же между NATS и NSQ. Выбор сделан пальцем в небо...


        В NATS подкупает Request-Reply если он необходим, хоть его и можно реализовать в NSQ.


        В NSQ так же можно организовать сообщение, канал и/или подписчика с выключенной гарантией доставки.

        • 0
          Спасибо. Нам нужен кластер, из общих соображений решили, что N процессов могут сбойнуть с меньшей вероятностью, чем 2N.

          NATS — at most once delivery.
          NSQ — at least once delivery.

          В случае at least once нужно самому бороться с повторными доставками.

          В NATS понравилась фича принудительное отключение «slow consumer» ради защиты кластера. Это и отсутствие гарантии доставки мотивирует писать надежных consumers.
          • +1

            NSQ имеет встроенный механизм повторной отправки сообщения. Так же можно самостоятельно переотправить сообщение в очередь.

            • 0
              «бороться с повторными доставками» — я имел в виду адресат должен самостоятельно отслеживать и (чаще всего) игнорировать, когда ему доставили сообщение повторно.
      • 0

        А вы попробовали? А то у вас же написано...


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

        При том что зукипер практически не требует администрирования.

  • +2
    Тоже использую NATS, т.к. потеря данных при доставке это событие с малой вероятностью, то восстановление доставки возложил на сторонний сервис который анализирует журнал работы, и изучает ключевые точки, если сообщение не прошло какую то из-точек, то происходит переотправка сообщения на этот этап.

    Пара ссылок для сравнения брокеров: раз, два
  • +3
    За табличку спасибо, но вот в упор не вижу, где там
    Видно, что запись на диск в 15 раз медленнее отправки по сети.
    • –2
      Сравниваем latency:

      Send 1K bytes over 1 Gbps network 10,000 ns
      Read 4K randomly from SSD* 150,000 ns

      Тут, конечно, указана цифра для чтения с SSD, но запись вряд ли будет быстрее.
    • 0
      Спасибо за замечание. Согласен, сформулировано не вполне корректно.

      Есть 3 главных показателя скорости внешнего носителя: throughput, latency и IOPS. Когда передаешь много маленьких сообщений, latency выходит на первый план по степени влияния на общую производительность системы.

      Из таблички: latency передачи пакета данных по сети: 0.01мс, что позволяет (теоретический предел) передать 100 000 сообщений в секунду.

      У SSD latency 0.15мс, и теоретический предел будет 1c / 0.15мс = 6 700 сообщений в секунду. У HDD latency выше и к-во сообщений в секунду будет еще меньше.

      Конечно, на фактическую производительность влияет много факторов. Например, если сообщений в MQ мало, все они большого размера и передаются не на сервер в соседней стойке, а на другой континент — то throughput может оказаться важнее.

      Вот тут есть сравнение latency для разных MQ:

      http://bravenewgeek.com/benchmarking-message-queue-latency/

      Из интересных наблюдений:

      — у NATS и Redis лучшие значения показателя latency (стабильно меньше 1мс), но он растет при увеличении размера сообщения

      — у RabbitMQ и Kafka показатель latency начинает быстро расти в районе 99,0-99,9 перцентилей даже при маленьких сообщениях

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


    Совершенно неверно. Пока сообщение не будет сохранено на диск, отправитель не получит об этом информацию, соответственно не зафиксирует со своей стороны что сообщение ушло. Получатель может использовать manual acknowledge mode, и сообщать что менеджеру очередей, что сообщение получил после того как зафиксирует у себя. И, в крайнем случае, используются распределенные транзакции.

    Гарантия есть, но надо уметь её готовить.
    • 0
      Опередили, хотел написать тоже самое. Могу только добавить что в большинстве случаев отправитель который работает с acknowledge — СУБД которая надёжно сохраняет состояние и вся система становится с гарантированной доставкой. Т.е. нужен надёжный источник, иначе ни при какой конфигурации система не может считаться надёжной.
  • 0
    А рассматривали ли вы вариант NanoMsg? Они вроде позиционируют себя как серьезное переосмысление ZeroMQ.
    • +1
      Даже использовали, нареканий нет. Субъективно, API немного проще 0MQ, производительность сравнимая с 0MQ. Но надо самим писать логику service discovery. Да и автор nanomsg перестал поддерживать проект. В github есть коммиты сообщества, конечно, но последние 3 мес (с января 2017) ни одного. Решили не рисковать.
  • 0
    «Ведь если упадет отправитель в момент передачи сообщения или упадет сам процесс очереди до записи на диск, то сообщение пропадет. Получается, очередь только создает иллюзию гарантии доставки, а сообщения по-прежнему могут теряться.»
    Не совсем так.
    В случае записи на диск после восстановления можно обнаружить, то некоторое сообщение пропало и откатить или повторить операцию, которая его породила. То есть запись на диск — часть транзакции, а передачу по сети транзакционной сделать сложнее.
  • +1
    Вы отмели RabbitMQ как продукт из-за измерений 2012 года? Сейчас ситуация кардинально поменялась, как минимум в мажорной версии продукта, а еще не правда то что RMQ сложен в конфигурировании. Очень важно как вы работаете с очередями, а не то как сконфигурирован HA Mirror Queueing.
  • +5

    Категорически не согласен со статьей. Нам тоже нужна была высокопроизводительная durable очередь (от 3 тыс сообщений в секунду) и вот мои выводы:


    Про гарантию доставки и in-memory queues.


    Этого недостаточно. Микросервисная архитектура для раскрытия всего своего потенциала требует сохранения работоспособности системы при отказе любой машины. Это позволяет хоститься на облаках с не очень надежными машинами, позволяет CD без даунтайма, позволяет A/B тестирование и т.д.


    Теперь рассмотрим REST сервис, который должен гарантировать доставку. Классический протокол выглядит так:


    • клиент посылает запрос
    • сервер обрабатывает запрос, сохраняет и коммитит результат, и только потом возвращает 200 OK.
    • если сервер вернул не 200 или не вернул ответ, клиент перепосылает запрос. В этом случае у нас будут дубликаты (решается наличием уникального id запроса и базы этих id на стороне сервера), но запрос никогда не будет потерян

    Если мы используем in-memory очереди, то возникает две проблемы:


    1. Если в любой момент времени сообщение хранится в памяти только одной машины, то при отказе этой машины мы теряем сообщение
    2. если мы используем кластер in-memory, то отказ одной машины проблемой не станет, НО усложняется деплоймент собственно кластера очереди сообщений (надо переводить ноду в readonly, дождаться окончания репликации, и только потом делать рестарт) И что делать, если очередь переполнится? Места на диске, как правило, значительно больше, чем в памяти.

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


    Про RabbitMQ.


    RabbitMQ тянет несколько тысяч запросов в секунду с одной ноды и работает хорошо, НО если объединить ноды в кластер, то мы наблюдали очень нестабильную работу под нагрузкой — кластер "разваливается" и не чинится автоматически. Также при падении одной из нод сама она в кластер не возвращается.


    Про Kafka.


    Стандартный кластер из трех нод на aws m3 тянет 20000 сообщений на запись или на чтение. Нормально переживает падение и возврат в кластер своих нод. Позволяет делать выбор между быстродействием и прочими свойствами очередей — гарантированным порядком сообщений, наличием повторов при отказе получателя и т.п.


    В целом ТС как-то странно понимает гарантию доставки.

    • 0
      если сервер вернул не 200 или не вернул ответ, клиент перепосылает запрос. В этом случае у нас будут дубликаты (решается наличием уникального id запроса и базы этих id на стороне сервера)

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

      Наверное, это явно не следовало из текста, стоит сказать, что durability не являлось критичным свойством для выбора очереди. И в итоге мы выбрали решение, у которого этого свойства нет.
  • 0
    Раз уже нагрузка на Devops для вас существенный фактор, почему не рассматриваете SaaS очереди, например эту: https://www.iron.io/platform/ironmq/
    • +1
      Предполагаем, что latency будет высокой — миллисекунды или десятки миллисекунд.

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

      Если есть реальный опыт с ней, было бы интересно почитать впечатления.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.