RabbitMQ может быть настроен для много-нодового кластера (с репликацией очередей с мастера на slave-ноды).
Отказоустойчивость может быть обеспечена добавлением round-robin лоадбалансера(например, HAProxy) впереди RabbitMQ-кластера
Для настройка High Availability необходимо настроить Cluster, а затем – репликацию/зеркалирование очередей сообщений т.к. при настройке кластера реплицируются на ноды кластера все необходимые для работы RabbitMQ данные, за исключением очередей сообщений.
Из официальной документации
1 2 3 4 5 6 7 |
A RabbitMQ broker is a logical grouping of one or several Erlang nodes, each running the RabbitMQ application and sharing users, virtual hosts, queues, exchanges, bindings, and runtime parameters. Sometimes we refer to the collection of nodes as a cluster. What is Replicated? All data/state required for the operation of a RabbitMQ broker is replicated across all nodes. An exception to this are message queues, which by default reside on one node, though they are visible and reachable from all nodes. To replicate queues across nodes in a cluster, see the documentation on high availability (note that you will need a working cluster first). By default, contents of a queue within a RabbitMQ cluster are located on a single node (the node on which the queue was declared). This is in contrast to exchanges and bindings, which can always be considered to be on all nodes. Queues can optionally be made mirrored across multiple nodes. Each mirrored queue consists of one master and one or more mirrors. The master is hosted on one node commonly referred as the master node. Each queue has its own master node. All operations for a given queue are first applied on the queue's master node and then propagated to mirrors. This involves enqueueing publishes, delivering messages to consumers, tracking acknowledgements from consumers and so on. Messages published to the queue are replicated to all mirrors. Consumers are connected to the master regardless of which node they connect to, with mirrors dropping messages that have been acknowledged at the master. Queue mirroring therefore enhances availability, but does not distribute load across nodes (all participating nodes each do all the work). If the node that hosts queue master fails, the oldest mirror will be promoted to the new master as long as it synchronised |
RabbitMQ-клиенты поддерживают подключение к нескольким нодам кластера(большинство клиентских библиотек позволяет задать несколько нод для подключения), но в конкретный текущий момент времени клиент может подключиться только к одной ноде из кластера. Если эта нода т.н. мастер для этой очереди(т.е. на этой ноде была создана эта очередь и реплицирована на все остальные ноды кластера), то именно эта нода и обработает запрос. Но, если клиент подключается к НЕ мастер ноде, то внутренним механизмом/транспортом RabbitMQ перенаправит подключение клиента к мастер ноде для этой очереди. Если, например, мастер нода не доступна, то клиент переподключается к одной из оставшихся нод в кластере(одна из slave-нод будет выбрана в качестве нового мастера и клиент будет перенаправлен на этот новый мастер в случае, если клиент переподключился к другим нодам кластера, после выхода со строя старой мастер ноды, на котором создавалась очередь)
В данном случае используется HAproxy в качестве балансировщика загрузки.
В коде приложения устанавливается IP-адрес HAProxy-сервера, который в свою очередь перенаправляет запросы на 2 RabbitMQ-сервера, находящихся в кластере и реплицирующих очереди сообщений между собой.
HAProxy мониторит состояние RabbitMQ-серверов путем проверки доступности порта RabbitMQ-службы и выбрасывает ноду из своего(не RabbitMQ) кластера( т.е. не перенаправляет на ноду запросы клиентов)
Т.е. цепочка прохождения запроса от клиента к RabbitMQ-мастер-серверу выглядит так
1 |
Client->HAproxy->RabbitMQ-master |
(если HAproxy направил клиентский запрос на мастер для этой очереди)
Либо так
1 |
Client->HAProxy->RabbitMQ-slave->RabbitMQ-master |
(если HAproxy направил клиентский запрос на слейв для этой очереди)
Если одна из двух нод кластера неисправна, то HAproxy выбрасывает ее из кластера и соответственно оставшаяся нода является мастером и путь прохождения запроса клиента выглядит так
1 |
Client->HAproxy->RabbitMQ-master |
Настройка RabbitMQ кластера из двух нод
1 2 |
app01.mydomain.com - 192.168.100.1 app02.mydomain.com - 192.168.100.2 |
RabbitMQ-ноды должны распознавать корректно друг друга по полным и коротким доменным именам. По умолчанию RabbitMQ использует короткие имена хостов(hostname -s)Это также справедливо при использовании инструмента rabbitmqctl
По умолчанию RabbitMQ имена каталогов в своей базе в /var/lib/rabbitmq/ создает по короткому имени хоста(hostname -s). Если имя сервера изменяется,то RabbitMQ создает новую базу с коротким именем сервера. Поэтому при изменении имени хоста не обходимо перезапустить RabbitMQ службу
app01
1 |
# grep -i app02 /etc/hosts |
1 |
192.168.100.2 app02.mydomain.com app02 |
app02
1 |
# grep -i app01 /etc/hosts |
1 |
192.168.100.1 app01.mydomain.com app01 |
Кластер может быть создан разными способами
1 2 3 |
• Manually with rabbitmqctl (e.g. in development environments) • Declaratively by listing cluster nodes in config file • Declaratively with rabbitmq-autocluster (a plugin) |
Rabbitmq-ноды и CLI-инструменты(например, rabbitmqctl) используют Erlang cookie-файл для аутентификации между собой
Каждый нода кластера должна иметь такой файл с одинаковым содержимым на всех нодах кластера
Содержимое файла /var/lib/rabbitmq/.erlang.cookie на app02 должно совпадать с содержимым файла на app01
Права на файл 400, владелец/группа – rabbitmq:rabbitmq
app01
1 |
# cat /var/lib/rabbitmq/.erlang.cookie |
1 |
RUXMOHIBWOGLDYCKKXOR |
app02
1 |
# cat /var/lib/rabbitmq/.erlang.cookie |
1 |
RUXMOHIBWOGLDYCKKXOR |
1 |
# ls -l /var/lib/rabbitmq/.erlang.cookie |
1 |
-r-------- 1 rabbitmq rabbitmq 20 Oct 1 2017 /var/lib/rabbitmq/.erlang.cookie |
Т.е. если на app02 такого файла нет,то копируем его с app01 и выставляем корректные права/владельца/группу
1 |
# chmod 400 /var/lib/rabbitmq/.erlang.cookie && chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie |
Запуск RabbitMQ-сервера в обычном режиме на обоих нодах
1 |
# systemctl start rabbitmq-server |
Либо
1 |
# rabbitmq-server -detached |
Это создает на каждой RabbitMQ ноде по НЕЗАВИСИМОМУ брокеру(broker)/кластеру
app01
1 |
# rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app01 [{nodes,[{disc,[rabbit@app01]}]}, {running_nodes,[rabbit@app01]}, {cluster_name,<<"rabbit@app01.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app01,[]}]}] |
app02
1 |
# rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app02 [{nodes,[{disc,[rabbit@app02]}]}, {running_nodes,[rabbit@app02]}, {cluster_name,<<"rabbit@app02.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app02,[]}]}] |
Для того, чтобы реплицировать очереди необходимо создать политику, которая будет описывать режим и тип репликации. Политики могут создаваться в любое время. Политики могут примняться ко всем очередям или к выборочным очередям (с фильтрацией имени очереди по шаблону регулярного выражения)
Можно создавать не реплицируемые очереди, а потом позже делать их реплицируемыми через создание политики
Настроим политику по синхронизации очередей сообщений между нодами.
На app01 выполняем
1 |
# rabbitmqctl set_policy ha-all "" '{"ha-mode":"all","ha-sync-mode":"automatic"}' |
1 |
Setting policy "ha-all" for pattern [] to "{\"ha-mode\":\"all\",\"ha-sync-mode\":\"automatic\"}" with priority "0" |
1 |
ha-all – имя политики(может быть произвольным) |
1 |
"" – нет фильтрации по имени/паттерну очереди, на которую необходимо применить эту политику( т.е. политика буде применять на все очереди. Здесь можно указывать шаблоны по имени очереди, для которых нужно применять политику) |
1 |
"ha-mode":"all" режим репликации очередей all – очереди реплицируются на все ноды в кластере |
1 |
"ha-sync-mode":"automatic" - тип репликации очередей – очереди будут автоматически синхронизированы при добавлении новой ноды |
Проверка действующих политик
1 |
# rabbitmqctl list_policies |
1 2 |
Listing policies / ha-all all {"ha-mode":"all","ha-sync-mode":"automatic"} 0 |
Для того, чтобы создать кластер из двух нод мы присоединим, например, вторую ноду(app02) rabbit@app02 к первой ноде(app01) rabbit@app01
Для этого на второй ноде
Остнавливаем RabbitMQ-приложение
1 |
# rabbitmqctl stop_app |
1 |
Stopping node rabbit@app02 ...done. |
Присоединяем к кластеру
1 |
# rabbitmqctl join_cluster rabbit@app01 |
1 |
Clustering node rabbit@app02 with rabbit@app01 |
Запускаем RabbitMQ-приложение
1 |
# rabbitmqctl start_app |
1 |
Starting node rabbit@app02 |
Следует заметить, что при присоединении ноды к кластеру, все ресурсы и данные, хранящиеся на этой ноде, будут удалены
Проверяем состояние кластера на обеих нодах
1 |
root@app01 ~ # rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app01 [{nodes,[{disc,[rabbit@app01,rabbit@app02]}]}, {running_nodes,[rabbit@app02,rabbit@app01]}, {cluster_name,<<"rabbit@app01.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app02,[]},{rabbit@app01,[]}]}] |
1 |
root@app02 ~ # rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app02 [{nodes,[{disc,[rabbit@app01,rabbit@app02]}]}, {running_nodes,[rabbit@app01,rabbit@app02]}, {cluster_name,<<"rabbit@app01.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app01,[]},{rabbit@app02,[]}]}] |
В последующем можно добавлять новые ноды в кластер в любое время, пока кластер запущен
Остановим RabbitMQ на второй ноде для прверки репликации
1 |
# rabbitmqctl stop |
1 |
Stopping and halting node rabbit@app02 |
Проверим состояние кластера на app01
1 |
root@app01 ~ # rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app01 [{nodes,[{disc,[rabbit@app01,rabbit@app02]}]}, {running_nodes,[rabbit@app01]}, {cluster_name,<<"rabbit@app01.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app01,[]}]}] |
На app01 добавляем пользователя, вирт.хост, определяем права для пользователя на этот вирт.хост
1 |
# rabbitmqctl add_user demo demopassword |
1 |
# rabbitmqctl set_user_tags demo Admin |
1 |
# rabbitmqctl add_vhost demovhost |
1 |
# rabbitmqctl set_permissions -p demovhost demo ".*" ".*" ".*" |
1 |
root@app01 ~ # rabbitmqctl list_users | grep demo |
1 |
demo [Admin] |
1 |
root@app01 ~ # rabbitmqctl list_vhosts | grep demo |
1 |
demovhost |
1 |
root@app01 ~ # rabbitmqctl list_user_permissions demo |
1 2 |
Listing permissions for user "demo" demovhost .* .* .* |
Запускаем RabbitMQ на второй ноде для проверки репликации
1 |
# rabbitmqctl start |
Либо
1 |
# systemctl start rabbitmq-server |
Проверем состояние кластера на обеих нодах
1 |
root@app02 ~ # rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app02 [{nodes,[{disc,[rabbit@app01,rabbit@app02]}]}, {running_nodes,[rabbit@app01,rabbit@app02]}, {cluster_name,<<"rabbit@app01.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app01,[]},{rabbit@app02,[]}]}] |
1 |
root@app01 ~ # rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app01 [{nodes,[{disc,[rabbit@app01,rabbit@app02]}]}, {running_nodes,[rabbit@app02,rabbit@app01]}, {cluster_name,<<"rabbit@app01.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app02,[]},{rabbit@app01,[]}]}] |
Проверяем на app02 наличие созданного на app01 пользователя, вирт.хоста и прав пользователя на этот хост
1 |
root@app02 ~ # rabbitmqctl list_users | grep demo |
1 |
demo [Admin] |
1 |
root@app02 ~ # rabbitmqctl list_vhosts | grep demo |
1 |
demovhost |
1 |
root@app02 ~ # rabbitmqctl list_user_permissions demo |
1 2 |
Listing permissions for user "demo" demovhost .* .* .* |
т.е. репликация работает корректно
Ноды, присоединенные в кластер в любое время могут быть отсоединены с кластера или по каким-то причинам выключены/разломаны и т.д.
Все остальные ноды кластера продолжают функционировать корректно и проблемная нода будет автоматически добавлена в кластер и синхронизирована с кластером после ее запуска/восстановления
Из официальной документации
1 2 3 4 5 6 7 8 9 10 11 12 |
It is important to understand the process node go though when they are stopped and restarted. A stopping node picks an online cluster member (only disc nodes will be considered) to sync with after restart. Upon restart the node will try to contact that peer 10 times by default, with 30 second response timeouts. In case the peer becomes available in that time interval, the node successfully starts, syncs what it needs from the peer and keeps going. When a node has no online peers during shutdown, it will start without attempts to sync with any known peers. It does not start as a standalone node, however, and peers will be able to rejoin it. When the entire cluster is brought down therefore, the last node to go down must be the first node to be brought online. However, since nodes will try to contact a known peer for up to 5 minutes (by default), nodes can be restarted in any order in that period of time. In this case they will rejoin each other one by one successfully. This window of time can be adjusted using two configuration settings: # wait for 60 seconds instead of 30 mnesia_table_loading_retry_timeout = 60000 # retry 15 times instead of 10 mnesia_table_loading_retry_limit = 15 By adjusting these settings and tweaking the time window in which known peer has to come back it is possible to account for cluster-wide redeployment scenarios that can be longer than 5 minutes to complete. In some cases the last node to go offline cannot be brought back up. It can be removed from the cluster using the forget_cluster_node rabbitmqctl command. Alternatively force_boot rabbitmqctl command can be used on a node to make it boot without trying to sync with any peers (as if they were last to shut down). This is usually only necessary if the last node to shut down or a set of nodes will never be brought back online. |
Выключение ноды с кластера
Например,выключим ноду rabbit@app02 с кластера ноды rabbit@app01
На app02 выполняем
1 |
# rabbitmqctl stop_app |
1 |
Stopping rabbit application on node rabbit@app02 |
1 |
# rabbitmqctl reset |
1 |
Resetting node rabbit@app02 |
1 |
# rabbitmqctl start_app |
1 |
Starting node rabbit@app02 |
Если при выключении с кластера нода app02 недоступна ,то ее можно выключить с кластера удаленно (выполняя команды на app01-ноде)
На app02-выполняем
1 |
# rabbitmqctl stop_app |
На app01-выполняем
1 |
# rabbitmqctl forget_cluster_node rabbit@app02 |
При этом app02-нода считает,что она все еще в кластере, поэтому ее запуск завершится с ошибкой
Нa app02 выполняем
1 |
# rabbitmqctl start_app |
Starting node rabbit@app02 …
1 |
Error: inconsistent_cluster: Node rabbit@app02 thinks it's clustered with node rabbit@app01, but rabbit@app01 disagrees |
Для запуска ноды app02 необходимо сбросить ее состояние
1 |
# rabbitmqctl reset |
1 |
Resetting node rabbit@app02 ...done. |
1 |
# rabbitmqctl start_app |
1 |
Starting node rabbit@app02 |
Т.е. в данный момент app02-нода работает как независимый RabbitMQ-брокер/кластер
1 |
root@app02 ~ # rabbitmqctl cluster_status |
1 2 3 4 5 6 |
Cluster status of node rabbit@app02 [{nodes,[{disc,[rabbit@app02]}]}, {running_nodes,[rabbit@app02]}, {cluster_name,<<"rabbit@app02.mydomain.com">>}, {partitions,[]}, {alarms,[{rabbit@app02,[]}]}] |
Настройка портов firewall
1 2 3 4 5 6 7 8 |
• 4369: epmd, a peer discovery service used by RabbitMQ nodes and CLI tools • 5672, 5671: used by AMQP 0-9-1 and 1.0 clients without and with TLS • 25672: used by Erlang distribution for inter-node and CLI tools communication and is allocated from a dynamic range (limited to a single port by default, computed as AMQP port + 20000). See networking guide for details. • 15672: HTTP API clients and rabbitmqadmin (only if the management plugin is enabled) • 61613, 61614: STOMP clients without and with TLS (only if the STOMP plugin is enabled) • 1883, 8883: (MQTT clients without and with TLS, if the MQTT plugin is enabled • 15674: STOMP-over-WebSockets clients (only if the Web STOMP plugin is enabled) • 15675: MQTT-over-WebSockets clients (only if the Web MQTT plugin is enabled) |
Для настройки кластера достаточно открыть взаимодействие между нодами по портам
4369, 25672 по протоколу TCP
Необходимо отметить,что для настройки кластера необходимо иметь одинаковые версии RabbitMQ-сервера и Erlang на всех нодах кластера.
Настройка HAproxy на поддержку RabbitMQ-кластера
1 |
# nano /etc/haproxy/haproxy.cfg |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
frontend rabbit_frontend mode tcp bind <IP-address-Haproxy-server>:5672 log global option tcplog option logasap option clitcpka timeout client 3h default_backend rabbit-backend backend rabbit-backend mode tcp balance roundrobin timeout server 3h server rabbit_app01 192.168.100.1:5672 check inter 5s rise 2 fall 3 server rabbit_app02 192.168.100.2:5672 check inter 5s rise 2 fall 3 |
1 |
# haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl reload haproxy |
Важно,установить занчение параметра timeout client в rabbitmq-секции frontend больше(дольше) чем значение параметра /proc/sys/net/ipv4/tcp_keepalive_time (по умолчанию 2 часа)
1 |
# cat /proc/sys/net/ipv4/tcp_keepalive_time |
1 |
7200 |
В глобальных настройках Haproxy, например, может быть установлен таймаут в 50 секунд
1 2 3 4 |
defaults … timeout client 50000 … |
Т.е по истечению 50 секунд соединение между клиентом и HAproxy-сервером в статусе «idle»(т.е. через него не передаются пакеты/данные) будет закрыто со стороны Haproxy
RabbitMQ-клиентам(потребителям) нужно иметь постоянное подключение к RabbitMQ-серверу,чтобы прослушивать очереди, забирать сообщения из очередей и т.д.
Решением может быть
ИЛИ
Переопределение дефолтных настроек(которые установлены на глобальном уровне) таймаута для Rabbitmq-фронтенд блока на размер таймаута, который превышает системный таймаут по поддержки tcp-соединеня путем посылки keep-alive пакетов(по умолчанию 2 часа)
1 |
# cat /proc/sys/net/ipv4/tcp_keepalive_time |
1 |
7200 |
ИЛИ
настройка RabbitMQ-клиентов на поддержку Heartbeat-запросов
Напрмер, установив таймаут Heartbeat-сообщений в 30 секунд(тем самым каждые 15 секунд будет отправляться heartbeat-запрос и,если 2 запроса выполнятся неуспешно, тогда(читай через 30 секунд) RabbitMQ-клиент посчитает, что соединение с сервером потеряно/разорвано и переподключится к другому серверу.
https://www.rabbitmq.com/heartbeats.html
Если оба указанных выше варианта не решают проблему закрытия «idle»-соединений, то необходимо установить как можно большие значения параметров timeout client (для frontend rabbitmq) и timeout server(для backend rabbitmq)
Полезные команды rabbitmqctl
Просмотр списка очередей
1 |
# rabbitmqctl list_queues |
Просмотр списка очередей с выводом имен политик,которые применены к этим очередям
1 |
# rabbitmqctl list_queues name policy pid slave_pid |
Ручная синхронизация очереди
1 |
# rabbitmqctl sync_queue <имя очереди> |
Отмена синхронизации очереди
1 |
# rabbitmqctl cancel_sync_queue <имя очереди> |
Проверка состояния RabbitMQ ноды
1 |
# rabbitmqctl node_health_check |
1 2 3 |
Timeout: 70.0 seconds Checking health of node rabbit@app01 Health check passed |
Просмотр статуса RabbitMQ-ноды
1 |
# rabbitmqctl status |
Полный отчет(включая состояние кластера, нод, политик, параметров, пользователей, вирт.хостов и т.д.)
1 |
# rabbitmqctl report | less |
Больше команд доступно по
1 |
# rabbitmqctl --help |
Настрйока Nginx для проксирования запросов на RabbitMQ WEB managment interface
1 |
# nano /etc/nginx/sites-enabled/default.conf |
1 2 3 4 5 6 |
location /rabbitmq-myapp/ { if ($request_uri ~* "/rabbitmq-myapp/(.*)") { proxy_pass http://localhost:15672/$1; } proxy_pass http://localhost:15672; } |
Источник:
https://www.rabbitmq.com/clustering.html
https://www.rabbitmq.com/ha.html
https://www.rabbitmq.com/heartbeats.html
https://www.rabbitmq.com/parameters.html#policies
http://roboconf.net/en/user-guide/clustered-rabbitmq.html
https://deviantony.wordpress.com/2014/10/30/rabbitmq-and-haproxy-a-timeout-issue/
https://insidethecpu.com/2014/11/17/load-balancing-a-rabbitmq-cluster/