Привет, Хабр!
Продолжаем рассказывать, как построить отказоустойчивую связку на кластере MySQL.
Краткое содержание первой серии части нашего мануала:
мы развернули двухузловой кластер MySQL с асинхронной репликацией по GTID, улучшенной полу-синхронностью, и добавили два уровня отказоустойчивости: на уровне сервиса IP и роли БД.
Теперь пришло время рассмотреть, как происходит отказоустойчивое переключение пошагово в разных сценариях.

Сценарий 1. Отказ мастера node01
Авария. Предположим, node01 выходит из строя полностью: питание отключилось или пропала сеть.
На node01 keepalived перестаёт рассылать VRRP-объявления. node02 в пределах нескольких секунд (~3) фиксирует тишину и поднимает у себя VIP 10.0.0.100. Таким образом, клиенты начинают подключаться к node02 по тому же IP (выше). Если node01 не совсем «умер», а завис, то track-скрипт на нём тоже упадёт, и он сам сбросит VIP. В любом случае VIP переходит к node02.
Orchestrator с интервалом в 5 секунд опрашивает мастера. Он обнаружит, что node01 не отвечает. По логике, он пометит его как мёртвый (DeadMaster) и начнёт процедуру Master Failover. Orchestrator выберет реплику node02 для промоута. Благодаря GTID он знает, на сколько реплика отставала. Если включён semi-sync, то node02 применил всё до последней транзакции или отстал максимум на одну, если мастер упал между ними. В настройках задаём DelayMasterPromotionIfSQLThreadNotUpToDate=true, чтобы Orchestrator немного подождал реплику. После он выполнит:
STOP SLAVE,который останавливает репликацию наnode02.RESET SLAVE ALLилиRESET SLAVEс сохранением GTID, не теряя GTID позицию.SET GLOBAL read_only = OFF, которыйснимет только-чтение, разрешив записи. Также, вероятно,node02включитsuper_read_only = OFF, если оно вдруг было включено.Переподключение других реплик, если они у вас есть, к новому мастеру. В нашем случае это неактуально.
Пометит
node01как «сбитый». Благодаря нашему параметруApplyMySQLPromotionAfterMasterFailover: true, Orchestrator не пытается подключитьnode01обратно автоматически. Он просто зафиксирует факт переключения кластера наnode02.
node02 теперь мастер: принимает запросы, VIP у него, read_only=OFF. node01 недоступен/выключен. В orchestrator UI кластер будет отображаться как имеющий новый мастер. Можно настроить уведомление. Orchestrator умеет посылать SMTP-alert или собщение в Slack. Если нужно, добавляем SMTP-настройки или OnFailureDetectionProcesses.
Потеря данных
Если semi-sync работает, то число потерянных транзакций минимально или таковых вообще нет , так как node02 подтянул всё. Если semi-sync нет, последние миллисекунды транзакций на node01 не успевают в node02. Orchestrator не сможет их восстановить, когда мастер упал. Этот риск мы специально снизили благодаря semi-sync.
Что можно сделать дополнительно? Orchestrator по умолчанию помечает старый мастер как неисправный. Если node01 «оживёт» (скажем, электричество восстановили), то:
Keepalivedна нём не вернёт VIP из-заnopreempt— VIP останется наnode02.MySQL на
node01запустится. НоOrchestratorне будет пытаться сделатьnode01обратно мастером. Фактическиnode01останется «в изоляции».
Теперь важный шаг после аварии: администратор должен вручную реинтегрировать node01 как реплику. Это можно сделать средствами Orchestrator или вручную:
Зайти в веб-интерфейс, кликнуть на
node01и выбрать Reconnect as replica подnode02—Orchestratorвыполнит наnode01:CHANGE MASTER TO MASTER_HOST='node02', MASTER_AUTO_POSITION=1, ...; START SLAVE;, а также установит на нём read_only=ON (обязательно, так как теперь он реплика). Но убедитесь, что на node01 нет «лишних» транзакций. Если node01 полностью мёртв, то всё ОК. Если же он был в split-brain и туда успели записаться данные, придётся разбираться и выравнивать вручную.Если делаем руками, то заходим на неисправную
mysql_node01 - mysql:
RESET SLAVE ALL; CHANGE MASTER TO MASTER_HOST='node02', MASTER_USER='replicator', MASTER_PASSWORD='yourpass', MASTER_AUTO_POSITION=1; START SLAVE;
Сценарий 2. Отказ реплики — node02
Авария. Если падает реплика node02, а мастер node01 продолжает работать, то для отказоустойчивости ничего делать не нужно — мастер же жив. Однако потеря реплики означает, что у нас временно нет резервного узла. Keepalived заметит, что node02 не отвечает на VRRP. node02 был резервным, поэтому сейчас не критично. node01 продолжит удерживать VIP.
Orchestrator отметит node02 как недоступный. Автофейловера не будет, ведь мастер жив. Orchestrator пошлёт оповещение о деградации, если вы зададите соответствующие настройки.
Восстановление
Когда node02 вернётся, Orchestrator автоматически подключит его как реплику к мастеру, если настроена опция ResumeSlaveRecovery или аналог. Нет? Делаем вручную. При старте node02 MySQL, скорее всего, увидит неполный GTID, и репликация не продолжится без RESET SLAVE. Проще всего: зайдите в UI Orchestrator и выберите recover, тогда orchestrator сам подцепит node02 как реплику заново к node01 — мастеру.
Пока реплика отсутствует, риск потери данных выше, так как нет второго узла. Как можно скорее восстановите реплику или добавьте временно другую.
Сценарий 3. Разделение сети между ЦОДами — split brain
Самый сложный случай. Допустим, node01 и node02 не могут связаться друг с другом, канал между ЦОД1 и ЦОД2 потерян. Но оба сервера и Orchestrator живы. Предположим, orchestrator находится в ЦОД2 с node02.
Если канал между узлами пропал, VRRP-пакеты не доходят. node01 – мастер — продолжит считать себя мастером: он в сети ЦОД1, VIP у него, клиенты ЦОД1 его видят. node02 перестанет слышать node01 и через 3 секунды решит, что мастер недоступен — возьмёт VIP на себя. Теперь у нас две копии VIP: в сети ЦОД1 на node01 и в сети ЦОД2 на node02. Если сети разделены полностью, они не конфликтуют на уровне IP, каждый отвечает в своем сегменте. Но приложения в ЦОД2 начнут работать с node02, а в ЦОД1 — с node01. Оба узла думают, что они — master. Репликация, конечно, на обоих остановится — соединение master–slave порвалось.
Вероятно, Orchestrator (node03 в ЦОД2) потеряет связь с node01 (ЦОД1), но продолжит видеть node02. Он решит, что node01 умер, и инициирует failover — продвижение node02 в мастера, как в сценарии 1. node02 стал мастером с точки зрения keepalived и VIP. Orchestrator установит node02 writeable и node01 detach. Orchestrator теперь считает кластер на node02.
Итог во время разделения: node02 — мастер для части мира (ЦОД2), node01 — «призрачный» мастер в ЦОД1. Они независимы, оба принимают изменения от своих клиентов. Это плохо для целостности данных — произойдёт расхождение.
Однако, возможно, у вас приложения распределены и, например, основная пишущая нагрузка — с одной стороны.
После восстановления связи Orchestrator снова увидит node01. Но благодаря ApplyMySQLPromotionAfterMasterFailover=true он не подключит его автоматически обратно. Он пометит node01 как WARNING или как устаревший. Администратору придётся решить, что выбрать:
Скорее всего, данные на
node01уже неактуальны (там не было новых данных из ЦОД2), и он не сможет просто подключиться кnode02из-за рассинхрона GTID. Самый безопасный путь — полностью заново синхронизироватьnode01из бэкапаnode02. То есть остановитьnode01MySQL, сделать dump/backup сnode02и восстановить наnode01, затем сделатьCHANGE MASTER TOnode02. Это, по сути, решит split-brain восстановлением из одной ветки, потеряв другую, второстепенную.Менее травматичный путь — подключить
node01как репликуnode02. Эта возможность подойдёт, если наnode01за время разделения не произошло изменений, например, не было клиентов в ЦОД1. Скорее всего,OrchestratorпотребуетRESET SLAVE ALLнаnode01иSET GLOBAL GTID_PURGED = ..., если GTID-сет отличается. Это сложный процесс, поэтому лучше перестраховаться полным ресинком.
Вывод:
split-brain нужно не лечить, а предотвращать. Не держите пишущие клиенты в обеих частях кластера и при разделении сразу останавливайте приложения в одном из ЦОД.
Как улучшить защиту от split-brain:
Добавить третий узел-арбитр. В случае MySQL вариант — использовать MySQL InnoDB Cluster (Group Replication) с тремя узлами или два узла + arbitrator. В MySQL Group Replication нет понятия арбитра, но можно добавить третий узел на слабом VM purely for quorum. Однако смена технологии выходит за рамки задачи.
Можно настроить скрипт в keepalived, который будет проверять, доступен ли мастер с другой стороны. Например,
node01может пинговатьnode02илиnode03, и, если теряет связь, он сам опустит свой priority (например, черезvrrp_script).Тем не менее рассмотрите возможность хотя бы мониторинга со стороны: чтобы оперативно заметить split-brain. Рекомендуем Zabbix/Nagios, который видит, что VIP одновременно отвечает с двух разных MAC/IP.
Взаимодействие таймаутов
Мы намеренно настроили keepalived достаточно агрессивно (интервал 3 секунды), а Orchestrator опрашивает состояние раз в 5 секунд. Это значит, что VIP переключится чуть раньше, чем Orchestrator успеет продвинуть реплику в мастера. Возникает короткое окно (2–5 секунд), когда IP уже указывает на новую ноду, но она всё ещё находится в режиме read_only=ON.
В этот момент попытки записи завершатся ошибкой — и это ожидаемое поведение во время переключения. Через несколько секунд Orchestrator снимает read_only, и сервис возвращается в рабочее состояние.
Можно попытаться синхронизировать тайминги иначе, но тогда рискуем потерять время. Этот промежуток считается «небольшим простоем сервиса» при переключении (обычно <5 сек). Важно учитывать это на уровне приложения: операции записи должны допускать безопасный повтор, а клиентская логика — поддерживать retry при ошибке read_only.
В альтернативном подходе MHA или hooks можно вигать VIP уже после промоушена. Однако это увеличит общее время переключения. Мы сознательно выбрали модель, где IP переезжает быстрее, чем завершается роль БД: несколько секунд отказа записи — приемлемая цена за минимальный RTO.
Опции Orchestrator для задержки VIP
Мы намеренно не включали у Orchestrator никаких PreFailoverProcesses типа «подождать VIP». Однако Orchestrator позволяет задать паузу между детектированием отказа и началом переключения (например, FailoverDelayBeforePromotion). Настроим 2–3 секунды — это даст шанс VIP уже оказаться на реплике к моменту, когда Orchestrator сделает её writeable. Это чуть уменьшит окно без записи. Если очень нужно, добавляем в конфиг: "DelayMasterPromotionIfSQLThreadNotUpToDate": true – тогда Orchestrator проверит, что реплика догнала мастера (SQL thread). Тогда тоже указываем "RemoteMasterCoordinatedDelaySeconds": 5 — пауза. В нашем случае GTID + semi-sync обеспечат, чтобы реплика либо догнала, либо стала очень близка.
Мы сознательно не использовали в Orchestrator предварительные сценарии переключения вроде ожидания переезда VIP. Вместо этого можно аккуратно управлять таймингами самого Orchestrator. Параметр FailoverDelayBeforePromotion позволяет задать паузу между обнаружением отказа и повышением реплики до мастера. Если установить задержку в 2–3 секунды, VIP с высокой вероятностью уже окажется на новой ноде к моменту снятия read_only. Это сокращает окно отказа записи.
При более строгих требованиях можно включить параметрDelayMasterPromotionIfSQLThreadNotUpToDate. В этом случае Orchestrator не будет продвигать реплику, пока её SQL-поток не догонит мастера. Дополнительно задаётся координационная пауза (RemoteMasterCoordinatedDelaySeconds, например 5 секунд).
В нашей конфигурации связка GTID и полусинхронной репликации гарантирует, что реплика либо полностью догоняет мастера, либо отстаёт минимально.
Проверяем работы
Проведите тестирование на боевых настроенных серверах:
Тест падения мастера. Остановите сервис MySQL на
node01:systemctl stop mysqld. Через ~3 секунды выполните на клиентской машине или наnode03:mysql -h 10.0.0.100 -u ... -p -e "SELECT @@hostname, @@read_only;". Ожидается, что соединение установится кnode02и вернёт его hostname,@@read_only = 0, так как запись разрешена. Orchestrator-лог и UI отразят событие переключения мастера — вы увидите запись оDetected DeadMasterиpromoted slave.Тест возврата узла. Запустите MySQL на
node01снова. Убедитесь, что VIP остался наnode02. Наnode01командыip addrне показывают VIP. В UI Orchestrator увидите, чтоnode01помечен красным или серым в оффлайне. Затем можно вручную подключитьnode01как реплику: на веб-интерфейсе выберите его и опцию “Make Slave of” ->node02. Orchestrator выполнит смену источника репликации. После этогоnode01станет репликой, догонит GTID, и в UI будет зеленым нижеnode02. Наnode01проверьте@@read_only = 1. Отлично.Тест VIP failback. Теперь переключите нагрузку обратно — плановый перенос мастера обратно на
node01. Делаем так: помечаемnode02в Orchestrator как желаемую реплику или используем команду graceful failover. Так как с версии 3.0+ вOrchestratorпоявился режимgraceful-master-takeover, что предусматривает ручной перевод ролей, мы можем:orchestrator -c graceful-master-takeover -alias <cluster_alias>
Это если cluster alias задан. Либо выполняем:
orchestrator -c graceful-master-takeover -uid <Node01_FQDN>:3306
Замечание инженера. В документации GitHub говорится, что graceful takeover работает во всех режимах: GTID, Pseudo, etc. В ручном режиме Orchestrator остановит приложения (можно заранее переключить VIP), но так как VIP у нас управляется вне orchestrator, лучше:
1. На node01 временно выключить read_only. Для реплики нужны права SUPER: SET GLOBAL read_only=OFF. Также стоит убедиться, что node01 догнал node02.
2. Остановить MySQL на node02 плавно или просто выключить keepalived на node02 systemctl stop keepalived . Так VIP переедет на node01, потому что node02 перестал держать его. Если node02 уйдёт, node01 станет мастером VIP обратно.
3. Orchestrator заметит, что node02 недоступен, и автоматически выполнит failover на node01, который уже актуален.
В общем, planned switchover — это отдельная процедура, можно её настроить, но она выходит за рамки основной инструкции. Главное — auto failback у нас не происходит, и это хорошо😊
Best practices
Помимо сказанного, мы рекомендуем предусмотреть:
Мониторинг и алерты. Настройте оповещения о событиях — падение узла, фейловер, split-brain.
Orchestratorможет отправлять оповещения (есть параметрыSMTP_*,OnFailureDetectionProcessesдля вызова скриптов).Keepalivedтоже может запускать скриптыnotify_master,notify_backup,notify_fault. Например, можно написать скрипт, который приnotify_masterнаnode02отправит сообщение ответственным. Пока мы эти детали опустили, но в продуктиве это нужно, чтобы дежурные знали о переключении.Тестирование обновлений. После каждого изменения конфигурации (MySQL, keepalived, Orchestrator) тестируйте все сценарии в контролируемой обстановке. Особенно проверяйте corner-cases: кратковременное мигание сети, перезапуск только демона MySQL без полного падения хоста, нагрузочное поведение. Например, при фейловере убедитесь, что полет транзакций консистентен.
Оркестратор Raft mode. При возможности добавьте третий узел, чтобы можно было включить режим распределённого Orchestrator — он использует Raft-консенсус для отказоустойчивости. Для этого нам понадобится три инстанса, например, в каждом ЦОД + ещё один. Это уже расширение. В нашей задаче Raft не применяется — Orchestrator запускается в одиночку. Поэтому позаботьтесь о резервной копии его SQLite-файла.
Проверка конфигурации MySQL. Убедитесь, что
skip_slave_start = 1, чтобы реплика не стартовала сама до Orchestrator при перезапуске, а также, чтоauto_increment_offsetиauto_increment_incrementне конфликтуют. Мы используем master-slave и наnode02не даём писать, так что конфликтов автоинкремента не будет.Эксплуатация. После любого фейловера, планового или аварийного, рекомендовано как можно скорее восстановить второй узел и убедиться, что снова есть мастер и реплика. Не оставляйте кластер надолго в деградированном состоянии (один узел), иначе следующая авария приведёт к простою или потере данных.
Версии ПО. MySQL 8.0.24 — рабочая, но убедитесь, что применены последние патчи (8.0.x). К 2026 году — актуальная версия Orchestrator 3.2.6. Keepalived — версия 2.x. Желательно иметь keepalived >=2.0 для корректной работы
nopreempt(в старых были баги).
Заключение
В результате схема выглядит предсказуемо в любом аварийном сценарии.
Если падает ЦОД1 (где был мастер)
ЦОД2 автоматически становится основным: реплика продвигается в мастера, VIP переезжает, потери данных минимальны благодаря полусинхронной репликации.
Если падает ЦОД2 (реплика и Orchestrator)
ЦОД1 продолжает работать как мастер, VIP остаётся на месте. Автоматическое переключение не требуется — отказоустойчивость сохраняется. После восстановления достаточно заново подключить реплику.
Если происходят кратковременные сетевые сбои или возврат питания
VIP не «скачет» между площадками, поскольку отключён preempt. Это важно: мы избегаем лишних разрывов соединений и повторных переподключений клиентов во время восстановления.
Таким образом, при потере любой из площадок сервис остаётся доступным.
Вам могут пригодиться эти материалы:
MySQL Orchestrator – топология и автоматический failover (Percona Blog)
Архитектура keepalived для failover без preempt (ArchLinux Wiki)
Настройка пользователя и прав для Orchestrator, требования к master_info_repository
Использование Orchestrator с GTID и Pseudo-GTID (возможности)
Спасибо, что дочитали!
