Одна опечатка в скрипте или неудачное обновление пакета в три часа ночи превращает спокойную смену администратора в долгое восстановление из резервных копий. Снапшоты ZFS строят для противоположного сценария: фиксация состояния набора данных занимает доли секунды, в момент создания снимок не занимает ни байта, а возврат к прошлому состоянию идёт быстрее, чем остывает кофе. Ниже разобрана связка, которая встречается на тысячах серверов: снимок перед рискованной операцией, авто-снапшоты через таймеры systemd, регулярная очистка старых копий по возрасту и репликация за пределы пула.
Механика копирования при записи и причина мгновенного появления снапшота
ZFS не изменяет блоки данных на месте: новые раскладываются по свободному месту пула, а старые остаются лежать нетронутыми, пока на них есть ссылки. Снапшот лишь фиксирует указатель на корень дерева блоков и запрещает освобождать всё, на что тот ссылается. Копирования терабайтов не происходит, поэтому создание снапшота занимает одинаковые доли секунды для набора в 50 GB и в 50 TB:
zfs snapshot tank/data@2026-01-12_21-30
Список точек проверяется одной командой:
zfs list -t snapshot -o name,used,referenced,creation
Свежий снимок покажет USED 0B и REFER 128G: хранить пока нечего. USED растёт вместе с расхождением между живыми данными и зафиксированным состоянием: пока файлы не меняются, снимок весит ноль, а как только приложение переписывает блоки, старые версии закрепляются за снапшотом. Грубая оценка для набора на 800 GB: при 2% перезаписанных блоков за сутки дневная глубина часовых снимков удержит порядка 16 GB. Политика хранения - это арифметика: объём закреплённых блоков примерно равен доле изменённых данных, умноженной на глубину истории.
Снапшот доступен только для чтения, и это плюс: точка восстановления остаётся нетронутой. По умолчанию снимки прячутся в скрытом каталоге .zfs/snapshot внутри точки монтирования, но по полному пути доступны всегда, а свойство snapdir=visible показывает каталог в листинге:
zfs set snapdir=visible tank/data
Рекурсивный вариант команды покрывает набор со всеми потомками, что удобно для деревьев вида tank/project:
zfs snapshot -r tank/project@before-update
Ручной снапшот с меткой времени и защита важной точки от удаления
Дисциплина имён решает половину успеха: метка должна отвечать на вопрос "зачем зафиксирована эта точка" без чтения журналов - before-app-update, before-migration, hourly-2026-01-12. Сценарии очистки ориентируются на префиксы, случайные имена вроде test123 ломают уборку. Короткая процедура перед рискованной операцией укладывается в несколько шагов:
- зафиксировать текущее состояние снапшотом с говорящей меткой, например tank/data@before-app-update;
- проверить появление точки командой zfs list -t snapshot;
- поставить защиту zfs hold с тегом, чтобы сценарии очистки не удалили точку возврата;
- выполнить рискованную операцию и сверить результат через zfs diff;
- при ошибке откатить набор целиком или вернуть отдельные файлы из каталога .zfs/snapshot;
- снять защиту командой zfs release и удалить ставший ненужным снимок.
Защита hold выглядит так:
zfs hold keep-forever tank/data@before-app-update
Снимают защиту командой zfs release с тем же тегом. Удалить защищённый снимок не выйдет - и это правильно: hold ставят там, где цена ошибки высока. Сравнение снимка с текущим состоянием показывает, что именно изменилось:
zfs diff tank/data@before-app-update
В выводе у каждой строки маркер: минус означает удаление, плюс - создание, буква M - изменение, R - переименование. Для одного потерянного файла полный откат избыточен, достаточно вернуть его из скрытого каталога:
cp /tank/data/.zfs/snapshot/before-app-update/app/config.php /tank/data/app/config.php
Бывает и так: файл удалён, а спохватились через неделю, когда свежие точки уже вычищены, поэтому глубину хранения не сокращают до минимума. Для баз отдельная тонкость: снимок работающей СУБД фиксируется в состоянии, из которого журнальные движки поднимаются штатно, но критичную базу правильнее прижать на минуту перед снимком.
Откат командой rollback и ограничение самой свежей точки цепочки
Команда rollback возвращает набор данных к состоянию снимка, но с жёстким ограничением: откат возможен только на самый свежий снапшот цепочки. Если после целевой точки появились более новые, их придётся удалить либо отказаться от отката в пользу клона. Ключ -r удаляет все более свежие точки автоматически:
zfs rollback -r tank/data@before-app-update
Всё записанное после снимка пропадает безвозвратно, поэтому перед откатом стоит свериться с zfs diff. Если в текущем состоянии есть ценное, а откатить всё равно нужно, порядок такой: снять снапшот свежего состояния, отправить его потоком send в файл, затем откатывать. Ключ -r удалит и свежий снимок как более новый, но поток останется в файле, и его можно принять в отдельный набор для разбора:
zfs snapshot tank/data@just-before-rollback
zfs send tank/data@just-before-rollback > /backup/fresh-state.zstream
zfs rollback -r tank/data@before-app-update
После отката набор перемонтируется сам, но у процессов остаются дескрипторы старых файлов, поэтому пишущие в набор службы перезапускают. Ключ -R удаляет не только более свежие снимки, но и зависящие от них клоны, с ним аккуратнее. Откат работает на уровне набора данных: отдельный файл он не выбирает, между пулами состояние не переносит - для этого есть каталог .zfs/snapshot и команды send с receive.
Клон вместо отката и безопасное изучение проблемного состояния
Когда правки, сделанные после снимка, могут оказаться ценными, откат - не единственный путь. Клон поднимает копию зафиксированного состояния как отдельный записываемый набор, не трогая оригинал:
zfs clone tank/data@before-app-update tank/data-forensic
Клон разделяет с исходным снимком все неизменённые блоки, поэтому его создание почти мгновенно и почти бесплатно по месту. Точка монтирования у клона своя, так что дерево спокойно изучают и сверяют с живыми данными. Типичная сцена: ночное обновление сломало конфигурацию, утром администратор поднимает клон точки before-update, сверяет дампы и решает, что перенести в продакшн, а что выкинуть. После разбора клон удаляют командой zfs destroy tank/data-forensic. Пока он существует, базовый снимок окончательно не удалить: destroy завершится ошибкой, а ключ -d лишь помечает удаление отложенным - место освободится после ухода последнего клона. Если копия нужна надолго, zfs promote разворачивает зависимость: клон становится самостоятельным набором и живёт без исходного снимка.
Автоматические снимки через таймеры systemd и утилиты с политикой удержания
Ручная дисциплина живёт неделю, потом люди устают, и именно в пропущенный вечер приходит ошибка - расписание закрывает человеческий фактор. Пара юнитов systemd занимает десяток строк. Файл /etc/systemd/system/zfs-snap-hourly.service с типом oneshot создаёт снимок с отметкой времени в имени:
[Unit]
Description=Часовой снапшот набора tank/data
[Service]
Type=oneshot
ExecStart=/bin/sh -c '/usr/sbin/zfs snapshot tank/data@auto-$(date +%%Y%%m%%d_%%H%%M)'
Файл /etc/systemd/system/zfs-snap-hourly.timer запускает его каждый час и догоняет пропущенные запуски после простоя сервера:
[Unit]
Description=Таймер часового снапшота tank/data
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
Включение и проверка:
systemctl daemon-reload
systemctl enable --now zfs-snap-hourly.timer
Знаки процента в юните удваиваются: %%H превращается в %H внутри shell, и дата попадает в имя снимка. Для ночного расписания строку OnCalendar меняют на -- 03:00:00, а снимки каждые 15 минут получают таймер с OnCalendar=:0/15. Пары "сервис плюс таймер" заводят по одной на каждый горизонт: частый, часовой, дневной, недельный, месячный.
Готовые утилиты автоматизации снимают и эту работу. Классическая zfs-auto-snapshot работает с пятью метками частоты: frequent для снимков каждые 15 минут, затем hourly, daily, weekly и monthly. Для каждой метки задаётся своя глубина хранения через ключ keep, а имя снимка складывается из префикса, метки и отметки времени:
zfs-auto-snapshot --quiet --syslog --label=hourly --keep=24 tank/data
В дистрибутивах утилита обычно приходит с готовыми таймерами или заданиями cron на все пять частот. Разумная стартовая политика выглядит так: частые снимки глубиной 4, часовые 24, дневные 14, недельные 8, месячные 6. Ближний горизонт ловит опечатки и неудачные обновления, дальний - медленно обнаруженные проблемы. Числа не аксиома: политику подбирают под скорость изменений и свободное место. Есть и более гибкие инструменты вроде sanoid с политиками в файле конфигурации и syncoid для репликации: такой стек берут, когда серверов много.
Регулярная очистка старых снапшотов по возрасту и контроль занятого места
Снапшоты без уборки замусоривают пул: тысячи точек замедляют вывод списков и инкрементальную репликацию, а старые снимки держат блоки, не давая им вернуться в свободное место. Очистка по возрасту решается скриптом из двадцати строк: утилита zfs list с ключом -p печатает дату создания снимка в секундах от начала эпохи, остаётся сравнить её с порогом.
#!/bin/sh
limit=$(date -d "30 days ago" +%s)
zfs list -H -p -t snapshot -o name,creation |
grep "^tank/data@auto-" |
while read -r name created; do
if [ "$created" -lt "$limit" ]; then
zfs destroy "$name" 2>/dev/null || :
fi
done
exit 0
Скрипт вешают на сервис и таймер с OnCalendar=daily. Защищённый hold снимок пропускается сам: destroy завершится ошибкой, цикл продолжится, а exit 0 не даст systemd пометить прогон неудачным. Префикс auto- должен совпадать в скриптах создания и уборки, иначе точки копятся без срока.
Нюанс с местом: удаление снимка освобождает только блоки, на которые, кроме него, никто не ссылается. Если более свежая точка цепочки ссылается на те же блоки, место вернётся только после её ухода. Реальную картину показывают свойства набора и пула:
zfs list -o name,used,usedbysnapshots tank/data
zpool list -o name,alloc,cap,health
Мониторинг держат на пороге usedbysnapshots, например 30% объёма набора. Ежемесячная команда zpool scrub tank дополняет картину: она находит повреждения блоков, до которых снапшотам нет дела.
Репликация командами send и recv и границы защиты внутри одного пула
Ограничение схемы простое: снапшот живёт в том же пуле, на тех же физических дисках, и отказ массива забирает данные и снимки одним махом. Снапшоты защищают от логических ошибок вроде опечатки или неудачного обновления, но от аппаратных отказов и повреждения метаданных пула - нет, поэтому копия вне пула обязательна. Инструменты уже в руках: send упаковывает снимок в поток блоков, receive принимает его на другой стороне:
zfs send -v tank/data@auto-20260112_2100 > /backup/tank-data-20260112.zstream
Файл потока выносят на внешние носители (при нехватке места сжимают на лету через zstd), а живую репликацию делают по сети конвейером:
zfs send tank/data@auto-20260112_2100 |
ssh backup-user@second-host zfs receive tank/replica/data
Для регулярной передачи хватает инкрементальных потоков: они переносят только блоки, изменившиеся между двумя снимками, поэтому трафик пропорционален дельте, а не объёму набора:
zfs send -i tank/data@auto-20260112_2100 tank/data@auto-20260113_2100 |
ssh backup-user@second-host zfs receive -F tank/replica/data
Ключ -F позволяет принимающей стороне откатиться к состоянию потока, отбросив свои более свежие изменения. Цепочку часовых снимков гоняют раз в час, и вечерняя дельта укладывается в минуты даже на узком канале. Кому не хочется собирать конвейер руками, пригодится syncoid: он оборачивает send и receive в одну команду. Если второй машины нет, выручает файл потока на съёмном носителе.
Схема складывается в три слоя: авто-снапшоты по расписанию ловят логические ошибки с точностью до 15 минут, уборка по возрасту удерживает пул в разумных границах, репликация выносит состояние за пределы аппаратуры. Поверх работают привычки: снапшот с меткой и защита hold перед рискованной операцией, проверка zfs diff после. И последнее: путь возврата репетируют заранее на тестовом наборе. Непроверенный откат - это не откат, а красивая строка в документации, которая окажется пустой в ночь аварии.