Крон отработал. Экзит-код ноль. Файл лежит в S3, размер похож на правду. Мониторинг зелёный. Всё говорит о том, что бэкап сделан успешно, вот только его никогда не пробовали восстановить.
Я лично сталкивался с этой проблемой дважды и один раз наблюдал её последствия в масштабах всего интернета. Это побудило меня создать инструмент, который регулярно восстанавливает бэкапы по расписанию и проверяет целостность данных. Ниже я расскажу, почему «бэкап снят» и «бэкап восстановится» — это два совершенно разных факта, а также как автоматическая проверка второго факта работает на практике.
31 января 2017 года, GitLab
Классика жанра, которую стоит перечитывать раз в год. Инженер GitLab, разбираясь с репликацией под нагрузкой, случайно выполнил rm -rf на каталоге данных продакшн-базы вместо реплики. Бывает. Для этого и существуют бэкапы.
Далее началось самое интересное. У GitLab было пять различных механизмов резервного копирования, но в момент инцидента выяснилось следующее:
регулярные
pg_dumpмолча падали из-за расхождения версий (утилита 9.2, а сервер 9.6). Эта проблема существовала неделями, поскольку код возврата команды никто не проверял должным образом, а соответствующие уведомления не доходили до адресатов;в S3, куда должны были складываться дампы, было пусто;
снапшоты дисков в Azure для серверов БД не были включены;
LVM-снапшоты делались, но раз в 24 часа.
В итоге спасла копия, сделанная для staging-окружения, шестичасовой давности. Это означало безвозвратную потерю примерно шести часов продакшн-данных, включая информацию о задачах, запросах на слияние и комментариях. Их отчёт об инциденте стоит прочитать целиком, но основная мысль выражена одной строкой из этого отчёта: backups are not working reliably — бэкапы работали ненадёжно, и это выяснилось только тогда, когда они понадобились.
Ключевое здесь не «инженер ошибся». Ключевое — пять механизмов выглядели работающими, пока их не пришлось использовать.
Четыре способа сломаться молча
За годы работы я собрал коллекцию типичных ситуаций, когда команда создания бэкапа выполняется успешно, но самого рабочего бэкапа в итоге нет. Все четыре сценария основаны на реальных случаях.
Дамп оборвался на середине. Диск кончился, сеть мигнула, контейнер прибило по OOM — а > уже создал файл. Файл есть, размер ненулевой, внутри — половина таблиц. pg_restore узнает об этом за вас, но только когда вы его запустите.
Файл нулевого размера. Любимый вариант: пайплайн вида pg_dump ... | gzip | aws s3 cp - ..., где упало первое звено, а код возврата проверили у последнего. В S3 аккуратными рядами лежат архивы по 20 байт. Задача cron при этом считается выполненной успешно.
Дрейф схемы. Дамп снимается, но за полгода в базе появились расширения, изменились типы, кто-то накатил несовместимую миграцию. Дамп формально валиден — и не накатывается на чистый сервер без деталей, которые в 3 часа ночи в день аварии вспоминаются плохо.
Исходная таблица была пуста. Бэкап корректно создаётся. Проблема в том, что он делается с реплики, которая три недели назад перестала синхронизироваться с мастером и теперь отдаёт пустые данные. Восстановление пройдёт идеально, но база данных останется пустой.
Общая черта всех этих четырёх сценариев: код возврата и проверка размера файла не выявляют ни одну из этих проблем. Единственный способ проверить все эти условия одновременно — это выполнить реальное восстановление и проверить целостность данных внутри.
А почему бы не восстанавливать руками раз в квартал?
Это распространённый и вполне логичный ответ, если у вас одна база данных и достаточно строгая дисциплина. Но, как всегда, не всё так идеально:
баз несколько, и ломаются они независимо;
сбои бэкапов происходят не по расписанию, а в самый неожиданный момент. Хотелось бы знать об этом сразу, а не ждать квартальных проверок;
и главное: ручная проверка не оставляет метрик. Сколько времени займёт восстановление — RTO — вы узнаёте в день аварии.
Следовательно, проверка должна быть автоматизированной, проводиться регулярно и в изолированной среде. Исходя из этих соображений, я разработал специальный агент.
Как устроен undump
undump — это агент, написанный на Go, который распространяется как один исполняемый файл или контейнер. Он работает внутри вашей собственной сети, непосредственно с вашими резервными копиями. Вся необходимая конфигурация содержится в одном YAML-файле:
targets: - name: "prod-billing" engine: "postgres" schedule: "0 * * * *" source: type: "s3" uri: "s3://backups/billing/latest.dump" access_key: "env:S3_ACCESS_KEY" # секретные ключи — только через переменные окружения secret_key: "env:S3_SECRET_KEY" checks: - type: "rowcount" table: "invoices" max_drop_pct: 10.0 - type: "freshness" table: "invoices" column: "created_at" max_age_hours: 24 - type: "sql_assert" query: "SELECT count(*) FROM invoices WHERE total < 0" expect: "0"
По расписанию для каждого таргета агент:
тянет свежий дамп из S3-совместимого хранилища (AWS S3, MinIO, что угодно);
поднимает эфемерный контейнер
postgres:18илиmysql:8через Docker Engine API;восстанавливает дамп — формат определяется по содержимому (custom-format
pg_dump, plain SQL,mysqldump);прогоняет проверки по восстановленной копии;
гарантированно удаляет контейнер после завершения всех операций, даже если процесс восстановления был прерван или агент получил сигнал SIGTERM;
регистрирует время, затраченное на восстановление (RTO), для каждого запуска. Это позволяет всегда знать реальное время, необходимое для восстановления, а не приблизительную оценку.
Вывод выглядит так:
$ undump run --config undump.yaml → pulling s3://backups/prod-2026-07-04.dump → restoring into ephemeral postgres:18 OK (42s) → rowcount +0.3% OK · freshness 2h OK → sql_assert 4/4 OK ✓ pass · rto 42s
Проверки
Процесс восстановления — это необходимый, но недостаточный шаг. Дамп, сделанный с пустой реплики, будет восстановлен без каких-либо ошибок. Поэтому поверх процесса восстановления реализованы следующие проверки данных:
rowcount — сравнивает количество строк в таблице с предыдущим запуском. Если количество строк уменьшилось более чем на заданный процент (
max_drop_pct), проверка считается неуспешной. Это позволяет выявлять пустые реплики или внезапно похудевшие таблицы.freshness — проверяет возраст записей в таблице на основе указанного столбца (
created_at). Эта проверка выявляет бэкапы, созданные с устаревших или неизменных источников данных.sql_assert — позволяет выполнять произвольные SQL-запросы и сравнивать их результат с ожидаемым значением. Любые известные вам закономерности в данных могут быть преобразованы в проверку.
Новая проверка в кодовой базе — это новый класс с интерфейсом Check, а не ветка в if/else: проверка получает соединение с восстановленной базой и возвращает результат.
Режимы
Команда undump run запускает агент в режиме демона. Для каждого таргета настраивается отдельный cron-интервал, и они работают независимо друг от друга. Если процесс восстановления занимает больше времени, чем заданный интервал, следующий запланированный запуск будет пропущен, чтобы избежать наложения задач. Команда undump check выполняет однократный проход по всем таргетам и завершается с соответствующим кодом выхода. Этот режим предназначен для интеграции с существующими системами планирования задач (cron, systemd-timer), позволяя использовать собственную систему оповещений. Агент полностью работоспособен и без использования облачных сервисов.
Вопросы безопасности: что передаётся наружу
Инструмент, который имеет доступ к резервным копиям продакшн-баз данных, должен предоставлять полную прозрачность в отношении того, куда отправляются данные. Архитектура undump построена на этом принципе. Процесс восстановления происходит исключительно внутри вашей локальной сети. Единственная информация, которая может быть отправлена наружу (если настроен соответствующий облачный эндпоинт), — это отчёт о статусе: pass или fail, время восстановления (RTO), метрики проверок и имена таргетов.
Содержимое самого дампа, строки данных или учётные данные никогда не передаются. Открытый исходный код агента позволяет любому желающему проверить, что именно отправляется в отчёте.
Облако: алерты и дашборд
Облачная часть добавляет то, что неудобно делать на хосте с агентом: историю прогонов, тренды RTO и размера дампа, и алерты в Telegram.
С алертами одно принципиальное решение: алертим только на смену состояния. Сломалось — одно сообщение с причиной («rowcount по invoices: просадка 18.4% при пороге 10%»). Починилось — одно сообщение о восстановлении. Никакого ежечасного спама одинаковым fail — иначе алерты мьютят через неделю, и всё возвращается к состоянию «узнаем в день аварии».
Что дальше
Сейчас поддерживаются PostgreSQL и MySQL с дампами в S3. Модель проверки — «восстанови и выполни ассерты по живой копии» — от движка не зависит, поэтому в планах MongoDB, Cassandra и другие СУБД, а также верификация физических бэкапов и PITR. Если у вас есть сценарий, которого не хватает, — расскажите в комментариях, роадмап сейчас формируется буквально из таких разговоров.
Итог
Бэкап, который ни разу не восстанавливали, — это не бэкап, а лотерейный билет. Проверять восстановлением руками — правильно, но не масштабируется и не оставляет метрик. Проверять автоматически — вполне подъёмная задача вокруг Docker Engine API; исходники агента открыты — github.com/UnDumpd/agent
Лайв демо - https://dash.undumpd.com/
А пока вы читали эту статью, где-то в S3 тихо лежит gzip на 20 байт.
Комментарии (10)

ch1971
20.07.2026 09:15А если гдето в ОС и/или контейнере заселилась какая нибудь сущность типа шифровальщика? Всё будет нормально восстанавливаться и работать правда ровно до того момента когда вас решать взять за яйца. Нормальную проверку ничего не заменит. Помню в старые времена это называлось "учения по ИБ" когда ставили сервер с пустым винтом и терминал с пустым винтом брали свежий бэкап и восстанавливали всё до полной работоспособности.

DatWork Автор
20.07.2026 09:15Полностью согласен, нормальные прогоны с восстановлением — нужны, и я этого не отрицаю. Автоматическая проверка — это не замена ручным прогонам, а то, что делается между ними. Ручные прогоны проводят раз в квартал или год, а бэкап успевает испортиться за неделю. Главное, чтобы к моменту тренировки (или реальной аварии) бэкап гарантированно восстановливается, а не просто «должен».
Что касается шифровальщика, есть два сценария, и они решаются по-разному:
Бэкапы портятся заранее. Часто бывает так: сначала тихо портят резервные копии, а потом уже бьют по продакшену, чтобы нечем было восстанавливаться. Ежедневная проверка восстановления выявляет это почти сразу: дамп перестает восстанавливаться — срабатывает оповещение за дни или недели до «часа Х», а не в момент, когда уже поздно.
Бэкапы уничтожают во время атаки. Здесь никакая проверка накануне не поможет. Спасут только неизменяемые и изолированные копии: S3 Object Lock, оффлайн-носители, отдельный аккаунт с другими ключами. Это дополнительная защита, которая нужна всегда, независимо от проверок.
Таким образом, проверка восстановлением помогает, когда «бэкап просто умер» (в том числе из-за злоумышленников), но не когда «всю инфраструктуру захватили». От этого защищают неизменяемость копий и сами тестовые прогоны .

ch1971
20.07.2026 09:15Одна неточность у Вас - шифровальщик не портит он шифрует при записи и расшифровывает при чтении. Поэтому может оказаться что уже давно всё зашифровано но всё прекрасно работает. Чтобы не светить производительностью он может шифровать одну страницу из например 10 всё равно каждая десятая испорченная страница это катастрофическая потеря данных.

DatWork Автор
20.07.2026 09:15Если я Вас правильно понимаю, то это хорошее замечание, но тут нужно различать логическое и физическое резервное копирование.
С логическим бэкапом (например, pg_dump) всё проще: данные расшифровываются при выгрузке. Если такой дамп сразу уходит с сервера, он останется рабочим, даже если потом шифровальщик срабаотает и расшифровку отключат. Моя агент не сможет обнаружить проблему заранее, но зато гарантирует работоспособность копии на момент атаки. То есть, вы не получите раннее предупреждение, но обеспечите непрерывность работы.
А вот с физическим бэкапом (pg_basebackup, снимки диска) вы абсолютно правы. Сырые данные копируются как есть, включая шифрование. Восстановление на заражённом сервере или его копии ничего не покажет, а проблема проявится только после отключения шифрования. В этом случае спасёт только восстановление на чистом сервере, отдельно от источника, а также отдельный контроль целостности данных.

ch1971
20.07.2026 09:15Основная опасность исходит то вобщем даже не от шифровальщика на рабочем сервере. Страшно когда шифровальщик работает именно на устройствах хранения бэкапов. Тогда если какие либо повреждения рабочей базы имеют место, даже не по чьемуто злому умыслу а просто например по сбою железа, то можно оказаться у разбитого корыта. И обычно если проводится кибератака то она проводиться осознанно, комплексно и по всем направлениям и атакующие скорее всего знают где бэкапы и пытаются определить в каких случаях их расшифровывать а в каких нет. На самом деле судя по последним инцидентам в крупных компаниях, которые были под успешной атакой длительное время но смогли восстановить работу довольно таки оперативно, у хакеров пока не получается взять под контроль всю систему архивации но быть первым в списке тех кого всё таки развалили вдрызг как то не хочется)
PS недавно кстати гдето был пост про компании которые обанкротились и закрылись после атак но это было не в России а по моему в Германии и были они не очень большими

DimNS
20.07.2026 09:15С нетерпением жду возможности указывать локальный файл, чтобы можно было сразу после создания бэкапа его проверить на восстановление

bigtrot
20.07.2026 09:15pg_dump не спасает от ситуации описанной выше, когда архив был создан, а авария произошла спусти 6 часов. Случилась потеря данных. Почему нет непрерывного архивирования? Опять в статье указано, что есть реплика, с которой снимается dump. Если есть реплика, что мешает реализовать непрерывное архивирование?
cher11
Классика.
Люди делятся на три типа: те, кто еще не делает бэкапы; те, кто уже делает бэкапы; и те, кто проверяет, что бэкапы корректно восстанавливаются.