MAATRIX / Блог / Снапшот тома снят, а база о нём не знала: что внутри

Снапшот тома снят, а база о нём не знала: что внутри

MAATRIX

Админ снимает снапшот тома по расписанию — гипервизор, облачный провайдер или LVM/ZFS справляются с этим за секунды, никого не спрашивая. Через месяц снапшот разворачивают для восстановления, а база данных внутри либо не стартует, либо стартует и жалуется на повреждённые страницы, либо тихо теряет часть последних транзакций. Формально снапшот «успешно создан» — но он ничего не знал о том, что творилось внутри СУБД в момент снятия, и именно в этом корень проблемы.

Что видит снапшот, а что — нет

Снапшот на уровне блочного устройства (LVM, ZFS, снапшот гипервизора, снапшот диска в облаке) работает на уровне ниже файловой системы и тем более ниже приложения. Он фиксирует состояние всех блоков диска на момент времени T — буквально делает пометку «дальше пишем в новые блоки, а старые не трогаем, пока кто-то на них ссылается». Это быстрая и элегантная операция, но у неё есть жёсткое ограничение: снапшот ничего не знает о смысле данных, которые лежат в этих блоках.

СУБД в любой момент времени — это не просто файлы на диске, а сложная система с несколькими слоями буферизации:

  • Буферный пул (buffer pool в PostgreSQL, InnoDB buffer pool в MySQL) — страницы данных, изменённые в памяти, но ещё не сброшенные на диск.
  • WAL/журнал транзакций — последовательный лог операций, который обычно пишется на диск раньше самих данных, но тоже может иметь несброшенный хвост в кэше ОС.
  • Кэш файловой системы — даже то, что СУБД считает «записанным», может физически лежать в page cache ядра и ещё не долететь до диска, если не был вызван fsync.
  • Многофайловая структура — таблицы, индексы и журнал у многих СУБД физически разнесены по разным файлам и даже разным точкам монтирования; снапшот одного тома может не захватить состояние другого синхронно.

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

Почему это похоже на внезапное отключение питания

Полезная аналогия, которая многое объясняет: снапшот тома без координации с СУБД — это ровно та же ситуация, что и мгновенное отключение питания сервера. В обоих случаях происходит одно и то же: содержимое диска фиксируется «как есть», а всё, что было в оперативной памяти и не успело долететь до постоянного хранилища, исчезает или не учитывается.

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

Большинство современных СУБД спроектированы так, чтобы переживать внезапное отключение питания без потери целостности — за счёт журнала с упреждающей записью (write-ahead logging) и процедуры восстановления (crash recovery), которая при следующем старте проигрывает или откатывает незавершённые транзакции. Это хорошая новость: снапшот, снятый в момент активной записи, чаще всего *не катастрофа*, а состояние, эквивалентное «падению по питанию» — СУБД способна из него восстановиться сама. Но это работает только при двух условиях, которые снапшот блочного устройства сам по себе не гарантирует.

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

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

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

Два условия, без которых crash recovery не спасёт

Первое условие — атомарность снапшота относительно всех вовлечённых томов. Если данные, индексы и журнал физически лежат на разных блочных устройствах (разные диски, разные LUN, разные точки монтирования), а снапшот снимается по каждому тому отдельно, не одной атомарной операцией, — между моментом снятия снапшота тома с данными и снапшота тома с журналом может пройти заметное время. За это время база могла записать транзакции, которые попали в журнал уже после снапшота данных, но до снапшота журнала, — или наоборот. Восстановление в этом случае может оказаться невозможным в принципе, потому что нет единой точки согласованности между файлами.

Второе условие — сам снапшот должен быть действительно атомарным на уровне блочного устройства, то есть захватывать все блоки на один и тот же момент времени, а не «размазанно» в течение нескольких секунд снятия. LVM- и ZFS-снапшоты в большинстве реализаций это гарантируют — операция снятия снапшота атомарна с точки зрения самого тома. А вот снапшоты сетевых хранилищ и некоторых облачных дисков могут иметь свои нюансы консистентности, которые стоит уточнять у конкретного провайдера, а не считать по умолчанию.

Даже при выполнении обоих условий остаётся третий фактор — открытые файлы и незафлашенный кэш файловой системы гостевой ОС, если снапшот снимается на уровне гипервизора или облачного диска, а не изнутри самой ОС с явным sync. Здесь начинается зона, где разные СУБД ведут себя по-разному: PostgreSQL и MySQL/InnoDB спроектированы устойчиво к такому сценарию за счёт WAL и redo-log, а решения без журналирования транзакций (некоторые конфигурации MyISAM, отдельные NoSQL-хранилища с отложенной записью) восстанавливаются из подобного состояния гораздо хуже или не восстанавливаются вовсе.

Что происходит на практике при развороте такого снапшота

Три сценария, которые встречаются чаще всего:

Повезло, и crash recovery отработал штатно. СУБД при старте на данных из снапшота обнаруживает незавершённое состояние, проигрывает журнал, откатывает незакоммиченные транзакции — и поднимается в согласованном состоянии, но *на несколько секунд или минут раньше*, чем был снят снапшот. Это нормальное поведение, аналогичное восстановлению после отключения питания, и именно на него рассчитывают многие практики бэкапа через снапшот. Важно понимать: это везение, обусловленное архитектурой конкретной СУБД, а не гарантия, которую даёт сам снапшот.

Частичное повреждение. Индексы или отдельные страницы данных оказываются повреждены, потому что попали в снапшот в момент незавершённой многостраничной операции записи (например, split B-tree страницы). СУБД стартует, но выдаёт ошибки при обращении к конкретным таблицам или требует восстановления через REINDEX/CHECK TABLE/аналоги — если такие инструменты вообще способны исправить конкретный вид повреждения.

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

Проверить, какой из трёх сценариев вы получите, можно только одним способом — тестовым разворачиванием снапшота и попыткой старта СУБД на копии. Это ровно та практика, которая разобрана в статье про проверку восстановления бэкапа раз в месяц: бэкап (и снапшот в роли бэкапа — не исключение) без регулярной проверки восстановления — это не бэкап, а недоказанное предположение о его существовании.

Два рабочих подхода вместо угадывания

Раз проблема — в отсутствии координации между снапшотом и СУБД, решение прямое: либо на момент снятия снапшота временно остановить активную запись, либо снимать бэкап средствами самой СУБД, которая знает о своей внутренней структуре и умеет обеспечить согласованность сама.

Подход 1: кратковременная приостановка записи вокруг снапшота. У большинства СУБД есть механизм для этого:

  • PostgreSQL: SELECT pg_backup_start('snapshot_label'); перед снятием снапшота и SELECT pg_backup_stop(); после (в версиях до 15 — pg_start_backup()/pg_stop_backup()). Функция переводит кластер в режим резервного копирования: контрольная точка форсируется, а WAL, необходимый для согласованного восстановления, сохраняется до завершения.
  • MySQL/MariaDB: FLUSH TABLES WITH READ LOCK; останавливает запись на короткое время, снапшот снимается, затем UNLOCK TABLES;. Для InnoDB с включённым --single-transaction-подходом на уровне логического дампа эта проблема решается иначе (см. ниже), но для снапшота блочного устройства актуальна именно блокировка записи.
  • Для файловых систем и виртуализации часто есть более общий механизм — fsfreeze на уровне гостевой ОС Linux или интеграция гипервизора с гостевыми агентами (VSS на Windows, qemu-guest-agent с --freeze-filesystems в связке с libvirt), которые сами замораживают файловую систему перед снятием снапшота и размораживают сразу после.

Ключевое здесь — что пауза именно кратковременная: снапшот на уровне блочного устройства снимается за доли секунды-секунды, поэтому запись останавливается на сопоставимое время, а не на всё время копирования данных. Это принципиально отличает такой снапшот от бэкапа через pg_dump или mysqldump, где без --single-transaction (для логически согласованного дампа InnoDB) или без временной блокировки несогласованность возникает по похожей причине, но в других масштабах времени.

Подход 2: использовать встроенные механизмы согласованного бэкапа СУБД вместо снапшота диска. Это как правило более надёжный путь, потому что СУБД сама знает, что и в каком порядке нужно зафиксировать:

СУБДВстроенный механизмЧто даёт
PostgreSQLpg_basebackup, WAL-архивирование + PITRСогласованный физический бэкап + возможность восстановления на произвольный момент времени
MySQL/MariaDB (InnoDB)Percona XtraBackup / MariaDB BackupГорячий согласованный бэкап без остановки записи, за счёт чтения redo-log во время копирования
MongoDBmongodump с --oplog, либо снапшот при выключенной записи через fsyncLockСогласованность на уровне oplog при активной записи
RedisBGSAVE (fork + copy-on-write)Согласованный снапшот памяти на момент fork, без остановки обслуживания

Подробнее про то, как устроен fork-механизм Redis и почему он в принципе не подвержен этой проблеме (в отличие от снапшота диска извне), разобрано в статье про форк и снапшот в Redis.

Почему это не теоретическая тонкость, а реальный риск инфраструктуры

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

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

На практике это означает: снапшот тома — отличный инструмент для быстрого отката после неудачного обновления пакета или конфига, когда откатываться приходится на систему целиком и в пределах секунд-минут после снятия. Но как единственный механизм резервного копирования для базы данных он требует либо явной приостановки записи на момент снятия (pg_backup_start/FLUSH TABLES WITH READ LOCK/fsfreeze), либо замены на бэкап встроенными средствами самой СУБД — и в обоих случаях regularной проверки, что из результата база реально стартует.

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

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

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

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

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

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

Если снапшот снимается за доли секунды, разве вероятность попасть на «неудачный» момент не мала?

Вероятность ненулевая и растёт с нагрузкой на запись: чем активнее СУБД пишет данные (много одновременных транзакций, фоновые операции вроде вакуума или компакции), тем выше шанс, что снапшот захватит промежуточное состояние многошаговой операции. На слабонагруженной тестовой базе риск действительно низкий; на боевой базе с постоянной записью — не стоит полагаться на удачу.

Если у меня PostgreSQL с WAL и я уверен в crash recovery, нужно ли вообще что-то менять?

Crash recovery действительно спасает в большинстве случаев single-volume снапшота — это встроенная защита от отключения питания. Но она не защищает от рассогласования между несколькими томами (данные и журнал на разных дисках) и не гарантирует восстановление для СУБД без надёжного журналирования. Явная приостановка записи на секунды через pg_backup_start/pg_backup_stop стоит дёшево и убирает саму неопределённость.

fsfreeze или заморозка гостевой ФС — это то же самое, что остановка записи в СУБД?

Не совсем. fsfreeze останавливает запись на уровне файловой системы гостевой ОС и сбрасывает её кэш на диск, что полезно и нужно, но не заменяет команды самой СУБД: файловая система может быть согласована, а внутренняя логика СУБД (например, многошаговая транзакция, растянутая по нескольким файлам) — нет. Правильная последовательность — сначала пауза записи средствами СУБД, затем fsfreeze/аналог, затем снапшот.

Насколько дольше идёт бэкап через pg_basebackup или XtraBackup по сравнению со снапшотом диска?

Зависит от размера базы и скорости диска — конкретных цифр без привязки к вашей нагрузке приводить не будем, это будет ориентир, а не измерение. Общая логика: снапшот диска — операция порядка секунд независимо от размера тома (copy-on-write), а бэкап средствами СУБД копирует реальный объём данных и идёт пропорционально его размеру, зато не требует остановки записи вообще.

Как проверить, что уже сделанные снапшоты вообще годятся для восстановления базы?

Единственный надёжный способ — развернуть снапшот на тестовом сервере и попытаться штатно запустить на нём СУБД, посмотрев в лог на предмет ошибок восстановления или повреждённых страниц. Делать это разово недостаточно — практика должна быть регулярной, иначе изменение в схеме нагрузки (новый фоновый процесс, выросший объём записи) может тихо сломать то, что раньше работало.

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

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

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