Снапшоты делали каждый час — через месяц пул перестал принимать запись
Настроили автоматический снапшот ZFS раз в час — удобно, откатиться можно на любую точку за последние недели, всё работает быстро и без ручного вмешательства. Через месяц пул внезапно отказывается принимать новую запись: приложения падают с ошибками ввода-вывода, а df показывает почти нулевой остаток свободного места при том, что «живых» данных на диске примерно столько же, сколько было и раньше. Разберём, почему снапшоты, каждый из которых сам по себе почти ничего не весит, в сумме способны сожрать весь пул — и что нужно было настроить с самого первого дня, а не после того, как запись уже встала.
Содержание
- Как появилась привычка снапшотить каждый час
- Почему «снапшот почти ничего не весит» — и почему это ловушка
- Хронология расследования: от лёгкой тревоги до отказа в записи
- Куда делось место: смотрим на снапшоты, а не на общий процент
- Корневая причина: настроили создание, забыли ротацию
- Практические действия: ротация, мониторинг, политика хранения
Как появилась привычка снапшотить каждый час
Сценарий типичный для проекта, где данные меняются активно и откатиться назад нужно быстро, без разворачивания бэкапа из внешнего хранилища. Кто-то добавил в cron простую команду:
0 * * * * /sbin/zfs snapshot -r rpool/data@auto-$(date +\%Y-\%m-\%d_\%H\%M)
Или то же самое через zfs-auto-snapshot / sanoid с профилем hourly. Результат один: каждый час на датасете появляется новая точка отката. Первую неделю всё выглядит идеально — случайно удалённый файл или неудачная миграция схемы откатываются за секунды через zfs rollback или монтирование снапшота в .zfs/snapshot/. Задача «настроить бэкапы через снапшоты» закрыта как выполненная.
Проблема в том, что закрыта она наполовину. Настроено создание снапшотов — регулярное, автоматическое, безотказное. Не настроено второе: автоматическое удаление старых снапшотов, которые больше никому не нужны. Разница между «настроить снапшоты» и «настроить создание снапшотов» на первый взгляд кажется придиркой к формулировке, но именно в ней лежит корень будущего инцидента. Похожая логика разбиралась в статье про снапшоты ZFS против бэкапа — там же подробнее про то, почему снапшот не заменяет резервную копию на другом пуле.
За месяц при снапшотах раз в час набирается порядка 720 снапшотов на один датасет — и это без единого срабатывания ротации, потому что её просто не существует в конфигурации. Число само по себе не выглядит угрожающим — снапшот же «почти ничего не весит», об этом и рассказывают в любом введении в ZFS вроде ZFS за 15 минут. Именно это ощущение и оказывается ловушкой.
Почему «снапшот почти ничего не весит» — и почему это ловушка
Фраза «снапшот в момент создания занимает минимум места» — правда, но правда неполная, и в ней спрятана временная оговорка, которую легко упустить.
ZFS — copy-on-write файловая система: она никогда не перезаписывает данные на месте. Когда вы меняете файл, ZFS записывает новые блоки в свободное место и обновляет указатели, а старые блоки на диске не трогает. Снапшот в этой модели — предельно дешёвая операция: это замороженный набор ссылок на блоки, существовавшие на момент создания снапшота, а не копия данных. Поэтому zfs snapshot выполняется меньше секунды и в момент создания не требует дополнительного места — новых данных не пишется, только фиксируется текущее состояние указателей.
Ключевой момент наступает позже. Пока снапшот существует, ZFS не имеет права физически освободить блоки, на которые он ссылается, даже если «живые» данные уже изменились или файл вовсе удалён из активного датасета. Снапшот обещает показать данные ровно такими, какими они были в момент создания, а значит старые версии блоков обязаны физически оставаться на диске, пока хотя бы один снапшот на них ссылается. Только когда последний ссылающийся снапшот удалён, ZFS может реально пометить блок как свободный.
Отсюда и неочевидность: «лёгкий» снапшот в момент создания и «тяжёлый» снапшот спустя месяц активных изменений — это один и тот же снапшот, просто в разные моменты его жизни. Растёт не сам снапшот, а объём данных, которые он *единолично* удерживает от освобождения, по мере того как активный датасет отходит от зафиксированного в нём состояния. При активно изменяющихся данных (перезаписи БД, ротации логов, временные файлы сборки) это расхождение растёт постоянно, и чем снапшот старше, тем больше блоков он держит только своей ссылкой.
При одном снапшоте это всё ещё терпимо. При 720 снапшотах за месяц накопленный эффект перестаёт быть линейной арифметикой «один снапшот занимает X мегабайт», потому что снапшоты частично перекрываются по тому, какие версии блоков удерживают, а частично — нет. Практический вывод один: общий объём места, занятого снапшотами активно изменяющегося датасета, растёт со временем даже если сами данные не увеличиваются в размере — и эта зависимость не видна из простого взгляда на размер одного снапшота в момент его создания.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверХронология расследования: от лёгкой тревоги до отказа в записи
Рост занятого места на пуле был не резким скачком, а медленным сползанием — именно поэтому его так долго не воспринимали как проблему.
Первые недели zpool list показывал плавное увеличение занятого пространства, и это списывали на естественный рост проекта: новые данные, новые логи, новые бэкапы приложений внутри тех же датасетов. Объяснение звучало правдоподобно и было отчасти верным — данные действительно росли. Просто параллельно рос и второй, невидимый на первый взгляд фактор — накопленные снапшоты, — и его вклад со временем стал перевешивать вклад собственно новых данных.
Тревога включилась только тогда, когда пул подошёл к границе доступного пространства и запись начала прерываться ошибками — приложения получали No space left on device, хотя в самих данных ничего аномального не происходило. ZFS ведёт себя здесь жёстче классической файловой системы: при близком к нулю остатке свободного места деградирует и производительность записи (copy-on-write требует находить новые свободные блоки), и в какой-то момент запись просто отказывает — это заметнее, чем постепенное замедление на других ФС при обычном заполнении диска (похожий, но более общий сценарий разобран в статье про диск, заполнившийся на 100%).
Первая реакция — проверить, что вообще выросло: du по активным директориям датасета ничего необычного не показал, файлы данных были плюс-минус там же по размеру. Несовпадение между «данные не выросли» и «место кончилось» — сигнал, что искать нужно не в активном датасете, а в том, что удерживает место помимо него.
Куда делось место: смотрим на снапшоты, а не на общий процент
Стандартный df или zpool list показывают только итоговую картину заполнения пула — они не разделяют, сколько места держат активные данные, а сколько снапшоты. Для этого разделения нужны команды уровня ZFS, а не общей файловой системы.
Первый шаг — посмотреть на снапшоты конкретного датасета с сортировкой по занимаемому месту:
zfs list -t snapshot -o name,used,refer -s used -r rpool/data
Колонка USED — это место, которое освободится, если удалить именно этот снапшот (при условии, что другие снапшоты его не удерживают). Отсортированный по убыванию список обычно сразу показывает картину: несколько снапшотов, совпавших с моментами больших изменений данных (миграция, массовая перезапись), держат заметно больше места, чем остальные, а сотни «рядовых» часовых снапшотов в сумме тоже составляют значительную долю.
Второй полезный срез — сравнение активных данных и снапшотов на уровне датасета:
zfs list -o space rpool/data
Эта команда раскладывает занятое место на составляющие: USEDDS (активные данные датасета), USEDSNAP (место, удерживаемое только снапшотами и больше ничем), USEDREFRESERV и USEDCHILD. Именно USEDSNAP объясняет расхождение между «данные не выросли» и «место кончилось» — доля, приходящаяся на снапшоты, оказывается кратно больше, чем интуитивно ожидалось от «почти ничего не весящих» точек отката.
Третий шаг — оценить, сколько места освободится при удалении конкретного диапазона снапшотов, не удаляя их сразу:
zfs destroy -nv rpool/data@auto-2026-07-01_0000%auto-2026-07-15_2300
Флаг -n — dry-run, -v — подробный вывод; конструкция с % задаёт диапазон снапшотов по имени. Это позволяет увидеть цифру ожидаемого освобождения до того, как что-то реально удалено, — полезно и для решения «что удалять первым», и для проверки перед плановой очисткой в будущем.
Корневая причина: настроили создание, забыли ротацию
Технический механизм заполнения пула разобран выше — накопление снапшотов, каждый из которых удерживает всё больше уникальных блоков по мере расхождения с активными данными. Но это симптом, а не корневая причина в смысле «что именно в системе позволило этому случиться и остаться незамеченным».
Корневая причина организационная, а не техническая: настроили автоматическое создание снапшотов, но не настроили автоматическое удаление снапшотов старше определённого срока хранения. Обе задачи по своей природе не должны разделяться — снапшот, который никогда не удаляется, гарантированно рано или поздно приведёт к этой же ситуации на любом активно изменяющемся датасете, вопрос только во времени. Отсутствие ротации — не отдельный баг, а логическая половина незавершённой конфигурации: cron-задача на создание без парной задачи на удаление — не менее опасная точка отказа, чем логирование без ротации логов, только с обычными файлами вместо снапшотов.
Вторая, менее очевидная часть корневой причины — отсутствие мониторинга именно за объёмом, занятым снапшотами, а не только за общим процентом заполнения пула. Общий алерт «пул заполнен на 85%» сработал бы одинаково поздно и без понимания, что делать дальше — он не говорит, растут ли активные данные или снапшоты. Именно поэтому постепенный рост так долго объясняли «естественным ростом данных проекта»: не было метрики, которая опровергла бы это правдоподобное, но неверное объяснение раньше, чем пул реально заполнился.
Практические действия: ротация, мониторинг, политика хранения
Первое и обязательное — ротация снапшотов настраивается одновременно с их созданием, одной конфигурацией, а не как отдельная задача «на потом». Если снапшоты создаются вручную через cron, вторая строка того же crontab должна удалять снапшоты старше выбранного срока:
# создание — каждый час
0 * * * * /sbin/zfs snapshot -r rpool/data@auto-$(date +\%Y-\%m-\%d_\%H\%M)
# ротация — раз в сутки удаляем часовые снапшоты старше 7 дней
0 3 * * * /sbin/zfs list -t snapshot -o name -H | grep 'rpool/data@auto-' | \
awk -v cutoff="$(date -d '-7 days' +\%Y-\%m-\%d)" '$0 < "rpool/data@auto-"cutoff' | \
xargs -r -n1 /sbin/zfs destroy
На практике удобнее не изобретать самописную ротацию на awk, а сразу использовать инструмент, где создание и удаление — часть одной декларативной политики. sanoid — распространённый выбор: в /etc/sanoid/sanoid.conf задаётся не только расписание снапшотов, но и лимит хранения по каждому классу:
[rpool/data]
use_template = production
recursive = yes
[template_production]
hourly = 48
daily = 14
monthly = 3
yearly = 0
autosnap = yes
autoprune = yes
Здесь hourly = 48 означает не «снапшотить раз в 48 часов», а «хранить не более 48 последних часовых снапшотов» — при новом создании самый старый автоматически удаляется через autoprune. Ротация в этой модели физически не может быть забыта отдельно от создания — они конфигурируются одной секцией.
Второе — регулярно проверять именно объём, удерживаемый снапшотами, а не только общий процент заполнения пула:
zfs list -o name,used,usedbysnapshots -r rpool/data
Если usedbysnapshots растёт неделя к неделе быстрее, чем растут собственно активные данные — это ранний сигнал ровно того сценария, который разобран в этой статье, и заметить его можно за недели до того, как пул упрётся в потолок.
Третье — осознанная политика хранения вместо «храним всё на всякий случай». Стоит явно ответить: на сколько версий назад реально может понадобиться откат в этом датасете? Для базы с активными миграциями разумный диапазон — часы и несколько последних дней, для датасета с редко меняющимися конфигурациями — недели. «Хранить всё вечно» на активно изменяющихся данных — не консервативная безопасная политика, а гарантированный повтор описанного инцидента, просто отложенный во времени. Подробнее о выборе сроков хранения — в статье про сколько хранить бэкапы и по какой схеме ротации; логика та же, что и для бэкапов, только окно отклика у снапшотов обычно короче.
Если пул уже заполнен и запись отказывает: сначала zfs list -t snapshot -o name,used -s used -r <pool> для поиска самых тяжёлых снапшотов, затем dry-run через zfs destroy -nv для оценки освобождения места, и только после этого — фактическое удаление, начиная с самых старых и тяжёлых, которые точно не нужны для отката. Удалять стоит с запасом: на ZFS-пуле, близком к 100% заполнения, деградирует производительность даже нормальной записи, и небольшой запас нужен не только для новых данных, но и для работы самого copy-on-write механизма.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли просто удалить самый старый снапшот, чтобы быстро освободить место?
Да, это безопасное первое действие — но эффект стоит сначала оценить через zfs destroy -nv, потому что объём освобождения зависит от того, ссылаются ли на те же блоки другие оставшиеся снапшоты. Иногда самый старый снапшот освобождает мало, а следующий за ним — значительно больше.
Почему не помогает удалить ненужные файлы из активного датасета, если место кончилось из-за снапшотов?
Потому что удаление файла из активного датасета не освобождает физическое место, пока хотя бы один снапшот ссылается на его блоки — они остаются занятыми до удаления последнего снапшота-держателя. Освободить место в такой ситуации можно только удалением снапшотов.
Сколько снапшотов вообще разумно хранить для датасета с активными изменениями?
Универсального числа нет — зависит от того, как быстро нужно замечать проблему и на сколько версий назад может понадобиться откат. Ориентир: плотная история (снапшоты каждый час) на недавнем окне в несколько дней, дальше — прореживание до ежедневных и ежемесячных точек, как в шаблонах retention у sanoid.
Отличается ли эта проблема от фрагментации свободного места в пуле ZFS?
Да, механизмы разные. Фрагментация — про то, что даже при формально свободном месте пул с трудом находит достаточно большие непрерывные участки под новую запись, это отдельная тема, связанная с возрастом пула. Здесь же место реально занято — не активными данными, а накопленными снапшотами без ротации.
Помогает ли квота или резервирование на датасете предотвратить такую ситуацию?
Частично — zfs set quota= на датасет с данными ограничивает, до какого предела он может вырасти, и защищает остальной пул от одного датасета, съедающего всё целиком. Но квота не решает корневую причину: снапшоты внутри лимита всё равно копятся без ротации, просто отказ в записи наступит раньше и локальнее.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →