MAATRIX / Блог / База снята в три часа, файлы — в три сорок: это не одна копия

База снята в три часа, файлы — в три сорок: это не одна копия

MAATRIX

В крон-задачах два бэкапа: дамп базы в 03:00, синхронизация файлового хранилища в 03:40. Оба отрабатывают без ошибок, в мониторинге зелёные галочки, место на бэкап-сервере есть — кажется, что защита полная. Восстановление на тестовом стенде показывает другое: у части записей в базе нет файлов, у части файлов нет записей. Разберём, почему раздельное расписание почти гарантированно даёт такой разрыв и как снимать оба бэкапа как одну согласованную операцию, а не как два независимых процесса.

Почему «оба бэкапа есть» не значит «бэкап согласован»

Дамп базы, снятый в 03:00, — это состояние базы ровно на 03:00. Копия файлового хранилища, снятая в 03:40, — это состояние хранилища ровно на 03:40. Между этими моментами прошло сорок минут, и если приложение в это время продолжало работать — а оно продолжало, ночью тоже кто-то регистрируется, загружает аватар, оформляет заказ с фото товара, — то оба снимка описывают два разных, никогда не существовавших вместе состояния системы.

Восстановить оба бэкапа рядом друг с другом — не значит восстановить приложение таким, каким оно было в какой-то реальный момент. Это значит собрать гибрид: базу из 03:00 плюс файлы из 03:40. Такой гибрид не соответствует ни одному моменту в истории приложения, и именно поэтому в нём находятся нестыковки, которых не было бы, сними вы оба бэкапа синхронно.

Это тот же класс проблемы, что разбирается в материале про mysqldump без --single-transaction — там несогласованность возникает между таблицами внутри одной базы при последовательном снятии дампа. Здесь она возникает между двумя вообще разными системами хранения — базой данных и файловым хранилищем, — и решить её сложнее: у СУБД есть механизм транзакций и consistent read view, а у пары «база + файловое хранилище» общего транзакционного механизма нет по умолчанию — его нужно организовать вручную на уровне процесса бэкапа.

Два сценария рассинхронизации: запись без файла и файл без записи

Возьмём конкретный пример: пользователь загружает изображение к товару в 03:15, между двумя бэкапами. Что произойдёт при восстановлении, зависит от порядка снятия копий.

Сценарий 1: файл в базе есть, а на диске его нет. Если дамп базы снимается позже файлового бэкапа (например, база — в 03:40, файлы — в 03:00), то запись о новом изображении, созданная в 03:15, попадёт в дамп (он снят уже после загрузки), а сам файл — нет (файловый бэкап снят раньше загрузки). После восстановления в базе есть строка с image_url, указывающим на несуществующий файл. На фронтенде это разбитая иконка изображения, в API — 404 на конкретный ресурс, в логах — не сразу понятная ошибка чтения файла с диска. Это самый заметный и самый неприятный вариант, потому что ломается на глазах у пользователя.

Сценарий 2: файл на диске есть, а записи в базе нет. Обратная раскладка: файловый бэкап снимается позже дампа базы (файлы — в 03:40, база — в 03:00). Файл, загруженный в 03:15, попадает в файловый бэкап, а вот запись о нём в базу — нет, потому что дамп снят раньше загрузки. После восстановления на диске лежит файл, на который никто не ссылается. Внешне это менее заметно — ничего не ломается визуально, — но по факту это тихая потеря данных: связанная с файлом запись (например, сам заказ или профиль, к которому файл был приложен) в восстановленной базе отсутствует, и заново связать файл с записью, если её не осталось ни в базе, ни в логах, обычно уже нечем.

На практике оба сценария встречаются одновременно, потому что в окне между бэкапами обычно происходит не одна операция, а десятки: что-то создаётся, что-то удаляется, что-то перезаписывается. Направление конкретной нестыковки зависит от того, какой бэкап на какую операцию успел, а какой нет — предсказать заранее, каких записей это коснётся, нельзя.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему это не редкий случай, а гарантированный результат раздельного расписания

Часто расписание бэкапов разносят по времени намеренно — чтобы дамп базы и синхронизация большого файлового хранилища не боролись за диск и сеть одновременно. Логика с точки зрения нагрузки понятна, но именно этот разнос по времени и создаёт окно рассинхронизации: чем больше разница между стартом двух задач, тем больше изменений успевает произойти между ними.

Дело не только в разнице времени старта. Каждая из задач сама по себе может выполняться долго: дамп средней базы — минуты, синхронизация файлового хранилища на несколько сотен гигабайт по сети — часы. Реальное окно рассинхронизации — это не разница между 03:00 и 03:40 в расписании, а весь интервал от начала более быстрой задачи до конца более медленной. Если файловый бэкап идёт с 03:00 до 06:00, а дамп базы снимается в 03:40 где-то посередине, то согласованность нарушена относительно всего трёхчасового окна, а не сорока минут.

При любой ненулевой интенсивности записи и ненулевом окне между снятием копий вероятность нестыковки не «может быть», а есть всегда — вопрос только в количестве задетых записей. На тихом проекте с парой изменений в сутки это будет одна-две строки раз в несколько недель, их можно не заметить долго. На активном сервисе с постоянной загрузкой файлов пользователями — это стабильный поток нестыковок при каждом восстановлении, и с ростом базы проблема растёт вместе с ней, а не остаётся на прежнем уровне.

Ложное чувство защищённости: оба бэкапа зелёные, а восстановление — нет

Мониторинг бэкапов обычно проверяет, что джоб отработал и код возврата — ноль: файл дампа создан, синхронизация файлов завершилась без ошибок сети. Оба показателя зелёные, и складывается ощущение полной защиты — миф, разобранный отдельно в статье бэкапить нужно только базу данных: там показано, почему бэкапа одной базы недостаточно, если приложение хранит часть данных в файлах. Здесь проблема на шаг дальше: даже когда бэкапят и то, и другое, этого всё равно недостаточно, если оба снимка не привязаны к одному моменту времени.

Код возврата 0 у обеих задач ничего не говорит о согласованности между ними — это два независимых факта («дамп снят», «файлы скопированы»), и ни один monitoring-чек по умолчанию не сравнивает содержимое базы и файлового хранилища между собой. Единственный способ увидеть проблему — реально попробовать восстановиться из обеих копий на отдельном стенде и прогнать по восстановленным данным проверку на битые ссылки: пройтись по таблице с полями-путями к файлам и убедиться, что каждый файл существует на диске, и наоборот — что каждый файл в хранилище на что-то ссылается. Про то, зачем и как регулярно гонять именно такие тестовые восстановления, а не полагаться на факт «бэкап есть», — в материале проверка восстановления бэкапа раз в месяц.

Практический подход: одна согласованная операция вместо двух независимых процессов

Правильное направление — не «бэкапить базу и файлы почаще, чтобы уменьшить окно», а сделать так, чтобы у обоих бэкапов была одна общая точка согласованности во времени. Это можно организовать тремя способами, от самого простого до самого гибкого.

Короткая пауза записи. Приложение на несколько секунд или минут переводится в режим «только чтение» (feature-флаг, страница обслуживания для операций записи, временная блокировка на уровне API), после чего дамп базы и копирование файлового хранилища снимаются впритык друг к другу — без пользовательской записи между ними нет и окна рассинхронизации. Для многих проектов пауза записи на несколько секунд не критична: сайт продолжает отдавать контент на чтение, просто временно не принимает новые заказы или загрузки. Это самый простой в реализации вариант и хорошая отправная точка, если инфраструктура пока не готова к более сложным схемам.

Атомарный снапшот на уровне файловой системы, покрывающий оба тома сразу. Если данные СУБД и файловое хранилище лежат на одном ZFS-пуле (в разных датасетах), можно снять согласованные снимки нескольких датасетов одной атомарной операцией, без паузы приложения:

zfs snapshot -r tank/app@backup-2026-08-29-0300

Флаг -r создаёт снимок сразу для датасета и всех вложенных — tank/app/pgdata и tank/app/uploads получают снимки из одной транзакции файловой системы, то есть буквально из одного и того же момента времени. Дальше снимки нужно выгрузить на отдельное хранилище (zfs send), потому что сам по себе снапшот — не бэкап, а лишь способ получить согласованную точку на диске сервера; если сервер выйдет из строя целиком, снапшот пропадёт вместе с ним. Разница между снапшотом и бэкапом отдельно разобрана в статье снапшоты ZFS против бэкапа — рекомендуем прочитать её перед тем, как полагаться на снапшоты как на единственную копию.

Честная оговорка: снимок PostgreSQL, сделанный так, — это crash-consistent состояние, как если бы серверу внезапно отключили питание. Postgres спроектирован переживать это через replay WAL при старте, и на практике такой снимок поднимается корректно, но перед тем как полагаться на схему в проде, стоит один раз проверить восстановление именно из такого снапшота на тестовом сервере, а не считать это гарантией по умолчанию.

Согласованность через точку восстановления (PITR), а не через одновременный старт задач. Если файловый бэкап идёт долго и его неудобно синхронизировать по старту с дампом базы, можно развязать привязку по-другому: зафиксировать момент завершения файлового бэкапа и восстанавливать базу через point-in-time recovery ровно на этот момент, а не на момент старта своего собственного дампа. Для этого у СУБД должно быть включено непрерывное архивирование WAL (PostgreSQL) или бинлогов (MySQL). Файловый бэкап может идти сколько угодно медленно и не блокировать ничего — точная синхронизация происходит один раз, на этапе восстановления, когда вы точно знаете временную метку окончания файлового бэкапа и просите базу воспроизвести журнал именно до неё.

Пример на сервере: пауза записи плюс бэк-to-бэк снятие копий

Рабочий скрипт для варианта с короткой паузой, где файловое хранилище — локальная директория, а база — PostgreSQL:

#!/usr/bin/env bash
set -euo pipefail

APP_MAINT_FLAG=/var/www/app/storage/framework/down
BACKUP_DIR=/backup/$(date +%F)
mkdir -p "$BACKUP_DIR"

# приложение уходит в режим "только чтение" для операций записи
touch "$APP_MAINT_FLAG"
sleep 2   # дать текущим запросам на запись корректно завершиться

rsync -a --delete /var/www/app/storage/uploads/ "$BACKUP_DIR/uploads/"
pg_dump -U appuser --single-transaction appdb > "$BACKUP_DIR/appdb.sql"

# снова принимаем запись
rm -f "$APP_MAINT_FLAG"

echo "$(date -u +%FT%TZ)" > "$BACKUP_DIR/consistent_at.txt"

Порядок между rsync и pg_dump внутри паузы не критичен именно потому, что записи в это время нет вообще — окна для рассинхронизации не существует, а не просто «оно маленькое». Файл consistent_at.txt фиксирует точку согласованности — пригодится и для аудита, и для варианта с PITR, если решите на него перейти позже.

Для объектного хранилища вместо локального диска (S3-совместимый бакет) принцип тот же, но конкретные команды другие: включите версионирование бакета, а при восстановлении поднимайте объекты в состоянии «на момент времени T» через листинг версий с фильтром по дате, где T — та же временная метка, на которую восстановлена база. Дублирование целого бакета через rclone sync на каждый бэкап обычно избыточно и дорого по трафику — версионирование решает задачу точки времени без полного копирования.

ПодходПауза приложенияСложностьКогда уместен
Пауза записи + бэк-to-бэк снятиеСекунды-минутыНизкаяНебольшие и средние проекты, где кратковременный read-only не критичен
Атомарный снапшот ФС (ZFS)НетСредняяДанные БД и файлы на одном сервере/пуле, есть требование к нулевому даунтайму
PITR к общей точке времениНетВысокаяКрупные базы, долгий файловый бэкап, уже настроено архивирование WAL/бинлогов

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

У меня небольшой проект, оба бэкапа снимаются за пару минут — стоит ли вообще об этом беспокоиться?

Риск невелик, но не нулевой — он пропорционален интенсивности записи в окне между бэкапами. Для сайта-визитки с редкими правками это можно долго не замечать. Для интернет-магазина или сервиса с постоянной загрузкой файлов пользователями — стоит настроить согласованный процесс сразу, не дожидаясь первого болезненного восстановления.

Можно ли просто снимать бэкапы чаще, чтобы сократить окно рассинхронизации?

Это уменьшает вероятность и количество задетых записей, но не устраняет проблему как класс — окно всё равно есть, просто короче. Правильное направление — согласованная точка времени, а не более частый снимок несогласованных копий.

Что если файловое хранилище — S3-совместимый объектный сторедж, а не локальный диск?

Используйте версионирование бакета и восстанавливайте объекты на состояние «до временной метки T», где T синхронизирована с точкой восстановления базы (через PITR). Отдельно проверьте политику жизненного цикла бакета — если старые версии объектов удаляются лайфсайклом раньше, чем истекает срок хранения бэкапов базы, нужная версия файла может просто не дожить до момента восстановления.

Нужна ли такая же согласованность для кэша или пользовательских сессий?

Как правило нет — кэш и сессии не критичны к согласованности с базой, их можно потерять или пересобрать без последствий для целостности данных. Согласованность важна там, где есть перекрёстные ссылки между хранилищами: база и файлы, основная база и внешний поисковый индекс, база и очередь заданий.

А если у меня уже настроена репликация базы — она решает эту проблему?

Нет, репликация решает другую задачу — доступность и отказоустойчивость самой базы, она никак не связывает состояние базы с состоянием отдельного файлового хранилища. Реплика базы точно так же может быть согласована с файловым бэкапом или нет — вопрос синхронизации по времени остаётся тем же самым.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →