Хабрахабр

[Перевод] RabbitMQ против Kafka: отказоустойчивость и высокая доступность в кластерах

Данная статья о RabbitMQ, а следующая — о Kafka, в сравнении с RabbitMQ. Отказоустойчивость и высокая доступность — большие темы, так что посвятим RabbitMQ и Kafka отдельные статьи. Статья длинная, так что устраивайтесь поудобнее.

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

Сбой сетевого соединения, сбой сервера, сбой жесткого диска, временная недоступность сервера из-за сборки мусора, потеря пакетов или замедление сетевого соединения. Эти понятия описывают, как система ведет себя при сбое. Оказывается, практически невозможно поднять систему, одновременно и полностью непротиворечивую (без потери данных, без расхождения данных), и доступную (будет принимать операции чтения и записи) для всех вариантов сбоя.
Мы увидим, что согласованность и доступность находятся на разных концах спектра, и вам нужно выбрать, в какую сторону оптимизировать. Все это может привести к потере данных или конфликтам. У вас эдакие «нердовские» рычажки, чтобы сдвигать баланс в сторону большей согласованности или большей доступности. Хорошая новость в том, что с RabbitMQ такой выбор возможен.

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

Устойчивые очереди/маршрутизация

В RabbitMQ два типа очереди: длительные/устойчивые (durable) и неустойчивые (non-durable). Все очереди сохраняются в базе данных Mnesia. Устойчивые очереди повторно объявляются при запуске узла и, таким образом, переживают перезапуск, сбой системы или сбой сервера (до тех пор, пока сохраняются данные). Это означает, что пока вы декларируете маршрутизацию (exchange) и очередь устойчивыми, инфраструктура очередей/маршрутизации вернется в оперативный режим.

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

Устойчивые сообщения

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

1.
Рис. Матрица устойчивости

Чтобы пережить потерю брокера, нам нужна избыточность. Можем объединить несколько узлов RabbitMQ в кластер, а затем добавить дополнительную избыточность путём репликации очередей между несколькими узлами. Таким образом, если падает один узел, мы не теряем данные и остаёмся доступными.

Зеркалирование очереди:

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


Рис. 2. Зеркалирование очереди

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

  • ha-mode: all
  • ha-mode: exactly, ha-params: 2 (один мастер и одно зеркало)
  • ha-mode: nodes, ha-params: rabbit@node1, rabbit@node2

Для достижения последовательной записи необходимы подтверждения паблишеру (Publisher Confirms). Без них есть вероятность потери сообщений. Подтверждение отправляется паблишеру после записи сообщения на диск. RabbitMQ записывает сообщения на диск не при получении, а на периодической основе, в районе нескольких сотен миллисекунд. Когда очередь зеркалируется, подтверждение отправляется только после того, как все зеркала также записали свою копию сообщения на диск. Это означает, что использование подтверждений добавляет задержку, но если безопасность данных важна, то они необходимы.
Когда брокер завершает работу или падает, все ведущие очереди (мастера) на этом узле отваливаются вместе с ним. Затем кластер выбирает самое старое зеркало каждого мастера и продвигает его в качестве нового мастера.

3.
Рис. Несколько зеркалированных очередей и их политики

Обратите внимание, что зеркало Очереди С на Брокере 2 повышается до мастера. Брокер 3 падает. RabbitMQ всегда пытается поддерживать коэффициент репликации, указанный в ваших политиках. Также обратите внимание, что для Очереди C создано новое зеркало на Брокере 1.

4.
Рис. Брокер 3 отваливается, что вызывает отказ очереди C

У нас остался только один брокер. Падает следующий Брокер 1! До мастера повышается зеркало Очереди B.

5
Рис.

Независимо от того, насколько успешно данные пережили потерю и восстановление брокера, все зеркалированные сообщения очереди отбрасываются при перезапуске. Мы вернули Брокера 1. Мы скоро рассмотрим эти последствия. Это важно отметить, поскольку будут последствия. Таким образом, Брокер 1 теперь снова является членом кластера, а кластер пытается соблюдать политики и поэтому создает зеркала на Брокере 1.

В этом случае потеря Брокера 1 была полной, как и данных, поэтому незеркалированная Очередь В потеряна полностью.

6.
Рис. Брокер 1 возвращается в строй

Но теперь все главные очереди на одном узле! Брокер 3 вернулся в строй, так что очереди A и B получают обратно созданные на нём зеркала, чтобы удовлетворить своим политикам HA. К сожалению, здесь нет особых вариантов для перебалансировки мастеров. Это не идеально, лучше равномерное распределение между узлами. Вернемся к этой проблеме позже, так как сначала нужно рассмотреть синхронизацию очереди.

7.
Рис. Все главные очереди на одном узле! Брокер 3 возвращается в строй.

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

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

Рассмотрим пример. Такая синхронизация выполняется автоматически или вручную и управляется с помощью политики очередей.

Очередь A синхронизируется автоматически, а Очередь B — вручную. У нас две зеркалированные очереди. В обеих очередях по десять сообщений.

8.
Рис. Две очереди с разными режимами синхронизации

Теперь мы теряем Брокера 3.

9.
Рис. Брокер 3 упал

Кластер создает зеркало для каждой очереди на новом узле и автоматически синхронизирует новую Очередь А с мастером. Брокер 3 возвращается в строй. Таким образом, у нас полная избыточность Очереди A и только одно зеркало для существующих сообщений Очереди B. Однако зеркало новой Очереди В остается пустым.

10.
Рис. Новое зеркало Очереди А получает все существующие сообщения, а новое зеркало Очереди B — нет

Затем Брокер 2 падает, а Очередь А откатывается к самому старому зеркалу, которое находится на Брокере 1. В обе очереди поступает ещё по десять сообщений. В Очереди B двадцать сообщений в мастере и только десять в зеркале, поскольку эта очередь никогда не реплицировала исходные десять сообщений. При сбое не происходит потери данных.

11.
Рис. Очередь А откатывается Брокера 1 без потери сообщений

Теперь падает Брокер 1. В обе очереди поступает еще по десять сообщений. Однако у Очереди В возникают проблемы. Очередь A без проблем переключается на зеркало без потери сообщений. На этом этапе мы можем оптимизировать либо доступность, либо согласованность.

Это значение по умолчанию, поэтому можно просто не указывать политику вообще. Если мы хотим оптимизировать доступность, то политику ha-promote-on-failure следует установить в always. Это приведет к потере сообщений, но очередь остается доступной для чтения и записи. В таком случае, по сути, мы допускаем сбои в несинхронизированных зеркалах.

12.
Рис. Очередь B откатывается на Брокера 3 с потерей десяти сообщений Очередь А откатывается на Брокера 3 без потери сообщений.

В этом случае вместо отката на зеркало очередь будет дожидаться, пока Брокер 1 со своими данными вернётся в оперативный режим. Мы также можем установить ha-promote-on-failure в значение when-synced. Доступность приносится в жертву безопасности данных. После его возвращения главная очередь снова оказывается на Брокере 1 без потери данных. Но это рискованный режим, который может привести даже к полной потере данных, что мы рассмотрим в ближайшее время.

13.
Рис. Очередь B остается недоступной после потери Брокера 1

Ответ заключается в том, что синхронизация является блокирующей операцией. Вы можете задать вопрос: «Может, лучше никогда не использовать автоматическую синхронизацию?». Во время синхронизации главная очередь не может выполнять никаких операций чтения или записи!

Сейчас у нас очень большие очереди. Рассмотрим пример. По нескольким причинам: Как они могут вырасти до такого размера?

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


Рис. 14. Две большие очереди с разными режимами синхронизации

Теперь падает Брокер 3.

15.
Рис. Брокер 3 падает, оставляя по одному мастеру и зеркалу в каждой очереди

Главная Очередь А начинает реплицировать существующие сообщения на новое зеркало, и в течение этого времени Очередь недоступна. Брокер 3 возвращается в строй, и создаются новые зеркала. Для репликации данных требуется два часа, что приводит к двум часам простоя для этой Очереди!

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

16.
Рис. Очередь остается недоступной во время синхронизации

Через два часа Очередь A тоже становится доступной и может снова начать принимать операции чтения и записи.

Обновления

Такое блокирующее поведение во время синхронизации затрудняет обновление кластеров с очень большими очередями. В какой-то момент узел с мастером нужно перезапустить, что означает либо переход на зеркало, либо отключение очереди во время обновления сервера. Если мы выберем переход, то потеряем сообщения, если зеркала не синхронизированы. По умолчанию во время отключения брокера переход на несинхронизированное зеркало не выполняется. Это означает, что как только брокер возвращается, мы не теряем никаких сообщений, единственным ущербом стал только простой очереди. Правила поведения при отключении брокера задаются политикой ha-promote-on-shutdown. Можно установить одно из двух значений:

  • always= включен переход на несинхронизированные зеркала
  • when-synced= переход только на синхронизированное зеркало, иначе очередь становится недоступной для чтения и записи. Очередь возвращается в строй, как только вернется брокер

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

Когда доступность повышает безопасность данных

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

Здесь нужно учесть следующее:

  • Может ли паблишер просто вернуть ошибку, а вышестоящая служба или пользователь позже повторят попытку?
  • Может ли паблишер сохранить сообщение локально или в базе данных, чтобы повторить попытку позже?

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

Таким образом, нужно искать баланс, а решение зависит от конкретной ситуации.

Идея ha-promote-on-failure= when-synced заключается в том, что мы предотвращаем переключение на несинхронизированное зеркало и тем самым избегаем потери данных. Очередь остается недоступной для чтения или записи. Вместо этого мы пытаемся вернуть упавший брокер с неповрежденными данными, чтобы он возобновил работу в качестве мастера без потери данных.

Все данные пропали! Но (и это большое но) если брокер потерял свои данные, то у нас большая проблема: очередь потеряна! Даже если у вас есть зеркала, которые в основном догоняют главную очередь, эти зеркала тоже отбрасываются.

Пока кластер помнит потерянный узел, он помнит старую очередь и несинхронизированные зеркала. Чтобы заново добавить узел с тем же именем, мы говорим кластеру забыть потерянный узел (командой rabbitmqctl forget_cluster_node) и запустить новый брокер с тем же именем хоста. Теперь нужно заново его объявить. Когда кластеру говорят забыть потерянный узел, эта очередь также забывается. Лучше было бы перейти на несинхронизированное зеркало! Мы потеряли все данные, хотя у нас были зеркала с частичным набором данных.

Документы говорят, что такой вариант существует для безопасности данных, но это обоюдоострый нож. Поэтому ручная синхронизация (и невыполнение синхронизации) в сочетании с ha-promote-on-failure=when-synced, на мой взгляд, довольно рискованна.

Как и было обещано, возвращаемся к проблеме скопления всех мастеров на одном или нескольких узлах. Это может произойти даже в результате «скользящего» (rolling) обновления кластера. В кластере с тремя узлами все главные очереди скопятся на одном или двух узлах.

Перебалансировка мастеров может оказаться проблематичной по двум причинам:

  • Нет хороших инструментов для выполнения перебалансировки
  • Синхронизация очередей

Для перебалансировки есть сторонний плагин, который не поддерживается официально. Относительно сторонних плагинов в руководстве RabbitMQ сказано: «Плагин предоставляет некоторые дополнительные инструменты настройки и отчётности, но не поддерживается и не проверен командой RabbitMQ. Используйте на свой страх и риск».

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

  • Удаляет все зеркала с помощью временной политики с более высоким приоритетом, чем существующая политика HA.
  • Изменяет временную политику HA для использования режима «узлы» с указанием узла, на который требуется перенести главную очередь.
  • Синхронизирует очередь для принудительной миграции.
  • После завершения миграции удаляет временную политику. Вступает в действие исходная политика HA и создается нужное количество зеркал.

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

Теперь посмотрим, как кластеры RabbitMQ работают с сетевыми разделами.

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

С RabbitMQ у нас две основных опции:

  • Разрешить логическое разделение (split-brain). Это обеспечивает доступность, но может спровоцировать потерю данных.
  • Запретить логическое разделение. Может привести к краткосрочной потере доступности в зависимости от способа подключения клиентов к кластеру. Также может привести к полной недоступности в кластере из двух узлов.

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

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

Если паблишеры отправляют данные в оба мастера, у нас получится две расходящиеся копии очереди.

Различные режимы RabbitMQ обеспечивают либо доступность, либо согласованность.

Режим Ignore (по умолчанию)

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

18.
Рис. Внутренне кластер направляет все запросы в главную очередь на Брокере 2. Три паблишера связаны с тремя брокерами.

Он видит, что другие брокеры отвалились, и продвигает свое зеркало до мастера. Теперь мы теряем Брокера 3. Так происходит логическое разделение.

19.
Рис. Записи идут в две главные очереди, и две копии расходятся. Логическое разделение (split-brain).

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

20.
Рис. Администратор отключает Брокера 3.

21.
Рис. Администратор запускает Брокера 3, и он присоединяется к кластеру, теряя все сообщения, которые там оставались.

Во время потери связности и после её восстановления кластер и эта очередь были доступны для чтения и записи.

Режим Autoheal

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

Режим Pause Minority

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

22.
Рис. Внутренне кластер направляет все запросы в главную очередь на Брокере 2. Три паблишера связаны с тремя брокерами.

Вместо того, чтобы повышать свое зеркало до мастера, Брокер 3 приостанавливает работу и становится недоступным. Затем Брокеры 1 и 2 отделяются от Брокера 3.

23.
Рис. Брокер 3 приостанавливает работу, отключает всех клиентов и отвергает запросы на подключение.

Как только связность восстановлена, он возвращается в кластер.

Посмотрим на другой пример, где главная очередь находится на Брокере 3.

24.
Рис. Главная очередь на Брокере 3.

Брокер 3 встаёт на паузу, поскольку находится на меньшей стороне. Затем происходит та же потеря связности. На другой стороне узлы видят, что Брокер 3 отвалился, так что более старое зеркало с Брокеров 1 и 2 повышается до мастера.

25.
Рис. Переход к Брокеру 2 при недоступности Брокера 3.

Когда связность восстановлена, Брокер 3 присоединится к кластеру.

26.
Рис. Кластер вернулся к нормальной работе.

Для большинства ситуаций лично я бы выбрал режим Pause Minority, но это реально зависит от конкретного случая. Здесь важно понимать, что мы получаем согласованность, но также можем получить доступность, если успешно переведем клиентов на бóльшую часть раздела.

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

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

Наши варианты:

  • Доступ к кластеру осуществляется с помощью балансировщика нагрузки, который просто циклически перебирает узлы, а клиенты выполняют повторные попытки подключения до успешного завершения. Если узел не работает или приостановлен, то попытки подключения к этому узлу завершатся неудачей, но последующие попытки пойдут на другие серверы (в циклическом режиме). Это подходит для кратковременной потери связности или упавшего сервера, который будет быстро поднят.
  • Доступ к кластеру через балансировщик нагрузки и удаление приостановленных/упавших узлов из списка, как только они обнаружены. Если быстро это сделать, и если клиенты способны на повторные попытки подключения, то мы получим постоянную доступность.
  • Дать каждому клиенту список всех узлов, а клиент при подключении случайным образом выбирает один из них. Если при попытке подключения он получает ошибку, то переходит к следующему узлу в списке, пока не подключится.
  • Убрать трафик от упавшего/приостановленного узла с помощью DNS. Это делается при помощи малого TTL.

У кластеризации RabbitMQ свои преимущества и недостатки. Наиболее серьезные недостатки заключаются в том, что:

  • при присоединении к кластеру узлы отбрасывают свои данные;
  • блокирующая синхронизация приводит к недоступности очереди.

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

  • Ненадёжная сеть.
  • Ненадёжное хранение.
  • Очень большие очереди.

Что касается настроек для высокой доступности, то рассмотрите такие:

  • ha-promote-on-failure=always
  • ha-sync-mode=manual
  • cluster_partition_handling=ignore (или autoheal)
  • устойчивые сообщения
  • убедитесь, что клиенты подключаются к активному узлу, когда какой-то узел выходит из строя

Для согласованности (безопасности данных) рассмотрите следующие настройки:

  • Publisher Confirms и Manual Acknowledgements на стороне потребителя
  • ha-promote-on-failure=when-synced, если издатели могут повторить попытку позже и если у вас есть очень надежное хранилище! Иначе ставьте =always.
  • ha-sync-mode=automatic (но для больших неактивных очередей может потребоваться ручной режим; кроме того, подумайте, не приведет ли недоступность к потере сообщений)
  • режим Pause Minority
  • устойчивые сообщения

Мы рассмотрели еще не все вопросы отказоустойчивости и высокой доступности; например, как безопасно выполнять административные процедуры (такие, как скользящие обновления). Нужно поговорить также о федерировании и плагине Shovel.

Если я еще что-то упустил, пожалуйста, дайте знать.

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

Предыдущие статьи серии:
№1 — habr.com/ru/company/itsumma/blog/416629
№2 — habr.com/ru/company/itsumma/blog/418389
№3 — habr.com/ru/company/itsumma/blog/437446

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть