Крон отработал. Экзит-код ноль. Файл лежит в 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 байт.
