Утренняя проверка бэкапа: как узнать, что ночная копия действительно создалась
Ночью отработал cron, в логе не видно ошибок, но вы всё равно не знаете, лежит ли в хранилище рабочий файл бэкапа за эту ночь или там пусто, обрезано на середине, либо архив вчерашней давности. Проверять это вручную каждое утро — зайти по SSH, посмотреть директорию, сверить дату — рано или поздно надоест и превратится в «кажется, всё нормально» без реального взгляда на файл. Ниже — как заменить это автоматической проверкой размера и времени последнего бэкапа с алертом, если копия не появилась к ожидаемому часу, и почему такая ежедневная проверка дополняет, а не заменяет более глубокий ежемесячный тест восстановления.
Содержание
- Почему «cron запустился» и «бэкап создался» — разные утверждения
- Что именно проверять утром — и почему этого достаточно для быстрого чек-апа
- Скрипт: проверка размера и времени последнего файла
- Алерт, если бэкап не появился к ожидаемому времени
- Куда встроить проверку: cron, systemd timer, healthchecks.io
- Частые грабли утренней проверки
- Чем эта проверка НЕ является
Почему «cron запустился» и «бэкап создался» — разные утверждения
Планировщик cron честно докладывает только один факт: процесс был запущен в указанное время. Он ничего не знает о том, что этот процесс сделал дальше. Между «задача стартовала» и «в хранилище появилась годная копия за эту ночь» помещается несколько независимых точек отказа:
- Скрипт упал с ошибкой, но exit code потерялся. Классика — конвейер вида
pg_dump mydb | gzip > backup.sql.gz, гдеgzipвозвращает 0, даже еслиpg_dumpупал в середине с ошибкой доступа. Архив существует, размер ненулевой, но внутри — обрывок дампа. Разбор такого случая, когда это оставалось незамеченным месяцами, — в статье «скрипт бэкапа падал молча четыре месяца»: безset -o pipefailкод возврата конвейера берётся от последней команды, а не от той, что реально сломалась. - Задача выполнилась, но не успела закончиться до утра. Объём данных вырос, диск или канал стали медленнее, и бэкап, который раньше укладывался в час, всё ещё идёт в момент проверки. Файл есть, но неполный.
- Задача не запустилась вовсе. Сервер перезагрузили патчем безопасности и забыли, что crontab относится к другому пользователю, кончилось место в
/var/spool/cron— вариантов много, и все дают одинаковый результат: тишину. Тишина — это не отсутствие проблемы, а отсутствие сигнала о ней. - Файл создался, но пустой или почти пустой. Сброшенные переменные окружения, битый пароль к БД, недоступный S3-эндпоинт — скрипт создаёт файл нулевого размера и завершается с кодом 0, потому что для shell «создать пустой файл» — тоже успех.
Общий принцип прост: успешный код завершения процесса — необходимое, но недостаточное условие того, что копия действительно рабочая. Утренняя проверка не решает эту проблему полностью (для этого нужен тест восстановления), но она ловит подавляющее большинство перечисленных случаев за секунды — потому что все они, так или иначе, отражаются в двух простых атрибутах файла: его размере и времени создания.
Что именно проверять утром — и почему этого достаточно для быстрого чек-апа
Идея утренней проверки в том, чтобы не заглядывать внутрь архива (это задача более глубокой и медленной проверки), а сверять два дешёвых в получении факта:
- Время создания последнего файла бэкапа не старше ожидаемого окна. Если ночной бэкап должен завершаться к 04:00, а проверка в 08:00 видит файл с
mtimeвчерашнего дня — задача не запустилась, зависла или упала до записи файла. - Размер последнего файла в разумных пределах от привычного значения. Не абсолютное число, а отклонение от скользящего среднего за последние несколько дней. Резкий провал размера (архив в 40 раз меньше вчерашнего) почти всегда означает частичный или пустой дамп, даже если время создания в порядке.
Этого достаточно, чтобы отловить незапустившуюся задачу, зависшую задачу, упавший на середине процесс и пустой или подозрительно маленький файл — основную массу реальных отказов бэкапа. Чего проверка не ловит: архив нормального размера, созданный вовремя, но с логически повреждёнными данными внутри (например, дамп снят без --single-transaction посреди активной записи). Для этого нужна проверка содержимого — контрольные суммы, попытка распаковки, тестовый импорт — она разобрана в статье «как проверить, что бэкап рабочий, не восстанавливая всё» и её стоит запускать реже, например раз в неделю, поскольку она тяжелее по ресурсам и времени.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверСкрипт: проверка размера и времени последнего файла
Ниже — рабочий скрипт для типичного случая, когда бэкапы складываются локальными файлами в директорию (после чего синхронизируются во внешнее хранилище через rclone или аналог). Логика: найти самый свежий файл, сравнить его возраст с порогом, сравнить его размер со средним размером нескольких последних файлов.
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/pgdump"
PATTERN="*.sql.gz"
MAX_AGE_HOURS=26 # бэкап не старше 26 часов (запас на дрейф расписания)
MIN_SIZE_RATIO=50 # тревога, если файл меньше 50% от среднего за последние 5 копий
latest_file=$(find "$BACKUP_DIR" -maxdepth 1 -name "$PATTERN" -printf '%T@ %p\n' \
| sort -rn | head -1 | cut -d' ' -f2-)
if [[ -z "$latest_file" ]]; then
echo "FAIL: в $BACKUP_DIR нет ни одного файла по маске $PATTERN" >&2
exit 1
fi
age_hours=$(( ( $(date +%s) - $(stat -c %Y "$latest_file") ) / 3600 ))
if (( age_hours > MAX_AGE_HOURS )); then
echo "FAIL: последний бэкап $latest_file создан $age_hours ч назад (порог $MAX_AGE_HOURS ч)" >&2
exit 1
fi
latest_size=$(stat -c %s "$latest_file")
avg_size=$(find "$BACKUP_DIR" -maxdepth 1 -name "$PATTERN" -printf '%s\n' \
| sort -rn | head -5 | awk '{s+=$1; n++} END {if (n>0) print int(s/n); else print 0}')
if (( avg_size > 0 )); then
ratio=$(( latest_size * 100 / avg_size ))
if (( ratio < MIN_SIZE_RATIO )); then
echo "FAIL: последний бэкап $latest_size байт — это $ratio% от среднего ($avg_size байт)" >&2
exit 1
fi
fi
echo "OK: $latest_file, возраст ${age_hours}ч, размер $latest_size байт (${ratio:-100}% от среднего)"
exit 0
Для restic то же самое проще получить из встроенных метаданных снапшотов — не нужно парсить файловую систему вручную:
#!/usr/bin/env bash
set -euo pipefail
export RESTIC_REPOSITORY="s3:s3.example.com/backups"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
MAX_AGE_HOURS=26
latest_json=$(restic snapshots --latest 1 --json)
latest_time=$(echo "$latest_json" | python3 -c 'import json,sys; print(json.load(sys.stdin)[0]["time"])')
age_hours=$(( ( $(date +%s) - $(date -d "$latest_time" +%s) ) / 3600 ))
if (( age_hours > MAX_AGE_HOURS )); then
echo "FAIL: последний restic-снапшот от $latest_time, возраст ${age_hours}ч" >&2
exit 1
fi
# restic check --read-data-subset проверяет часть данных на целостность,
# но это уже тяжелее ежедневного чек-апа — раз в неделю через cron.
echo "OK: снапшот от $latest_time, возраст ${age_hours}ч"
Для borg аналогично, через borg info --json и поле archives[-1].time, либо короче — borg list --last 1 --format '{time}{NL}'.
Оба скрипта не заглядывают внутрь архива — это выход за рамки утренней проверки, их задача занимает секунды и не нагружает прод.
Алерт, если бэкап не появился к ожидаемому времени
Скрипт сам по себе бесполезен, если его вывод никто не читает. Есть два рабочих подхода, и лучше сочетать оба.
Подход 1 — dead man's switch через healthchecks.io или self-hosted аналог. Идея в том, что молчание само по себе становится сигналом: вы регистрируете «проверку» с ожидаемым периодом и окном, скрипт бэкапа (или отдельный проверочный скрипт) пингует URL после успешного завершения, а если пинга не было вовремя — сервис сам присылает алерт, без дополнительной логики на вашей стороне.
# в конце скрипта бэкапа, после успешной проверки размера/времени
curl -fsS -m 10 --retry 3 https://hc-ping.com/<ваш-uuid> > /dev/null
В настройках проверки на стороне healthchecks.io задаётся период (например, «раз в 24 часа») и допустимый грейс-период (например, 2 часа) — если пинг не пришёл в это окно, приходит уведомление в Telegram, email или куда угодно ещё. Подробная настройка, включая self-hosted вариант для тех, кто не хочет полагаться на внешний сервис, разобрана в статье «healthchecks.io: мониторинг cron-задач». Плюс этого подхода — не нужно поднимать собственный крон для проверки: сама неявка бэкапа и есть триггер.
Подход 2 — активная проверка с явной отправкой алерта. Отдельный cron-скрипт (тот, что выше) запускается каждое утро в фиксированное время и сам решает, слать алерт или нет:
#!/usr/bin/env bash
# /usr/local/bin/check-backup-and-alert.sh
if ! /usr/local/bin/check-backup.sh > /tmp/backup-check.log 2>&1; then
MSG="Проверка бэкапа провалена на $(hostname): $(cat /tmp/backup-check.log)"
curl -s -X POST "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
-d chat_id="${TG_CHAT_ID}" \
-d text="$MSG"
fi
# каждое утро в 08:00, после того как ночной бэкап точно должен был завершиться
0 8 * * * /usr/local/bin/check-backup-and-alert.sh
Частые ошибки именно этой части — токен бота истёк, чат не тот, сообщения блокируются лимитами Telegram при частой отправке. Стоит иметь в виду: если алерт шлёт тот же сервер, у которого могут быть проблемы (недоступна сеть, сервер лежит целиком), сообщение может не дойти именно тогда, когда нужнее всего — поэтому dead man's switch на внешнем сервисе (подход 1) надёжнее как последняя линия: он бьёт тревогу от отсутствия сигнала, а не полагается на то, что сломанный сервер сумеет пожаловаться сам.
Куда встроить проверку: cron, systemd timer, healthchecks.io
Три варианта запуска самой проверочной задачи, от простого к более надёжному:
| Вариант | Плюсы | Минусы |
|---|---|---|
| Обычный cron | Ничего дополнительно ставить не нужно, работает везде | Молчит, если сам cron не запустился; нет истории запусков |
systemd timer + OnFailure= unit | Лог в journalctl, OnFailure может сам вызывать алерт-unit | Чуть больше настройки, только для systemd-дистрибутивов |
| Healthchecks.io пинг из проверочного скрипта | Ловит и «проверка не прошла», и «проверка не запустилась вовсе» | Внешняя зависимость (или свой self-hosted инстанс) |
Практичная комбинация — запускать проверочный скрипт через cron или systemd timer, а в конце успешного прогона дополнительно пинговать healthchecks.io. Тогда молчание любого звена — бэкап не создался, проверочный скрипт упал, сервер целиком не поднялся после перезагрузки — одинаково приводит к алерту, потому что все эти случаи одинаково не дают пинга вовремя.
Если помимо утренней проверки нужен постоянный дашборд с метриками бэкапов (успешность запусков, история размеров, тренды по времени), утренняя проверка из этого текста хорошо встраивается как один из источников данных для него, а не заменяет его.
Частые грабли утренней проверки
- Порог возраста впритык к расписанию бэкапа. Если бэкап обычно завершается в 03:10, а порог стоит «не старше 24 часов» при проверке в 03:05 следующего дня — проверка регулярно ложно падает из-за минутного дрейфа. Оставляйте запас в несколько часов сверх обычного времени завершения.
- Проверяется локальная копия, а не та, что нужна при аварии. Если архив сначала пишется локально, а затем синхронизируется в S3, проверка «файл на диске свежий» ничего не говорит об успехе синхронизации. Проверяйте итоговое хранилище напрямую (
aws s3 ls,rclone lsl) или добавьте отдельный шаг после синхронизации. - Средний размер считается по слишком короткому окну. Если брать среднее по двум последним файлам, а один из них уже был битым, порог занижается и пропускает следующий сбой того же рода. Пять-семь последних успешных копий — более устойчивая база.
- Алерт шлётся, но никто не назначен его читать. Рабочий алерт в общий чат, где сообщения тонут в потоке, — то же самое, что отсутствие алерта. Закрепите, кто реагирует и в какой срок, иначе алерт превращается в фоновый шум.
- Порог размера без учёта контекста. Распродажа или наплыв регистраций законно увеличивает бэкап, а чистка старых логов — законно уменьшает. Процент порога стоит калибровать под конкретный сервис, а не копировать бездумно.
Чем эта проверка НЕ является
Важно держать границу этой практики отдельно от более глубокой ежемесячной проверки. Утренняя проверка — быстрый, дешёвый ежедневный чек-ап, который ловит «бэкапа нет» и «бэкап явно неполный». Она не проверяет:
- Что данные внутри архива логически целостны (нет оборванных транзакций, все таблицы на месте).
- Что архив реально разворачивается в рабочую систему — с правильными правами, без конфликтов версий ПО.
- Что ключ шифрования, если бэкап зашифрован, доступен и подходит именно к этой копии.
Эти вопросы закрывает полноценный тест восстановления в отдельное окружение — не каждое утро, а раз в месяц, с фиксацией фактического времени восстановления и проверкой данных по существу. Пошаговый сценарий такой проверки на 30 минут разобран в статье «проверка восстановления бэкапа раз в месяц». Правильная связка — обе практики вместе: утренняя ловит сбои быстро и почти без затрат, ежемесячная ловит то, что утренняя пропускает по своей природе.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько времени занимает настройка утренней проверки?
Сам скрипт — 15–30 минут на написание и тест под конкретную схему бэкапа, плюс 10–15 минут на алерт в Telegram или регистрацию на healthchecks.io. Дальше это работает без участия человека.
Нужно ли проверять каждый бэкап или достаточно одного, главного?
Если бэкапится несколько независимых сущностей (база, файлы, конфиги) — проверяйте каждую отдельно: они могут ломаться независимо друг от друга.
Что делать, если алерт сработал, а разбираться некогда прямо сейчас?
Зафиксировать факт и время, чтобы не потерять сигнал, и запустить бэкап вручную при первой возможности. Отложенный на несколько дней разбор алерта — частый способ незаметно остаться без рабочей копии дольше, чем кажется.
Можно ли обойтись без внешнего сервиса вроде healthchecks.io?
Можно, но тогда точка отказа — тот же сервер, который бэкапится: если он недоступен целиком, локальный алерт-скрипт тоже не запустится. Внешний dead man's switch устраняет именно этот сценарий.
Как часто перепроверять сам порог возраста и размера?
При заметном изменении расписания бэкапа или объёма данных — сразу. Иначе достаточно раз в несколько месяцев бегло свериться, что пороги ещё соответствуют реальности.
Достаточно ли утренней проверки без ежемесячного теста восстановления?
Нет. Она не гарантирует, что архив реально разворачивается и данные внутри корректны — лишь резко снижает число случаев, когда до ежемесячного теста доходит битая копия. Практики дополняют друг друга.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →