Откат на снапшот вернул данные и потерял три часа заказов
В обед на проде выкатили миграцию, которая начала тихо портить строки в таблице заказов. Решение приняли быстрое и на первый взгляд правильное — откатиться на утренний снапшот, когда база была ещё целая. Снапшот отработал безупречно: база поднялась ровно в том состоянии, в котором была в 09:00. Проблема в том, что с 09:00 до момента отката прошло почти три часа, и за это время успели оформиться реальные заказы живых клиентов — они исчезли вместе с испорченными миграцией строками. Снапшот не подвёл; он сделал именно то, для чего создан. Разберём, почему так происходит всегда, а не только в этом случае, и что можно сделать, чтобы в следующий раз потерять не три часа, а три минуты.
Содержание
- Что случилось: снапшот сработал идеально и в этом всё дело
- Почему так устроено: снапшот — это точка, а не диапазон
- Прежде чем откатывать: попробуйте вытащить данные из окна потерь
- Практика на живом инциденте: как выглядело восстановление окна
- Частые снапшоты как способ сузить окно, а не устранить его
- Что стоит настроить заранее, чтобы окно не пришлось закрывать вручную
Что случилось: снапшот сработал идеально и в этом всё дело
Хронология простая. В 09:00 по расписанию сработал плановый снапшот тома с базой — штатная процедура, которая выполнялась каждое утро месяцами без единого вопроса. В 11:40 деплой выкатил миграцию, добавляющую колонку с дефолтным значением через UPDATE-триггер; из-за ошибки в условии триггер начал перезаписывать поле total_amount у части заказов нулём. Проблему заметили в 12:35, когда в панели аналитики выручка за день внезапно просела вдвое. К 12:50 приняли решение — откатить том на снапшот 09:00, потому что чинить данные точечными UPDATE-ами по логам казалось дольше и рискованнее.
Восстановление заняло около семи минут. База поднялась, испорченные нулями заказы исчезли — вместе с ними исчезли и примерно 340 нормальных заказов живых клиентов между 09:00 и 12:50. Это не баг восстановления и не повреждение снапшота, а именно то, что он обязан делать по своей природе: вернуть систему целиком в состояние на конкретный момент. Все данные, появившиеся позже этого момента — хорошие они или испорченные, — с точки зрения снапшота одинаково не существуют.
Почему так устроено: снапшот — это точка, а не диапазон
Ключевая вещь, которую стоит держать в голове перед любым откатом: снапшот не «умеет» отличать хорошие изменения от плохих. Он фиксирует состояние тома, файловой системы или базы целиком в один конкретный момент — своего рода фотографию всего диска. Откат к снапшоту технически означает «замени текущее состояние диска на то, что было зафиксировано тогда». У операции нет представления о семантике данных: она не знает, что часть строк в таблице orders — это легитимные заказы, а часть — испорченные миграцией нули. Для файловой системы или блочного устройства это просто байты.
Отсюда прямое следствие: окно между моментом снятия снапшота и моментом отката — это окно гарантированной потери, если специально не позаботиться о его сохранении. Неважно, чем вызван откат — багом в миграции, случайным DROP TABLE, шифровальщиком или человеческой ошибкой. Если откатываетесь на снапшот трёхчасовой давности, вы теряете все три часа целиком, а не только «плохую» часть этого периода. LVM thin-snapshot, ZFS snapshot, снимок диска в панели облачного провайдера, снапшот виртуальной машины в Proxmox — механизм везде разный, инвариант один и тот же: снапшот виртуалки — это не резервная копия в привычном смысле, а скорее чекпоинт, который откатывает всё сразу.
Второй нюанс, который часто упускают: три часа — это не абстрактная цифра, а прямое следствие частоты снятия снапшотов. Если бы снапшоты снимались не раз в сутки, а раз в час, худший случай был бы «потеряли час», а не «потеряли три». Частота снапшота напрямую определяет максимально возможный размер этого окна — к этому вернёмся ниже.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПрежде чем откатывать: попробуйте вытащить данные из окна потерь
Самая практичная вещь, которую можно сделать до отката, а не после него — это попытаться сохранить данные, накопленные между снапшотом и текущим моментом, отдельно от самого отката. Тогда откат возвращает систему в рабочее состояние, а сохранённые данные накатываются обратно вручную уже на чистую базу. Конкретные приёмы зависят от того, что у вас за система хранения.
Если это реляционная база с журналом транзакций (WAL/binlog). Это самый благодарный случай — журнал уже содержит все изменения за нужный период в структурированном виде. Прежде чем сносить текущее состояние, скопируйте журнал целиком в отдельное место:
# PostgreSQL — скопировать WAL-сегменты, начиная с момента снапшота
sudo -u postgres pg_basebackup --wal-method=stream -D /mnt/backup/wal-window -h localhost
# Либо, если архивация WAL уже настроена — просто забрать нужные файлы
cp /var/lib/postgresql/16/main/pg_wal/0000000100000A1F0000005* /mnt/backup/wal-window/
# MySQL/MariaDB — узнать позицию binlog на момент снапшота и текущую
mysqlbinlog --start-datetime="2026-08-27 09:00:00" \
--stop-datetime="2026-08-27 12:50:00" \
/var/lib/mysql/binlog.000042 > /mnt/backup/orders-window.sql
Если журнал сохранён, после отката на снапшот его можно проиграть заново через pg_waldump/восстановление PITR или через mysqlbinlog | mysql — но с фильтром, который выкинет именно те операции, что внесла сломанная миграция, и оставит легитимные записи клиентов. Это ручная и внимательная работа, а не одна команда — но именно она превращает «потеряли три часа» в «потеряли двадцать минут на разбор журнала».
Если журнала нет или доверия к нему нет (миграция могла повредить и его). Тогда единственный вариант — выгрузить нужные данные напрямую из текущего, ещё живого состояния до отката:
-- Выгрузить заказы за окно потерь в отдельный файл, пока текущая база ещё жива
COPY (
SELECT * FROM orders
WHERE created_at BETWEEN '2026-08-27 09:00:00' AND '2026-08-27 12:50:00'
AND total_amount > 0 -- отсечь то, что успела занулить сломанная миграция
) TO '/mnt/backup/orders-window.csv' WITH CSV HEADER;
Здесь придётся решать под давлением времени, какую часть данных за окно считать «достаточно чистой». Если критерий очевиден (как выше — нулевая сумма явно указывает на брак), выгрузка займёт минуты. Если неочевиден — иногда быстрее откатиться сразу и потом сверяться с внешним источником: письмами с подтверждением заказа, логами платёжного шлюза, событиями во внешней аналитике. Платёжный провайдер в этом смысле часто надёжнее собственной базы — у него независимая транзакционная запись, которую не затронула ваша миграция.
Если это файловое хранилище, а не база. Тот же принцип: перед откатом синхронизируйте директорию с изменениями за нужный период в отдельное место —
# Скопировать файлы, изменённые с 09:00, до отката тома
find /data/uploads -newermt "2026-08-27 09:00:00" -type f \
-exec cp --parents {} /mnt/backup/files-window/ \;
— а затем после отката вернуть их обратно поверх восстановленного состояния, проверив, что они не конфликтуют с уже существующими файлами тех же имён.
Важная оговорка: этот шаг стоит делать, только если сама система ещё в рабочем состоянии для чтения — то есть повреждение локализовано и не мешает выгрузке. Если авария такая, что читать текущие данные уже нельзя (диск умирает, база не запускается), извлекать нечего — придётся откатываться вслепую и закрывать окно потерь другими средствами: из логов приложения, из очереди сообщений, из внешних систем, куда данные успели продублироваться до отказа.
Практика на живом инциденте: как выглядело восстановление окна
Вернёмся к истории с 340 заказами. После отката на снапшот 09:00 команда не остановилась на «база снова работает» — восстановление легитимных заказов прошло в три шага.
- Определили границы окна. По логу приложения (не по базе — она уже была откачена) нашли первый и последний
order_id, созданные между 09:00 и 12:50, — 2847 записей всего, из них по логам платёжного шлюза подтверждено оплатой 340. - Сверили с независимым источником. Платёжный провайдер хранит собственную историю транзакций за 90 дней — выгрузили все успешные платежи за окно и сопоставили с
order_idиз логов приложения по сумме и времени. Это дало список именно тех заказов, по которым деньги реально прошли, — не то же самое, что «заказ был создан», потому что часть создаётся и бросается на этапе оплаты. - Накатили вручную через staging. Собрали INSERT-выражения по сопоставленному списку, прогнали их сначала на копии базы на отдельном сервере, проверили целостность (внешние ключи на клиентов, товары, склад), и только после этого применили на проде.
Весь процесс занял около четырёх часов — дольше, чем сам инцидент. Но альтернатива — сказать 340 клиентам, что их заказ «не найден в системе», — обошлась бы дороже, чем четыре часа работы одного инженера. Выяснилась и болезненная деталь: часть из этих заказов уже списала позиции со склада — сверять пришлось отдельно, потому что откат вернул и остатки склада к состоянию 09:00, задвоив часть списаний при повторном проведении заказа.
Частые снапшоты как способ сузить окно, а не устранить его
Если ручное восстановление окна невозможно или ненадёжно (данные не в базе с журналом, а в чём-то, откуда их сложно выгрузить точечно), единственный системный рычаг — снижать сам размер окна за счёт более частых снапшотов. Логика прямая: снапшот раз в сутки — потенциальная потеря до суток; снапшот раз в час — потенциальная потеря до часа; снапшот раз в пять минут — до пяти минут. Но у этого рычага есть своя цена, и её стоит считать честно, а не просто «давайте снимать почаще».
| Частота снапшота | Худшее окно потерь | Основная цена |
|---|---|---|
| Раз в сутки | до 24 часов | минимальная нагрузка на IOPS и место |
| Раз в 4 часа | до 4 часов | заметный рост потребления места при активном COW |
| Раз в час | до 1 часа | ощутимая деградация IOPS на copy-on-write снапшотах, больше объектов для ротации |
| Раз в 5-15 минут | до 15 минут | обычно оправдано только вместе с журналируемой репликацией, а не как отдельный механизм |
Copy-on-write снапшоты (LVM thin, ZFS, снапшоты большинства облачных провайдеров) не бесплатны с точки зрения производительности: каждый следующий снапшот добавляет слой отслеживания изменённых блоков, и чем их больше живёт одновременно, тем заметнее просадка на запись. Практический разбор того, во что это выливается в IOPS и в месте на диске, — в статье сколько снапшотов можно держать: цена каждого следующего в IOPS. Поэтому для критичных систем правильный ответ обычно не «снимаем снапшоты каждые пять минут», а «снапшоты остаются нечастыми, а короткое окно закрывается журналом транзакций и репликацией» — WAL в PostgreSQL или binlog в MySQL дают возможность точечного восстановления на произвольный момент времени без необходимости частить снапшотами. О том, как устроен этот журнал и почему он вообще нужен, — в статье что такое WAL и зачем писать всё дважды.
Разумный компромисс для большинства некритичных проектов — снапшоты раз в несколько часов плюс регулярный бэкап с ротацией, а не попытка героически закрыть окно потерь одной лишь частотой снапшотов. Для систем, где каждый потерянный заказ считается в деньгах напрямую (платёжный поток, биллинг, склад в реальном времени), журналируемое хранилище с PITR стоит закладывать с самого начала, а не после первого инцидента.
Что стоит настроить заранее, чтобы окно не пришлось закрывать вручную
Разбор конкретного инцидента подсказывает набор мер, которые снижают вероятность повторения и облегчают жизнь в следующий раз, если откат всё-таки понадобится:
- Включить WAL-архивирование или binlog в режиме ROW заранее, а не после первого инцидента — тогда точечное восстановление на произвольный момент между снапшотами становится штатной операцией, а не разовым геройством. Практика такого восстановления с pg_restore, частичным восстановлением и PITR разобрана в статье восстановление базы данных из бэкапа: практика.
- Держать независимый источник истины для критичных событий — платежи, отправка заказа, списание склада — вне основной базы: очередь сообщений, лог событий, внешний платёжный шлюз. Это ровно то, что спасло 340 заказов в разборе выше: без независимой сверки с провайдером платежей отличить «реальный заказ» от «созданный, но брошенный» было бы невозможно после отката.
- Формализовать чек-лист перед откатом, а не полагаться на то, что дежурный вспомнит про журнал транзакций в стрессе. Три пункта: (1) зафиксировать точное время последнего целостного снапшота и точное время принятия решения об откате, (2) если возможно — выгрузить или скопировать журнал/данные за это окно, (3) только после этого запускать сам откат.
- Проверить, что откат тестировался заранее на нейтральной среде, а не в первый раз во время аварии — иначе к потере данных за окно добавляется ещё и время на выяснение, как вообще работает merge снапшота на конкретном стеке хранения.
- Отдельно продумать связанные состояния, которые откатятся вместе с основной базой — остатки склада, счётчики, кэши, внешние вебхуки, которые уже ушли наружу до момента отката и не откатятся вместе с ней. В разборе выше именно рассинхронизация склада стала неожиданным побочным эффектом, который нашли не сразу.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли настроить снапшот так, чтобы он откатывал только «плохие» данные, а хорошие оставлял?
Нет, это противоречит самой природе снапшота — он фиксирует состояние блочного устройства или файловой системы целиком, без понятия о семантике данных внутри. Избирательное восстановление возможно только на уровне приложения или СУБД: точечные UPDATE/DELETE по журналу или ручная выгрузка нужных строк перед откатом, как описано выше.
Что если журнал транзакций (WAL/binlog) сам оказался повреждён вместе с базой?
Тогда единственный источник для восстановления окна — независимые от базы данные: логи приложения, события в очереди сообщений, записи внешних систем (платёжный шлюз, CRM, вебхуки). Именно поэтому критичные события стоит дублировать за пределы основной СУБД заранее, а не рассчитывать на журнал как на единственную страховку.
Стоит ли снимать снапшоты каждые пять минут, чтобы вообще не думать об этой проблеме?
Технически можно, но для большинства СУБД на copy-on-write хранилище это ощутимо бьёт по IOPS и месту при активной записи. Для действительно критичных систем разумнее не частить снапшотами, а включить журналируемое восстановление (PITR через WAL/binlog) — оно даёт точность до секунды без постоянной деградации производительности.
Как понять заранее, сколько часов данных вы рискуете потерять при откате на текущей инфраструктуре?
Возьмите время между двумя соседними плановыми снапшотами по расписанию — это и есть ваше худшее окно потерь при откате на «последний снапшот». Если это число вас не устраивает (например, сутки для системы с постоянным потоком заказов), это повод либо учащать снапшоты с оглядкой на их цену в IOPS, либо добавить журналируемое восстановление, либо и то и другое.
Нужно ли предупреждать клиентов, если после отката часть их заказов «потерялась»?
Да, и чем раньше, тем лучше — молчание в такой ситуации обычно дороже репутационно, чем сам инцидент. Параллельно с технической частью восстановления стоит запустить сверку по независимым источникам (платежи, письма-подтверждения) именно для того, чтобы явно и быстро связаться с теми, чей заказ действительно пострадал, а не с оговоркой «возможно, что-то потерялось».
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →