Реплика повторила ваш DROP TABLE за одну секунду
Инженер набирает DROP TABLE orders;, метясь в тестовую базу, а попадает в боевую — секунда на нажатие Enter, и та же команда уже выполняется на реплике, которую держали именно на случай аварии. Реплика ничего не перепутала: она в точности сделала то, для чего создана, — как можно быстрее повторить у себя всё, что происходит на мастере. Ниже — что происходит технически в эти секунды, почему реплика физически не может отфильтровать ошибку, и какие механизмы дают реальное время на реакцию.
Содержание
- Что технически происходит в момент DROP TABLE
- Хронология: сколько на самом деле проходит времени
- Почему реплика не умеет отказаться выполнить команду
- Три заблуждения, которые дорого стоят
- Что реально защищает: бэкапы с точками восстановления в прошлое
- Второй рубеж: репликация с намеренной паузой
- Как собрать это в рабочую схему защиты
Что технически происходит в момент DROP TABLE
У любой транзакционной СУБД есть журнал упреждающей записи: в PostgreSQL это WAL (Write-Ahead Log), в MySQL/MariaDB — binlog. Каждое изменение данных сначала фиксируется в журнале, и только затем применяется к самим файлам данных. Репликация построена поверх этого же журнала: реплика — это, по сути, процесс, который непрерывно читает журнал с мастера и проигрывает его у себя.
В PostgreSQL за это отвечает пара walsender на мастере и walreceiver на реплике, работающие в режиме потоковой репликации:
# на реплике проверяем, что она вообще получает поток
SELECT status, sender_host, latest_end_lsn
FROM pg_stat_wal_receiver;
А на мастере можно посмотреть, насколько реплика отстаёт по трём стадиям — доставке, записи на диск и применению:
SELECT client_addr, state, write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
В MySQL аналогичная картина видна через SHOW REPLICA STATUS\G (в старых версиях — SHOW SLAVE STATUS), где ключевое поле — Seconds_Behind_Source (раньше Seconds_Behind_Master). Именно это число инженеры интуитивно принимают за «время на реакцию» — и именно оно вводит в заблуждение, потому что в здоровом кластере оно почти всегда близко к нулю.
Важно: в журнал попадает не намерение, а результат — уже свершившаяся, закоммиченная операция. Команда DROP TABLE orders; порождает в WAL или binlog запись вида «удалить объект orders», и реплика применяет её буквально, без всякого анализа смысла. О внутреннем устройстве этого механизма и о том, что вообще считается отставанием реплики, — отдельный разбор: как работает репликация и отставание реплики.
Хронология: сколько на самом деле проходит времени
Разберём типичный путь ошибки по шагам, без придуманных цифр — только порядок событий и ориентировочные величины, которые сильно зависят от вашей сети, нагрузки и конфигурации.
- Команда выполнена и закоммичена на мастере. Транзакция с
DROP TABLEсчитается завершённой в момент фиксации коммита — с этого момента откатить её на мастере уже нельзя средствами СУБД. - Запись журнала уходит на реплику. При потоковой репликации в локальной сети дата-центра это происходит почти сразу же после коммита — задержка обычно измеряется миллисекундами и держится в пределах долей секунды, если реплика не отстаёт по другим причинам.
- Реплика применяет запись.
replay-процесс на реплике (в PostgreSQL) илиSQL thread(в MySQL) выполняет ту же операцию у себя. При нормальной нагрузке это тоже занимает доли секунды. - Инженер осознаёт ошибку. Между «нажал Enter» и «понял, что не туда» у человека обычно уходит куда больше времени, чем весь путь записи от мастера до реплики, — счёт идёт уже на секунды, а не на миллисекунды.
Итог: к моменту, когда человек успевает среагировать («стоп, это же прод!»), таблица чаще всего уже пуста и на мастере, и на всех подключённых репликах — синхронных и асинхронных. Это не гипербола заголовка, а прямое следствие того, что репликация проектировалась для минимальной задержки, а не для того, чтобы оставлять окно на раздумья.
Отдельно стоит сказать про синхронную репликацию (synchronous_commit = remote_apply в PostgreSQL, semi-sync или rpl_semi_sync в MySQL). Она не замедляет, а ускоряет попадание ошибки на реплику относительно восприятия человека: клиент получает подтверждение коммита только после того, как изменение уже применено на реплике. То есть в момент, когда вы видите «команда выполнена успешно», откатывать уже нечего ни на одной из копий.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему реплика не умеет отказаться выполнить команду
Здесь ключевое непонимание: люди ждут от реплики какой-то «разумности» — способности отличить полезное изменение от разрушительного. У обычной физической (streaming/binlog) репликации такой способности нет и не может быть по конструкции.
Реплика получает поток низкоуровневых операций над страницами данных или строками — не SQL-текст с семантикой, а факт «изменилась страница X» или «удалена строка Y». На этом уровне невозможно понять, была ли операция ошибкой оператора, багом в коде или легитимным действием — с точки зрения журнала это одинаковые по формату записи. Фильтрация по смыслу потребовала бы разбора бизнес-логики в реальном времени, что противоречило бы самой цели репликации — быть точной и быстрой копией.
Даже логическая репликация в PostgreSQL (publication/subscription, репликация на уровне строк через pgoutput) не решает эту проблему — она может фильтровать таблицы или столбцы на этапе настройки, но не умеет распознавать, что конкретное DML-событие было ошибкой, уже находясь в потоке. Если таблица включена в публикацию, все операции над ней — легитимные и ошибочные — реплицируются одинаково добросовестно.
Единственный практический способ прервать распространение — это либо физически остановить применение журнала на реплике (pg_wal_replay_pause() в PostgreSQL, STOP REPLICA в MySQL) до того, как опасная запись до неё дошла, либо изначально не давать этой записи применяться сразу — тем самым мы приходим к идее задержанной репликации, о которой ниже.
Три заблуждения, которые дорого стоят
«У нас есть реплика — можно не так строго следить за бэкапами». Реплика и бэкап решают разные задачи: реплика — это живая, постоянно синхронизируемая копия, бэкап — это зафиксированная точка в прошлом. Если единственная защита данных — реплика, у вас нет ни одной независимой точки восстановления, только вторая копия текущего (в том числе ошибочного) состояния. Развёрнутый разбор именно этого заблуждения — в статье миф: репликация спасёт от случайного удаления данных.
«При аварии переключимся на реплику — данные точно сохранятся». Верно только для аппаратных сбоев мастера. Если авария — это сама ошибочная команда, переключение на реплику не помогает: на ней то же самое разрушенное состояние.
«Синхронная репликация надёжнее в смысле защиты от ошибок». Формально она надёжнее в смысле отсутствия потери данных при отказе мастера (нет неподтверждённых транзакций), но применительно к логическим ошибкам она даже опаснее асинхронной — как показано выше, она гарантирует, что ошибка применена на реплике ещё до того, как вы увидели подтверждение коммита.
Все три заблуждения объединяет одна и та же ошибка мышления: смешение «избыточности копий» с «защитой от неверных данных». Первое решает проблему отказа оборудования, второе — совершенно другая задача, требующая других инструментов.
Что реально защищает: бэкапы с точками восстановления в прошлое
Единственный механизм, который позволяет вернуться к состоянию до ошибки, а не повторить её, — это резервное копирование с восстановлением на точку во времени (Point-in-Time Recovery, PITR). Идея: у вас есть полная копия данных плюс непрерывно архивируемый журнал транзакций за период после этой копии, и восстановление проигрывает журнал вперёд только до заданного момента — не доходя до разрушительной команды.
Для PostgreSQL типичный стек — wal-g или pgbackrest для базовых копий и непрерывной архивации WAL:
# postgresql.conf — включаем архивацию журнала
archive_mode = on
archive_command = 'wal-g wal-push %p'
Восстановление на момент времени, предшествующий инциденту:
wal-g backup-fetch /var/lib/postgresql/16/main LATEST
# затем в postgresql.conf/recovery.signal указываем
recovery_target_time = '2026-08-29 14:31:55+03'
recovery_target_action = 'pause'
Флаг recovery_target_action = 'pause' останавливает восстановление сразу после достижения указанного момента и даёт время проверить состояние базы, прежде чем открывать её на запись, — это подстраховка от ошибки в самом времени восстановления.
Для MySQL/MariaDB логика похожая: полный бэкап (например, через Percona XtraBackup) плюс архив binlog, из которого при восстановлении исключается транзакция с ошибочной командой:
mysqlbinlog --start-datetime="2026-08-29 00:00:00" \
--stop-position=884213 \
/var/log/mysql/binlog.000112 | mysql -u root -p
Здесь --stop-position указывает точную позицию в журнале непосредственно перед разрушительным запросом — её можно найти, прочитав тот же binlog в текстовом виде и найдя нужную транзакцию по времени.
Без непрерывной архивации журнала транзакций PITR не существует в принципе — обычный периодический pg_dump или полный дамп раз в сутки даёт точку восстановления не точнее интервала между дампами, то есть в худшем случае вы теряете почти сутки данных. Практический порядок действий при восстановлении, включая типичные ошибки на этом пути, разобран в статье восстановление базы данных из бэкапа: практика.
PITR тоже не панацея: он требует времени на разворачивание полной копии и проигрывание журнала — это часы простоя, а не секунды переключения на реплику. Поэтому его стоит комбинировать со вторым рубежом защиты — задержанной репликой.
Второй рубеж: репликация с намеренной паузой
Delayed replication (задержанная реплика) — это обычная реплика, которой намеренно велено применять журнал не сразу, а с фиксированным отставанием — например, на 3-6 часов. Если на мастере происходит DROP TABLE, у вас появляется реальное окно времени, чтобы заметить проблему по алертам и остановить применение журнала на этой реплике до того, как разрушительная запись до неё дойдёт.
В PostgreSQL это один параметр на стороне реплики:
# postgresql.conf на delayed-реплике
recovery_min_apply_delay = '4h'
При срабатывании алерта на аномальное падение размера таблицы или количества строк применение журнала останавливают вручную:
SELECT pg_wal_replay_pause();
и восстанавливают состояние на момент до инцидента, не разворачивая бэкап с нуля.
В MySQL аналогичный параметр задаётся прямо при настройке источника репликации:
CHANGE REPLICATION SOURCE TO SOURCE_DELAY = 10800; -- 3 часа в секундах
В MongoDB похожая роль — у скрытого delayed-члена replica set: он поднимается с priority: 0, hidden: true и параметром задержки применения оплога:
cfg = rs.conf()
cfg.members[3].priority = 0
cfg.members[3].hidden = true
cfg.members[3].secondaryDelaySecs = 14400 // 4 часа
rs.reconfig(cfg)
Три оговорки, чтобы delayed-реплика не создала ложное чувство защищённости:
- Это дополнение к бэкапам с PITR, а не их замена: окно фиксированное, и если проблему не заметили за отведённые часы, задержанная реплика применит ту же ошибку.
- Задержку нужно подбирать по реальному времени реакции команды на алерты, а не наугад — слишком короткое окно не даёт запаса, слишком длинное увеличивает расхождение с продакшном и стоимость хранения журнала.
- Нужен отдельный мониторинг именно на аномалии (резкое падение числа строк, DDL-события), а не только на технический лаг — иначе никто не остановит репликацию вовремя.
Для СУБД в принципе нет команды «отменить» уже применённую структурную операцию, как это иногда бывает с миграциями схемы: если DDL выполнен и разошёлся по копиям, откатывать приходится либо по журналу (PITR), либо по паузе на задержанной реплике. Смежная тема — почему миграцию схемы часто физически нельзя откатить и что готовят заранее вместо отката — разобрана в статье откатить миграцию базы нельзя, что готовят вместо отката.
Как собрать это в рабочую схему защиты
Комбинация трёх механизмов закрывает основные сценарии потери данных, и у каждого — своя зона ответственности:
| Механизм | От чего защищает | Время восстановления | Чего не даёт |
|---|---|---|---|
| Обычная реплика (sync/async) | Отказ диска, сервера, сети мастера | Секунды-минуты (failover) | Не защищает от логической ошибки — повторяет её |
| Delayed replica | Логическая ошибка, замеченная быстро | Минуты после паузы | Фиксированное окно; не замена архива журнала |
| Бэкап + PITR | Логическая ошибка, замеченная поздно | Часы (развёртывание + проигрывание журнала) | Требует непрерывной архивации журнала заранее |
Практический минимум для боевой базы, которую жалко потерять: обычная реплика для отказоустойчивости, непрерывная архивация журнала транзакций для PITR и хотя бы одна delayed-реплика с задержкой, подобранной под реальное время реакции дежурного. Ни один из трёх компонентов по отдельности не закрывает весь риск — репликация без архива журнала не даёт отмотать назад, архив журнала без регулярных полных копий даёт долгое и дорогое восстановление, а delayed-реплика без мониторинга аномалий превращается в такую же обычную реплику, просто с задержкой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если я замечу ошибку через минуту после DROP TABLE, есть шанс успеть на обычной (не delayed) реплике?
Практически нет: при штатной потоковой или binlog-репликации в локальной сети запись обычно применяется за доли секунды — минуты для реакции человека, как правило, недостаточно, чтобы физически остановить репликацию раньше применения.
Можно ли настроить репликацию так, чтобы она сама отличала DDL от DML и блокировала опасные команды?
Штатными средствами потоковой/binlog-репликации — нет, она работает на уровне физических изменений, а не смысла команд. Для этого нужны внешние средства: proxy с анализом запросов, ролевые ограничения на выполнение DDL в проде, ревью перед выполнением — то есть организационные и инфраструктурные барьеры до репликации, а не после.
PITR через архив WAL/binlog — это то же самое, что снапшот диска раз в час?
Нет: снапшот диска фиксирует состояние на момент снятия и позволяет откатиться только к этим дискретным точкам, а PITR с непрерывным журналом позволяет восстановиться на любую секунду в пределах периода хранения журнала — в том числе на момент за секунду до инцидента.
Нужна ли delayed-реплика, если бэкапы с PITR уже настроены?
Не обязательна, но сильно ускоряет восстановление при вовремя замеченной ошибке — пауза на delayed-реплике занимает минуты, а разворачивание полного бэкапа с проигрыванием журнала — часы, особенно на большой базе.
Что делать, если DROP TABLE уже применился и на delayed-реплике, и в её задержку не успели?
Тогда единственный путь — восстановление из архивной копии через PITR на независимом сервере, не трогая ни мастер, ни существующие реплики, пока не подтверждено целевое состояние.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →