Начало
Короткая история про то, как штатная замена ноды оставила quorum-очереди с двумя членами вместо трёх, и почему cluster_status этого не показывает.
Окружение: OpenStack Caracal (2024.1), развёрнут kolla-ansible. RabbitMQ — кластер из трёх нод, quorum-очереди.
Симптом
В одном из регионов перестали создаваться виртуальные машины. Не медленно, не через раз — вообще.
В логах Cinder:
AMQP server on <host>:5672 is unreachable MessageNacked: ...
Cinder не мог достучаться до RabbitMQ. Nova ждала тома, тома не приезжали, создание ВМ вставало намертво.
Кластер RabbitMQ при этом состоял из трёх нод, из которых отвалилась ровно одна. То есть по всем канонам сервис должен был продолжать работать. Он не продолжил.
Первый след: сеть
Начали с самого прозаичного — со связности. И почти сразу нашли причину падения ноды.
Незадолго до инцидента на leaf-коммутаторе переназначали порт. Порт считался свободным. Он свободным не был — на нём висела одна из нод контроллеров. Порт переконфигурировали, нужные VLAN с него ушли, нода потеряла связность.
Откатили конфиг коммутатора на предыдущий. Связность с одной нодой вернулась, со второй — нет: в откаченном конфиге не оказалось части VLAN, добавленных позже. Досыпали VLAN руками, связность восстановилась полностью. Простой — 1 час 40 минут.
Сеть починили. Но главный вопрос остался открытым.
Почему кластер из трёх нод не пережил потерю одной
Кластер из трёх нод обязан переживать потерю одной. Это его единственная работа.
Полезли смотреть состояние очередей — и вот тут выяснилось главное.
Quorum-очереди имели по два члена вместо трёх.
Для quorum-очередей это приговор. Кворум считается как большинство от числа членов конкретной очереди, а не от числа нод в кластере:
членов 3 → большинство 2 → потеря одной ноды переживается спокойно;
членов 2 → большинство всё равно 2 → при потере одной ноды живой остаётся одна реплика из двух, кворума нет, очередь недоступна и на запись, и на чтение.
То есть кластер формально был трёхнодовым, а фактически каждая очередь — двухнодовой, с отказоустойчивостью хуже, чем у одиночной ноды. Одиночная нода хотя бы не притворяется.
Именно поэтому падение одного контроллера уронило Cinder, а с ним и создание ВМ во всём регионе.
Как очереди стали двухчленными
Вот здесь самое интересное, и это не экзотика, а рутинная операция.
Некоторое время назад в кластере меняли ноду: выводили старую, вводили новую. Кластер после замены выглядел абсолютно здоровым — три ноды, cluster_status зелёный, всё running.
Но состав членов quorum-очереди — это не то же самое, что состав кластера:
Очереди были созданы, когда в кластере были ноды A, B и C_old. Члены каждой очереди: A, B, C_old.
Ноду C_old вывели. Из членов очередей она ушла — осталось два: A и B.
Ноду C_new ввели в кластер. В члены существующих очередей она не попала.
И вот это — ключевой момент, который стоит запомнить:
RabbitMQ не добавляет новых членов в существующие quorum-очереди автоматически. Состав членов фиксируется при создании очереди и дальше меняется только явными командами.
Можно добавить в кластер хоть десять нод — cluster_status покажет красивую десятку, а очереди как жили на двух нодах, так и останутся. Никакого предупреждения, никакого варнинга в логах, никакой деградации в обычной работе. Всё работает ровно до первого отказа ноды.
Отдельно про kolla-ansible: она приводит в порядок состав кластера, но членство в quorum-очередях — это runtime-состояние брокера. Плейбук за вас его не дорастит.
Фикс
Доращивание новой ноды до члена всех quorum-очередей:
rabbitmq-queues grow <new-node> all
Проверка результата:
rabbitmqctl list_queues name type members leader --formatter=pretty_table
Все quorum-очереди должны показывать по три члена.
Правильная процедура замены ноды
Это, пожалуй, главное, что стоит унести из статьи. Порядок важен: сначала растим, потом сжимаем — иначе получаете окно, в котором очереди деградированы.
# 1. Новая нода входит в кластер rabbitmqctl join_cluster rabbit@<new-node> # 2. Дорастить членство quorum-очередей до новой ноды rabbitmq-queues grow <new-node> all # 3. Убедиться, что членов стало на один больше rabbitmqctl list_queues name type members --formatter=pretty_table # 4. Только теперь убрать старую ноду из членов очередей rabbitmq-queues shrink <old-node> # 5. И удалить её из кластера rabbitmqctl forget_cluster_node rabbit@<old-node> # 6. Финальная проверка: не критична ли какая-то нода для кворума rabbitmq-queues check_if_node_is_quorum_critical
Шаг 6 — самая недооценённая команда во всём RabbitMQ. Она отвечает ровно на тот вопрос, который вы хотите задать перед выводом ноды: «если я сейчас погашу эту ноду, что-нибудь потеряет кворум?» Ненулевой код возврата означает «не трогай». Стоит секунду, снимает целый класс аварий, и её место — в чек-листе любых плановых работ и в мониторинге.
Полезно также глянуть политики — если где-то выставлен quorum-initial-group-size меньше числа нод, новые очереди будут создаваться уже деградированными:
rabbitmqctl list_policies
И, для полноты, в kolla-ansible за режим очередей отвечает om_enable_rabbitmq_quorum_queues в globals.yml — стоит проверить, что у вас там на самом деле включено.
Что забрать с собой
1. «Три ноды в кластере» и «три реплики у очереди» — разные утверждения. Отказоустойчивость определяется вторым, а мониторится обычно первое. Между ними спокойно живёт полностью неработающая избыточность, о которой вы узнаете в худший из возможных моментов.
2. Замена ноды в кластере — не атомарная операция. Состав кластера и состав членов очередей меняются по отдельности, и после join_cluster работа не закончена. Если у вас в рантбуке замены ноды нет шага grow — считайте, что у вас нет рантбука замены ноды.
3. Проверять кворум-критичность нужно до работ, а не после инцидента. В нашем случае нода отвалилась незапланированно, но результат был бы точно таким же при штатном выводе ноды в обслуживание. И это, честно говоря, страшнее: мы бы уронили регион своими руками, по плану, в согласованное окно.
4. «Свободный порт» — это гипотеза, а не факт. Перед переназначением стоит потратить полминуты на статус интерфейса, таблицу MAC-адресов и соседей по LLDP. Стоимость проверки — секунды, стоимость ошибки — час сорок простоя региона.
5. Откат конфига не равен возврату в исходное состояние. Мы откатились на сохранённый конфиг и получили не «как было», а «как было на момент сохранения» — без VLAN, добавленных позже. Резервная копия конфигурации устаревает ровно с той скоростью, с какой вносятся изменения мимо неё.
6. Инцидент закончился раньше, чем закончилась проблема. Связность восстановили за 1:40, сервис поднялся, инцидент закрыли. А настоящая причина — двухчленные очереди, оставшиеся после замены ноды месяцами ранее, — была найдена и устранена уже потом. Инцидент, который вы закрыли, и причина, которую вы устранили, — разные события, и путать их дорого.