Бэкап баз данных без остановки сервиса
Продакшн-база принимает записи круглосуточно, а бэкап нужен уже сегодня ночью. Соблазн простой: остановить сервис на пять минут, скопировать файлы, запустить обратно. Но простой — это деньги и нервы, а если просто скопировать файлы БЕЗ остановки, есть шанс получить копию, которая на вид цела, но при восстановлении рассыпается на противоречивые данные. Разберём, почему это происходит и как сделать бэкап работающей базы правильно.
Содержание
- Почему cp и rsync на живой базе — это лотерея
- Как СУБД сама даёт консистентный снимок: дамп с изоляцией транзакций
- Специализированные инструменты физического бэкапа: Percona XtraBackup и pgBackRest
- Когда даже дамп — нагрузка: бэкап с реплики
- Снапшоты диска: почему нужен fsync/checkpoint до, а не после
- Практика: тестируйте восстановление, а не доверие к инструменту
Почему cp и rsync на живой базе — это лотерея
База данных на диске — это не один файл, а набор файлов и структур, которые должны оставаться согласованными друг с другом в каждый момент времени. У MySQL/MariaDB на InnoDB это табличные .ibd-файлы, общий табличный контейнер ibdata1, файлы redo-лога ib_logfile*. У PostgreSQL — каталог base/ с файлами данных, WAL-сегменты в pg_wal/, служебные файлы вроде pg_control. Утилиты cp и rsync копируют файлы последовательно, один за другим, и каждый файл — в своё, отдельное мгновение.
Пока копирование идёт, база продолжает писать. Представим: в момент t1 скопирован файл таблицы orders, в момент t2 — файл таблицы order_items. Между t1 и t2 прошла транзакция, которая добавила заказ и его позиции. Результат: в копии orders эта транзакция уже есть, а в order_items — ещё нет. После восстановления это не ошибка чтения, а тихо испорченные данные: заказ без позиций, внешний ключ в никуда, отчёты с неверными суммами. Ситуация усугубляется, если параллельно копируется WAL/redo-лог — он может не совпасть с состоянием файлов данных, и штатный механизм восстановления при старте либо не сможет докатить журнал до согласованного состояния, либо докатит его не полностью.
Отдельная проблема — copy-on-the-fly отдельной страницы данных. rsync читает файл блоками; если СУБД в этот момент дозаписывает страницу (например, 16 КБ InnoDB-страницу), можно скопировать её наполовину записанной. Это классический сценарий «torn page» — на диске в бэкапе окажется страница, которая никогда не существовала в согласованном виде ни до, ни после транзакции. Обычные файловые утилиты понятия не имеют о внутренней структуре СУБД и не могут этого предотвратить — они просто не для этого созданы.
Вывод простой: cp/rsync по живым файлам БД — это не бэкап, а фиксация случайного, потенциально противоречивого среза. Работает он или нет, вы узнаете только в момент восстановления — то есть тогда, когда откатывать назад уже поздно.
Как СУБД сама даёт консистентный снимок: дамп с изоляцией транзакций
Правильный путь — не трогать файлы напрямую, а попросить саму СУБД отдать данные через встроенный механизм экспорта. Именно для этого существуют pg_dump и mysqldump — оба используют внутреннюю транзакционную изоляцию, чтобы зафиксировать согласованный снимок данных на момент старта дампа, при этом не блокируя новые записи от других клиентов.
Для PostgreSQL pg_dump работает поверх MVCC (multi-version concurrency control) — движка, на котором и так строится вся работа с параллельными транзакциями в Postgres. В начале работы pg_dump открывает одну транзакцию с уровнем изоляции REPEATABLE READ и читает данные так, как они выглядели в момент её старта, — все более поздние изменения дамп просто не видит, но они спокойно продолжают записываться в базу параллельно:
pg_dump -U postgres -Fc -f /backup/mydb_$(date +%F).dump mydb
Флаг -Fc — это формат custom, компактный и пригодный для выборочного восстановления через pg_restore. Для кластера целиком (все базы + роли) — pg_dumpall, но учтите, что она не умеет параллелизм и на больших кластерах будет медленнее по сравнению с pg_dump по отдельным базам.
Для MySQL/MariaDB на InnoDB аналогичную консистентность даёт флаг --single-transaction у mysqldump. Он открывает одну транзакцию REPEATABLE READ перед началом выгрузки — все таблицы читаются в рамках этого единого снимка, без блокировки таблиц для записи:
mysqldump --single-transaction --routines --triggers \
-u backup_user -p mydb > /backup/mydb_$(date +%F).sql
Важная оговорка: --single-transaction даёт консистентность только для транзакционных таблиц (InnoDB). Если в базе остались таблицы MyISAM, для них механизм не работает — MyISAM не поддерживает транзакции, и mysqldump в этом случае вынужден либо блокировать такие таблицы (--lock-tables), либо смириться с их несогласованностью. На практике это ещё один повод мигрировать с MyISAM на InnoDB, если такие таблицы ещё остались в проекте.
Логический дамп — надёжный базовый вариант для баз, где полный дамп укладывается в разумное окно (условно говоря, от нескольких минут до пары часов на базах в десятки-сотни гигабайт, конкретика зависит от диска, сети и схемы). Он читаемый, портируемый между версиями СУБД и не требует специального инструментария для восстановления — только psql или mysql.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверСпециализированные инструменты физического бэкапа: Percona XtraBackup и pgBackRest
Логический дамп читает и сериализует каждую строку — на действительно больших базах это ощутимая нагрузка на CPU и I/O, а восстановление из SQL-дампа означает заново проиграть все INSERT, что может занять часы. Физический бэкап работает иначе: он копирует сами файлы данных, но делает это так, чтобы результат оставался консистентным, — параллельно отслеживая и подтягивая изменения из журнала транзакций, которые случились уже во время копирования.
Percona XtraBackup — стандарт для «горячего» физического бэкапа MySQL/MariaDB на InnoDB. Он копирует табличные файлы напрямую, а параллельно непрерывно читает redo-лог и записывает в бэкап все изменения, произошедшие за время копирования. В конце процесса XtraBackup «докатывает» эти изменения (prepare), приводя набор файлов к состоянию, идентичному тому, что получилось бы при мгновенном снимке на момент завершения бэкапа — без блокировки таблиц на запись на всё время копирования:
xtrabackup --backup --target-dir=/backup/xtrabackup_$(date +%F) \
--user=backup_user --password=...
xtrabackup --prepare --target-dir=/backup/xtrabackup_$(date +%F)
pgBackRest решает ту же задачу для PostgreSQL: физическое копирование каталога данных вместе с непрерывным архивированием WAL, встроенная проверка контрольных сумм страниц (page checksums), инкрементальные и дифференциальные бэкапы, параллельное сжатие. В отличие от самописного скрипта поверх pg_basebackup, pgBackRest сразу даёт удобное управление ретеншеном и восстановление на произвольную точку времени (point-in-time recovery) через архив WAL.
Оба инструмента снимают с вас заботу о фазах «заморозки» и докатки журнала — это их основная работа. Но физический бэкап переносим только между машинами с одинаковой архитектурой и, как правило, той же мажорной версией СУБД — в отличие от логического дампа, который проще перенести между версиями или даже между похожими СУБД.
Когда даже дамп — нагрузка: бэкап с реплики
На базе в сотни гигабайт и с высокой интенсивностью записи даже консистентный дамп или физический бэкап создаёт заметный I/O и CPU в момент снятия — на боевой машине это может выражаться в просевших задержках ответа прямо в рабочие часы. Решение — снимать бэкап не с продакшн-базы, а с её реплики, поднятой специально для этой роли.
Идея простая: реплика через потоковую репликацию (PostgreSQL streaming replication, MySQL/MariaDB async или semi-sync репликация) постоянно получает и применяет тот же поток изменений, что и мастер, и в любой момент, когда репликация не отстаёт, содержит копию данных, эквивалентную мастеру. Бэкап-процесс — pg_dump, pg_basebackup, xtrabackup или mysqldump --single-transaction — запускается против реплики, а не мастера:
- продакшн-база вообще не тратит ресурсы на процесс бэкапа;
- процессу бэкапа не нужен прямой доступ к боевой базе — только к выделенной реплике, что упрощает изоляцию по сети и правам доступа;
- при живой отдельной реплике вы заодно получаете второй канал для отказоустойчивости, а не только для бэкапов.
Здесь важно главное ограничение подхода: бэкап с реплики консистентен относительно данных реплики на момент снятия, а не мастера. Если репликация отстаёт (высокая нагрузка на запись, медленная сеть, долгий запрос-читатель, который держит транзакцию на реплике), бэкап отражает более старое состояние, чем текущее на мастере. Перед снятием бэкапа стоит проверять лаг явно:
-- PostgreSQL, на реплике
SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag;
-- MySQL/MariaDB, на реплике
SHOW REPLICA STATUS\G
-- смотреть поле Seconds_Behind_Source (Seconds_Behind_Master в старых версиях)
Если лаг выше приемлемого порога — например, больше нескольких минут для базы, где актуальность бэкапа критична, — разумнее подождать или временно поставить дополнительную запись в очередь, чем зафиксировать заведомо устаревший снимок.
Снапшоты диска: почему нужен fsync/checkpoint до, а не после
Снапшот на уровне файловой системы или диска целиком (LVM, ZFS, снапшот виртуального диска в Proxmox, снапшот EBS-тома в облаке) — ещё один рабочий подход, особенно для очень больших баз, где логический дамп занимает неприемлемо долго. Но у снапшота есть фундаментальное ограничение: он ничего не знает о внутренней структуре СУБД. Снапшот фиксирует состояние блоков на диске на конкретный момент — это «crash-consistent» состояние, то есть ровно то же самое, что получилось бы, выдернув питание сервера в этот момент.
СУБД проектируются так, чтобы уметь восстанавливаться после именно такого сценария — через журнал упреждающей записи (WAL/redo-лог): при старте после «грязного» выключения база проигрывает журнал и докатывает незавершённые операции до согласованного состояния. Именно поэтому crash-consistent снапшот в принципе может быть безопасен для СУБД — но только при соблюдении двух условий.
Первое условие: файлы данных и журнал должны попасть в снапшот одновременно, атомарно. Если данные лежат на одном томе, а WAL — на другом, и снапшоты снимаются последовательно (сначала один том, потом другой с задержкой), согласованность между ними не гарантирована — это тот же класс проблемы, что и с rsync, просто на уровне томов, а не файлов. Снимайте оба тома одной операцией (LVM group snapshot, единый снапшот виртуального диска, снапшот на уровне ZFS-пула, а не отдельного датасета) либо не разносите данные и журнал по разным томам без крайней необходимости.
Второе условие — не полагаться на то, что грязные страницы из файлового кеша ОС успели улечься на диск сами. Перед снапшотом стоит явно сбросить и приостановить запись:
# для файловой системы под данными БД
fsfreeze -f /var/lib/postgresql
lvcreate --size 5G --snapshot --name pgdata_snap /dev/vg0/pgdata
fsfreeze -u /var/lib/postgresql
fsfreeze -f останавливает новые записи на файловую систему и сбрасывает всё, что накопилось в кеше, непосредственно на диск — снапшот в этот момент фиксирует чистое, без незавершённых операций записи, состояние. Пауза обычно занимает доли секунды — время самого lvcreate --snapshot, а не время копирования данных.
Дополнительно для PostgreSQL можно обернуть снапшот в pg_backup_start()/pg_backup_stop() — они не блокируют запись, но фиксируют момент начала и конца в WAL, что упрощает последующее point-in-time восстановление именно с этого снапшота. Для MySQL/MariaDB аналог — кратковременный FLUSH TABLES WITH READ LOCK непосредственно перед снапшотом (не путать с постоянной блокировкой на время всего копирования, как при наивном подходе — здесь блокировка держится секунды, ровно до момента lvcreate).
Мораль раздела: снапшот файловой системы сам по себе не гарантирует консистентность данных СУБД внутри — гарантию даёт связка «снапшот всех томов атомарно» + «явный сброс на диск / freeze перед снимком» + «способность движка докатить журнал при восстановлении». Уберите любое из трёх звеньев — и получите ровно ту же лотерею, что с cp/rsync, просто на другом уровне абстракции.
Практика: тестируйте восстановление, а не доверие к инструменту
Самая частая иллюзия — «мы используем правильный инструмент (pg_dump/xtrabackup/pgBackRest), значит бэкап точно консистентен, проверять не нужно». Это ошибка на уровне процесса, а не техники. Правильный инструмент снижает риск получить несогласованную копию, но не отменяет вероятность других проблем: битый диск с бэкапами, оборванная передача по сети, забытый пароль шифрования, изменившаяся схема, из-за которой pg_restore падает на середине, банально неверно настроенное расписание, при котором бэкап годами писался не туда.
Единственный способ узнать, что бэкап реально работает, — попробовать восстановить его на отдельном стенде и убедиться, что база поднимается, данные на месте, а приложение с ней нормально работает. Практический алгоритм разобран в статье про восстановление базы данных из бэкапа: поднимаете временный экземпляр СУБД, накатываете дамп или физический бэкап, прогоняете несколько контрольных запросов (счётчики строк в ключевых таблицах, суммы в финансовых таблицах, свежесть последней записи), сверяете с продакшном.
Регулярность важна не меньше факта проверки. Разовый тест восстановления, сделанный полгода назад, ничего не говорит о сегодняшнем бэкапе — с тех пор могли поменяться версия СУБД, схема данных, объём, расписание задачи в cron. Разумный минимум — тестовое восстановление хотя бы раз в месяц для некритичных баз и после каждого значимого изменения схемы для критичных. Автоматизировать это несложно: скрипт поднимает контейнер с СУБД, накатывает последний бэкап, гоняет пару SQL-проверок и шлёт уведомление при ошибке — расход ресурсов разовый и небольшой по сравнению с ценой невосстановимого бэкапа, обнаруженного в момент реального сбоя.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли просто выключить репликацию на время бэкапа, чтобы не думать про лаг?
Не стоит — тогда реплика начнёт стабильно отставать от мастера, и после включения репликации обратно ей придётся долго накатывать накопленный WAL/бинлог, увеличивая риск, что архив журнала на мастере успеет вычиститься раньше, чем реплика его прочитает.
Дамп mysqldump с флагом --single-transaction точно не блокирует запись в базу?
Он не держит блокировку таблиц на всё время выгрузки, но требует короткой начальной синхронизации метаданных; для очень активных серверов иногда добавляют --skip-lock-tables и следят, чтобы длинные DDL-операции не выполнялись параллельно с дампом — DDL и такой дамп плохо совместимы независимо от флагов.
Что лучше для большой базы: физический бэкап (xtrabackup/pgBackRest) или снапшот тома?
Если СУБД и диск на одной машине и есть доступ к LVM/ZFS — снапшот обычно быстрее по времени выполнения; физический бэкап специализированным инструментом выигрывает переносимостью, встроенной проверкой целостности и понятным point-in-time восстановлением без привязки к конкретной подсистеме хранения.
Нужно ли шифровать бэкап базы данных отдельно, если диск на сервере и так зашифрован?
Да — шифрование диска защищает только от кражи физического носителя сервера, но не от компрометации самого бэкапа при копировании на другое хранилище или в облако; для дампов и физических бэкапов стоит настраивать отдельное шифрование при передаче и хранении.
Как часто снимать бэкап активной базы?
Зависит от допустимой потери данных (RPO): для транзакционных баз с высокой ценой потерянных изменений разумны частые инкрементальные физические бэкапы (например, раз в час) поверх полного бэкапа раз в сутки; для менее критичных проектов часто достаточно ежесуточного полного дампа.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →