MAATRIX / Блог / Переключили базу на новый сервер и потеряли двенадцать секунд транзакций

Переключили базу на новый сервер и потеряли двенадцать секунд транзакций

MAATRIX

Миграция базы данных на новый сервер по всем признакам прошла гладко: реплика была «синхронизирована», приложение переключили, сайт не упал. А через день выясняется, что десяток заказов из корзины исчез, у пары пользователей задвоились платежи, а в логах — дыра ровно там, где должно было быть переключение. Это не редкая случайность, а типичная ошибка cutover: между остановкой записи на старый сервер и подтверждённым догоном реплики на новом всегда есть окно, и если его не закрыть явно, оно закрывается само — за ваш счёт.

Откуда берётся это окно

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

Слово «синхронизирована» в мониторинге репликации почти никогда не значит «применены все байты по состоянию на текущую наносекунду». Оно значит «отставание в пределах нормы на момент последнего замера» — а замер тоже не мгновенный. В PostgreSQL это pg_stat_replication.replay_lsn в сравнении с текущим pg_current_wal_lsn(), в MySQL — Seconds_Behind_Master из SHOW SLAVE STATUS. Оба показателя обновляются с интервалом, оба могут показать «0 секунд отставания» в момент, когда фактически несколько транзакций ещё летят по сети или лежат в очереди на применение.

Дальше происходит вот что при типичном (плохом) сценарии cutover:

  1. Администратор смотрит на дашборд, видит «lag: 0s» или «lag: 1s», решает, что реплика догнала.
  2. Переключает DNS, балансировщик или конфиг приложения на новый сервер.
  3. Старый сервер либо продолжает принимать часть трафика ещё несколько секунд (пока не обновится DNS-кэш или не отвалятся старые соединения), либо новый сервер начинает принимать запись раньше, чем реально применил последние транзакции со старого.

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

Двенадцать секунд из заголовка — не выдуманная страшилка, а реалистичный порядок величины: столько может занять доставка последних WAL-сегментов или binlog-событий по сети между дата-центрами при миграции между локациями, плюс задержка обновления DNS/TTL на стороне клиентов, плюс время, которое админ тратит между «посмотрел на лаг» и «переключил трафик». По отдельности каждая цифра небольшая, вместе — вполне достаточно, чтобы потерять реальные заказы.

Почему «на глаз» не работает даже с опытным инженером

Дело не в невнимательности — дело в том, что сам процесс наблюдения за отставанием реплики устроен так, что глазами его не закрыть:

  • Метрика лага дискретна и с задержкой. Даже если панель мониторинга обновляется раз в секунду, между «реплика реально догнала» и «дашборд это показал» проходит время.
  • «0 секунд» не означает «0 транзакций». Метрика по времени скрывает случаи, когда за последнюю секунду до переключения на мастер прилетело ещё 5-10 коротких транзакций, которые физически не успели уйти на реплику.
  • Переключение — это не одна операция, а последовательность. Смена конфига приложения, рестарт пула соединений, обновление DNS, инвалидация кэша балансировщика — каждый шаг занимает время, и старый сервер может принимать запись, пока вы уже смотрите в дашборд нового.
  • Нагрузка в момент cutover часто выше обычной. Если миграцию делают не глубокой ночью, а в рабочее окно, именно в момент переключения может прийти всплеск записи — а это как раз и есть транзакции с максимальным риском потери.

Всё это работает и при переезде между обычными серверами (см. миграцию базы данных между серверами), и тем более при переезде между разными дата-центрами или странами, где сетевая задержка сама по себе добавляет секунды к лагу.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Правильная последовательность переключения

Единственный надёжный способ закрыть это окно — не полагаться на визуальное «лаг маленький», а провести явную процедуру с точками синхронизации, на каждой из которых система сама подтверждает состояние, а не человек его угадывает.

Общая схема для cutover с минимальным риском потери данных:

  1. Перевести приложение в режим read-only или полностью остановить запись на старый мастер. Это ключевой шаг, который часто пропускают, пытаясь сократить простой. Без него следующие шаги бессмысленны — реплика будет пытаться догнать движущуюся цель.
  2. Зафиксировать позицию мастера на момент остановки записи. В PostgreSQL — SELECT pg_current_wal_lsn();, в MySQL — SHOW MASTER STATUS; (или SHOW BINARY LOG STATUS; в свежих версиях) даёт файл и позицию binlog на этот момент.
  3. Дождаться, пока реплика подтверждённо достигнет именно этой позиции, а не просто «лаг близок к нулю». Для PostgreSQL сравнить pg_last_wal_replay_lsn() на реплике с зафиксированной LSN мастера — они должны сравняться или реплика должна её обогнать. Для MySQL — дождаться, пока Exec_Master_Log_Pos (или Relay_Log_Space в зависимости от топологии) сравняется с зафиксированной позицией, и Seconds_Behind_Master станет 0 при отсутствии новой записи на мастере.
  4. Только после подтверждения — переключить точку записи приложения на новый сервер (через конфиг, через промоут реплики в мастер, через изменение DNS с уже прогретым низким TTL).
  5. Снять read-only с нового сервера и вернуть приложение в обычный режим.
  6. Проверить целостность — сверить количество строк в ключевых таблицах, последние ID/timestamp на старом и новом сервере, отсутствие дублей по уникальным бизнес-ключам (номер заказа, external_id платежа и т.п.), а не только по факту, что сервер отвечает.

Псевдокод для PostgreSQL, который стоит завернуть в скрипт, а не выполнять руками по шагам:

# 1. Останавливаем запись (пример: приложение снимается с очереди балансировщика
# или переводится в maintenance-режим на уровне конфига)
kubectl scale deployment app-writer --replicas=0
# или проще: UPDATE pg_hba и SELECT pg_reload_conf() чтобы блокировать новые коннекты на запись

# 2. Фиксируем позицию мастера
psql -h old-db-host -U admin -c "SELECT pg_current_wal_lsn();"
# пример вывода: 0/3F2A1B8

# 3. Ждём, пока реплика догонит именно эту позицию
until psql -h new-db-host -U admin -tAc \
  "SELECT pg_last_wal_replay_lsn() >= '0/3F2A1B8'::pg_lsn;" | grep -q t; do
  echo "реплика ещё не догнала, ждём..."
  sleep 1
done
echo "реплика подтверждённо синхронизирована"

# 4. Промоутим реплику или переключаем connection string приложения
psql -h new-db-host -U admin -c "SELECT pg_promote();"

# 5. Возвращаем запись
kubectl scale deployment app-writer --replicas=3

Для MySQL логика та же, только сравниваются координаты binlog:

-- на мастере, после остановки записи
SHOW MASTER STATUS;
-- File: mysql-bin.000042, Position: 154887321

-- на реплике, в цикле опроса
SHOW SLAVE STATUS\G
-- ждём Relay_Master_Log_File = 'mysql-bin.000042'
-- и Exec_Master_Log_Pos >= 154887321
-- и Slave_SQL_Running_State пуст (нет ожидающих событий)

Важно: сравнивать нужно именно координаты (LSN, файл+позиция), а не поле секунд отставания. Seconds_Behind_Master = 0 теоретически возможно и при незавершённом применении последних событий, если между гетбитами мастера прошло меньше секунды — координаты дают точный, а не приблизительный ответ.

Синхронная репликация как альтернатива ручному ожиданию

Если такие переключения случаются не разово при миграции, а регулярно (например, плановый failover в кластере), процедуру с ручным ожиданием LSN стоит заменить на архитектурный уровень — синхронную или полусинхронную репликацию, при которой мастер не подтверждает commit клиенту, пока реплика не подтвердила приём. Тогда к моменту, когда клиент получил успешный ответ, гарантированно есть копия на реплике, и окно потери данных не появляется по конструкции, а не по расписанию проверки.

Плата за это — задержка записи растёт, потому что каждая транзакция ждёт round-trip до реплики, и при разнесённых дата-центрах (RU/US/UK-топология, например) это может быть заметно. Подробный разбор компромисса — в статье про синхронную репликацию и цену надёжности. Для разовой миграции с одного сервера на другой городить синхронную репликацию обычно избыточно — хватает описанной выше процедуры с явным ожиданием координаты. Общий механизм отставания реплики и то, почему цифра в секундах — не абсолютная гарантия, разобран в статье как работает репликация и что значит отставание в секундах.

Идемпотентность как страховка на случай дублей

Даже правильная процедура cutover не защищает от одного класса проблем — повторных запросов клиента, который не дождался ответа во время переключения (соединение оборвалось, таймаут) и повторил операцию. Если приложение слепо создаёт новую запись на каждый POST-запрос, вы получите дубль платежа или заказа, даже если данные на серверах базы полностью консистентны.

Практическая страховка — идемпотентность на уровне бизнес-логики:

  • Клиент присылает idempotency_key (UUID, сгенерированный на его стороне при первой попытке).
  • На сервере — уникальный индекс по этому ключу в таблице транзакций/заказов.
  • Повторный запрос с тем же ключом возвращает результат первой попытки, а не создаёт новую запись.
CREATE UNIQUE INDEX idx_orders_idempotency_key
  ON orders (idempotency_key)
  WHERE idempotency_key IS NOT NULL;

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

Что проверять после переключения, а не только до

Процедура с LSN закрывает окно потери данных на уровне репликации, но не гарантирует, что приложение действительно смотрит туда, куда нужно. После cutover стоит явно проверить:

  • Пул соединений приложения — переоткрылся ли он на новый хост, не осталось ли старых keep-alive соединений на прежний сервер (особенно если используется PgBouncer/ProxySQL — их тоже нужно переключать, и в них тоже есть свой буфер соединений).
  • Автоувеличение (auto-increment/sequence) на новом сервере не отстаёт от старого — при промоуте реплики в мастер PostgreSQL пересчитывает последовательности не всегда автоматически при логической репликации; для физической (потоковой) репликации это не проблема, но если использовалась логическая — стоит сверить last_value вручную.
  • Триггеры и внешние интеграции, которые могли быть отключены на реплике для снижения нагрузки (частая практика) — их нужно включить обратно на новом мастере.
  • Итоговые суммы по ключевым таблицам — количество строк, сумма по денежным полям, максимальный ID/timestamp — сравнить со старым сервером на момент остановки записи. Чек-лист общего вида есть в статье как проверить, что миграция прошла успешно.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли просто подождать «с запасом», например 30 секунд после того, как лаг показал 0?

Отчасти работает, но это угадывание вслепую: если в момент переключения был всплеск записи, 30 секунд может не хватить, а если запись на старый сервер не была явно остановлена — не поможет никакое ожидание, потому что цель продолжает двигаться. Явная проверка координаты LSN/binlog-позиции стоит примерно столько же усилий, но даёт точный, а не вероятностный ответ.

Что если переключение делает не человек по шагам, а скрипт/оркестратор — там же не бывает «на глаз»?

Проблема остаётся, если скрипт использует ту же метрику Seconds_Behind_Master или lag в секундах вместо точного сравнения координат. Автоматизация не спасает от неверной логики проверки — она просто выполняет её быстрее и без раздумий.

Актуально ли это для managed-баз (RDS, Cloud SQL и подобных)?

Да, механизм тот же — асинхронная репликация есть и там, просто у управляемого сервиса обычно есть встроенная процедура promote/failover, которая делает описанное выше внутри себя. Стоит явно проверить в документации конкретного провайдера, ждёт ли promote подтверждённого догона или просто переключает по таймауту.

А если это не первичная миграция, а постоянная схема с несколькими читающими репликами — там тоже нужно это ожидание?

Для операций чтения с реплики (read-after-write) проблема похожая, но решается иначе — обычно направлением конкретного запроса на мастер сразу после записи или проверкой той же LSN перед чтением, а не полной остановкой записи. Это тема отдельного разбора read-after-write consistency, а не разовый cutover.

Что делать, если старый сервер уже выключен, а расхождение обнаружено постфактум?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →