Бэкапы шли год и оказались нерабочими: как это выясняется
Сервер лёг, база не поднимается, и единственный вариант — восстановиться из бэкапа. Вы разворачиваете последний архив и понимаете: он пустой. Или открывается, но там данные полугодовой давности. При этом в логах cron все эти месяцы стояло аккуратное Backup completed successfully. Это не гипотетическая история для запугивания — это типичный сценарий, который повторяется на серверах снова и снова, потому что «бэкап выполняется» и «бэкап работает» — два разных утверждения, и между ними никто не проверял разницу.
Содержание
- Момент истины: что происходит при первом реальном восстановлении
- Почему "успешный" бэкап годами был бесполезным: четыре типовых причины
- Корень проблемы: exit code 0 — это не про пользу бэкапа
- Что проверить прямо сейчас, если вы не уверены в своих бэкапах
- Единственная настоящая проверка: пробная реставрация на тестовый сервер
- Автоматическая проверка вместо ручной: контрольные суммы, размер, healthchecks
Момент истины: что происходит при первом реальном восстановлении
Собственно факт аварии — не самое страшное. Страшное начинается через 10 минут после неё, когда вы запускаете tar -xzf backup-latest.tar.gz или mysql < dump.sql и сталкиваетесь с одним из трёх вариантов:
- Архив пустой или почти пустой. Файл физически существует, весит несколько килобайт вместо гигабайт, распаковывается без ошибок — просто внутри почти ничего нет.
- Архив не разворачивается.
tar: Unexpected EOF in archive,mysql: ERROR 1064посреди дампа, битый gzip — процесс создания архива оборвался на середине, но программе резервного копирования об этом никто не сообщил. - Архив разворачивается, но данные там старые. Не сегодняшние и не вчерашние — а такие, какими база была несколько месяцев назад, до того как что-то в процессе бэкапа сломалось молча.
Ни один из этих трёх случаев не был виден по логам планировщика. Задача стартовала, что-то записала на диск или в S3-совместимое хранилище, завершилась с кодом 0, и cron (или systemd timer) добросовестно отчитался: всё хорошо. Мониторинг, если он вообще стоял, смотрел только на факт запуска задачи и код возврата — а не на содержимое результата.
Почему "успешный" бэкап годами был бесполезным: четыре типовых причины
Когда разбираешь такие инциденты постфактум, причина почти всегда одна из четырёх — и все четыре объединяет то, что скрипт бэкапа технически отработал без ошибок.
1. Путь к данным изменился после миграции, а бэкап-скрипт — нет.
Классика: полгода назад данные лежали в /var/lib/mysql, потом перенесли инстанс на новый диск, поменяли datadir в my.cnf на /data/mysql, приложение переключили — а cron-скрипт бэкапа как архивировал /var/lib/mysql, так и продолжил. Директория осталась на диске (пустая или с остатками старых файлов), tar её честно упаковывает, exit code — 0. Никто не сверил, что путь в скрипте бэкапа и реальный путь к рабочим данным — это одно и то же после переезда.
То же самое случается с Docker: volume пересоздали под другим именем при обновлении docker-compose.yml, а скрипт бэкапа продолжает бэкапить старый volume по старому пути или ID, который либо не существует, либо давно не пишется приложением.
2. Дамп базы данных снимался при недоступной БД — и записывал пустой файл без ошибки.
Скрипт вызывает mysqldump или pg_dump, но в момент запуска задачи сама СУБД была недоступна: перезагружался контейнер, шло обновление версии, сеть до отдельного инстанса БД моргнула, либо просто не хватило прав доступа после смены пароля. Многие обвязки вокруг дампа устроены так: команда дампа падает с ошибкой, но скрипт-обёртка ловит только код завершения архиватора (gzip, tar), который жмёт что получил — даже если получил пустой поток. mysqldump: Got error: 2003: Can't connect to MySQL server улетает в лог, который никто не читает, а файл dump.sql.gz на диске появляется, весит пару сотен байт (это сжатый пустой файл или служебные строки без единой таблицы) — и cron получает 0, потому что упал не сам архивирующий процесс, а только шаг перед ним, если pipeline собран без set -o pipefail.
3. Ротация удаляла старые копии быстрее, чем накапливались новые рабочие.
Скрипт ротации (find /backups -mtime +7 -delete или подобный) настроен агрессивнее, чем реальная частота успешных бэкапов. Если реальные удачные копии появляются раз в месяц (а остальные попытки по факту не создают ничего полезного — см. пункты 1 и 2), а ротация чистит всё старше 7 дней, то момент, когда нужно восстановиться, застаёт вас с единственной свежей, но битой копией — предыдущие рабочие уже удалены. Ротация должна быть завязана не на календарь, а на подтверждённо валидные копии.
4. Место назначения бэкапа само не проверялось на доступность.
Бэкап должен уезжать на другой сервер, в объектное хранилище или хотя бы на отдельный физический диск — а по факту rsync или rclone пишет в примонтированную по SSHFS/NFS директорию, которая в какой-то момент отвалилась и превратилась в обычную локальную папку на переполненном системном разделе. Команда синхронизации всё равно отчитывается об успехе (файл-то создан — просто не там, где вы думали), а по факту резервная копия физически лежит на том же диске, что и оригинал. Если этот диск умирает целиком, бэкап умирает вместе с ним.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверКорень проблемы: exit code 0 — это не про пользу бэкапа
Все четыре сценария выше объединяет одна вещь: код возврата процесса резервного копирования отвечает только на вопрос «команда завершилась без падения?». Он ничего не говорит о том, что именно попало в архив, актуальны ли данные, разворачивается ли архив обратно и лежит ли он там, где вы рассчитываете. Это разные уровни проверки, и большинство систем бэкапа на серверах покрывают только первый.
Мониторинг вида «задача из cron отработала» (обычный cron-алерт, healthchecks.io без проверки содержимого, systemd OnFailure= только на упавший unit) ловит падение процесса, но не ловит «процесс отработал успешно, но бесполезно». Это ловушка: чем стабильнее выглядит зелёный статус в системе мониторинга, тем меньше поводов туда заглянуть — и тем дольше живёт нерабочий бэкап, пока не наступит момент реального восстановления.
Что проверить прямо сейчас, если вы не уверены в своих бэкапах
Прежде чем городить автоматизацию, стоит один раз вручную пройтись по контрольному списку — это займёт полчаса и часто вскрывает проблему до того, как она станет аварией:
# Размер последних N архивов — если он скачет в разы без видимой причины
# или почти не растёт при растущей базе, это повод присмотреться
ls -lh /backups/*.tar.gz | tail -10
# Дата модификации файлов внутри самого архива данных,
# а не дата создания файла архива на диске
tar -tzvf backup-latest.tar.gz | head -20
# Быстрая проверка целостности gzip без полной распаковки
gzip -t backup-latest.tar.gz && echo "OK" || echo "CORRUPT"
# Для дампа БД — посчитать реальное число таблиц/строк внутри,
# а не просто убедиться, что файл не нулевого размера
grep -c "^INSERT INTO" dump.sql
zcat dump.sql.gz | grep -c "CREATE TABLE"
Если grep -c "CREATE TABLE" возвращает 0 на дампе, который якобы бэкапит боевую базу с полусотней таблиц — вот он, тот самый нерабочий бэкап, который может идти месяцами до первой попытки восстановления.
Отдельно стоит свериться с путями в самом скрипте бэкапа:
# Что реально бэкапится по мнению скрипта
grep -E 'SOURCE_DIR|DATADIR|BACKUP_PATH' /usr/local/bin/backup.sh
# Что реально существует и куда пишет приложение
mysql -e "SHOW VARIABLES LIKE 'datadir';"
docker inspect --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ println }}{{ end }}' <container>
Если пути в скрипте бэкапа и пути, которые вернули эти команды, различаются — вы нашли причину №1 из списка выше без единой пробной реставрации.
Единственная настоящая проверка: пробная реставрация на тестовый сервер
Всё, что описано выше, — диагностика по косвенным признакам. Она находит явный брак, но не гарантирует, что архив реально восстановит рабочую систему: файл может быть не пустым, проходить проверку gzip и содержать нужные таблицы, но всё равно не подняться из-за несовместимости версии СУБД, битых индексов внутри дампа или отсутствия части зависимых файлов (конфигов, ключей шифрования, файлов сессий).
Поэтому единственный способ по-настоящему знать, что бэкап рабочий, — периодически (не разово, а по расписанию) поднимать его на отдельном тестовом сервере или во временной виртуалке и проверять, что сервис реально стартует на восстановленных данных:
# Пример: разворачиваем дамп на отдельном тестовом инстансе MySQL/MariaDB
mysql -h test-restore-host -u restore_user -p restore_db < dump.sql
# Проверяем не факт импорта без ошибок, а реальные данные
mysql -h test-restore-host -e "SELECT COUNT(*) FROM restore_db.orders;"
mysql -h test-restore-host -e "SELECT MAX(created_at) FROM restore_db.orders;"
Второй запрос важнее первого: если COUNT(*) разумный, а MAX(created_at) показывает дату полугодовой давности — это ровно тот сценарий из начала статьи: архив разворачивается, но бэкапит не то, что должен, уже давно.
Для файлового бэкапа логика та же: разворачиваете архив на чистом сервере и пытаетесь запустить приложение на этих данных, а не просто убеждаетесь, что tar -x прошёл без ошибок:
# На тестовом сервере
tar -xzf backup-latest.tar.gz -C /srv/restore-test/
docker compose -f /srv/restore-test/docker-compose.yml up -d
curl -sf http://localhost:8080/health || echo "СЕРВИС НЕ ПОДНЯЛСЯ ИЗ БЭКАПА"
Частота таких пробных восстановлений зависит от критичности данных: для боевой базы, от которой зависит бизнес, разумно делать это не реже раза в месяц; для менее критичных сервисов — хотя бы раз в квартал. Дешевле всего пробную реставрацию делать на отдельном небольшом сервере или временном VPS, который поднимается перед проверкой и гасится сразу после неё — так расходы на инфраструктуру для этой задачи остаются минимальными, а не размазанными на постоянно работающую машину.
Автоматическая проверка вместо ручной: контрольные суммы, размер, healthchecks
Ручную пробную реставрацию невозможно и не нужно делать каждую ночь — но часть проверок стоит встроить в сам скрипт бэкапа, чтобы он переставал молчать об аномалиях:
#!/bin/bash
set -euo pipefail # без pipefail ошибка dump до архиватора останется незамеченной
BACKUP_FILE="/backups/db-$(date +%F).sql.gz"
MIN_SIZE_KB=1024 # ориентир: возьмите реальный размер вашей последней рабочей копии
mysqldump --single-transaction mydb | gzip > "$BACKUP_FILE"
ACTUAL_SIZE=$(du -k "$BACKUP_FILE" | cut -f1)
if [ "$ACTUAL_SIZE" -lt "$MIN_SIZE_KB" ]; then
echo "ALERT: backup $BACKUP_FILE too small ($ACTUAL_SIZE KB)" >&2
exit 1
fi
# Контрольная сумма для отслеживания "бэкап есть, но не меняется" —
# если хэш не менялся N дней подряд при растущей базе, это тоже сигнал
sha256sum "$BACKUP_FILE" >> /backups/checksums.log
# Сообщаем внешнему сторожу об успехе — если этот пинг не придёт,
# внешний сервис сам поднимет тревогу по таймауту
curl -fsS -m 10 --retry 3 https://hc-ping.com/<ваш-uuid>
Важное отличие такого пинга (например, через healthchecks.io) от обычного мониторинга cron: сторожевой сервис бьёт тревогу, если пинг не пришёл вовремя, а не только если скрипт явно упал — это ловит зависшие и молча не запустившиеся задачи, что частично закрывает проблему из пункта про недоступную БД. Подробнее про такую схему разобрано в статье про мониторинг cron-задач через healthchecks.io — она не подменяет пробную реставрацию, но резко сокращает время до обнаружения, что бэкап вообще перестал отрабатывать штатно.
Если для восстановления данных используется отдельная утилита вроде BorgBackup или restic, у них есть встроенные команды проверки целостности архива (borg check, restic check), которые стоит гонять по расписанию отдельно от самого создания бэкапа — они проверяют не факт создания снапшота, а его читаемость, что ловит часть повреждений раньше, чем до них дойдёт очередь при реальном восстановлении.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Если exit code 0 ничего не гарантирует, зачем вообще на него смотреть?
Затем, что это первый и самый дешёвый уровень проверки — он ловит явные падения процесса. Проблема не в том, что на него смотрят, а в том, что им ограничиваются: код возврата нужно дополнять проверкой размера/контрольной суммы и периодической пробной реставрацией, а не заменять их.
Как часто на самом деле нужно проверять бэкап пробным восстановлением?
Единого числа нет — зависит от критичности данных и от того, как часто они меняются. Для боевой базы, простой которой стоит бизнесу денег, разумный ориентир — не реже раза в месяц; для второстепенных сервисов подойдёт раз в квартал. Ключевой принцип: интервал проверки не должен быть длиннее, чем вы готовы потерять данных при аварии.
Что делать, если тестового сервера для пробной реставрации нет?
Поднять временный VPS специально под проверку и погасить его сразу после — это дешевле, чем держать отдельную машину постоянно включённой, и снимает главное возражение «негде проверять».
Помогает ли резервное копирование в несколько мест сразу от описанных проблем?
Частично — от физической потери диска с бэкапом (причина №4) помогает, но не спасает от причин №1-3: если скрипт бэкапит не тот путь или пустую базу, он с одинаковым успехом разложит бесполезные копии хоть в одно хранилище, хоть в три.
Нужно ли хранить несколько версий бэкапа вместо одной последней?
Да, и это прямо связано с проблемой ротации: если хранится только последняя копия, а именно она оказалась битой, откатываться уже не на что. Держите минимум 2-3 предыдущие точки восстановления, даже если место на диске под них стоит отдельных денег.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →