Пятничный вечер, один неверный rm -rf в общем каталоге, и результат работы целого отдела уходит в никуда. Полноценная копия снимается раз в сутки, значит вместе с файлами пропал и весь день. Снимки ZFS закрывают именно эту боль: они создаются за миллисекунды, почти не занимают места, пока данные не меняются, и возвращают любой файл в состояние часовой давности. Дальше собирается полный цикл для файлового сервера: ручные команды для разминки, скрипт на bash с ротацией, расписание в cron и systemd, готовые инструменты и восстановление данных. Отдельное место в разговоре занимает производительность: снимки не должны просаживать дисковый массив и съедать пул целиком.
Чем снимок файловой системы отличается от обычной копии на второй диск
ZFS работает по принципу копирования при записи. Снимок не переписывает файлы и не читает содержимое dataset, а лишь фиксирует набор уже занятых блоков и запрещает их перезапись. Пока данные не меняются, снапшот весит ровно ноль байт. Если файл перезаписали, прежние блоки остаются в пуле и числятся за снимком, а новые данные ложатся рядом. Поэтому команда zfs snapshot на массиве с двумя терабайтами живых файлов выполняется за десятки миллисекунд и не замедляет ни чтение, ни запись.
Из этого следуют два практических вывода. Первый: снапшот отличная страховка от ошибок людей и программ, но не замена настоящему бэкапу, ведь он живёт в том же пуле и пропадёт вместе с ним при отказе дисков. Второй: место под снимки растёт ровно по объёму изменений. Если на dataset tank/share за сутки перезаписывают 10 ГБ, то серия из тридцати дневных снимков удержит в худшем случае около 300 ГБ, а не десятки терабайт, как пугает воображение новичка.
Посмотреть картину на живом сервере можно двумя командами:
zfs list -t snapshot -o name,used,creation -s creation
zfs list -o name,usedbysnapshots tank/share
Колонка usedbysnapshots показывает суммарный объём, удерживаемый снимками dataset, и именно по ней судят, не разрослась ли история.
Ручные команды, с которых стоит начать знакомство со снимками
Прежде чем вешать автоматизацию, полезно проделать цикл руками и убедиться, что поведение предсказуемо. Создание снимка выглядит так:
zfs snapshot tank/share@manual-2026-11-07_1800
Имя строится из dataset, символа @ и произвольной метки, в которую обычно закладывают дату и время. Если нужно снять файловую систему вместе с дочерними dataset и томами, добавляется опция -r:
zfs snapshot -r tank/share
Список снимков с временем создания и занимаемым местом выводится так:
zfs list -t snapshot -o name,used,creation -s creation
Удаляют лишний снимок командой zfs destroy. Она завершится ошибкой, если на снимке стоит удержание или от него создан клон, поэтому жёсткая чистка не заденет нужное:
zfs destroy tank/share@manual-2026-11-07_1800
Удержание ставится отдельной командой и защищает снимок от любого удаления, включая чужие скрипты ротации:
zfs hold important tank/share@manual-2026-11-07_1800
zfs release important tank/share@manual-2026-11-07_1800
Сначала снимок закрепляется меткой important, потом освобождается, когда страховка больше не нужна. На бэкап-сервере такое удержание спасает от ротации, которая сработала бы раньше, чем администратор заметил пропажу данных у клиента.
Скрипт на bash, который создаёт снимок и удаляет лишние копии
Автоматизация сводится к короткому сценарию с тремя аргументами: dataset, метка и глубина хранения. Метка отделяет серии друг от друга, поэтому часовые снимки не мешают дневным при подсчёте. Содержимое файла /usr/local/bin/zfs-snap.sh:
#!/bin/bash
# использование: zfs-snap.sh <dataset> <метка> <сколько хранить>
FS="$1"
LABEL="$2"
KEEP="$3"
# свежий снимок с датой и временем в имени
STAMP="$(date +%Y-%m-%d_%H%M)"
zfs snapshot "${FS}@${LABEL}-${STAMP}" || exit 1
# все снимки этой метки, отсортированные от старых к новым
LIST="$(zfs list -H -t snapshot -o name -s creation "$FS" | grep "^${FS}@${LABEL}-")"
# считаем лишние и удаляем их, начиная с самых старых
TOTAL="$(printf '%s\n' "$LIST" | grep -c .)"
EXTRA=$((TOTAL - KEEP))
if [ "$EXTRA" -gt 0 ]; then
printf '%s\n' "$LIST" | head -n "$EXTRA" | while read -r SNAP; do
zfs destroy "$SNAP"
done
fi
Сценарий опирается только на zfs, date, grep и head, так что работает на любом дистрибутиве с ZFS. Сортировка по ключу -s creation выводит старые снимки первыми, а head -n отрезает ровно столько строк, сколько вышло за лимит. Формат времени %Y-%m-%d_%H%M даёт читаемые имена вида hourly-2026-11-07_1800, которые корректно сортируются даже по алфавиту. Осталось сделать файл исполняемым:
chmod +x /usr/local/bin/zfs-snap.sh
Перед постановкой в расписание сценарий прогоняют руками на тестовом dataset с малым лимитом и смотрят, что получилось:
zfs-snap.sh tank/share hourly 3
zfs list -t snapshot -o name,creation -s creation | head
Если хочется видеть следы работы в системном журнале, в сценарий добавляют вызов logger -t zfs-snap с именем созданного снимка, и каждое срабатывание остаётся в журнале с собственной меткой. На первых неделях такая строка в syslog заменяет полноценный мониторинг: всегда видно, когда ротация сработала в последний раз.
Расписание в cron и systemd-таймерах без постоянного участия человека
Классический вариант: один файл в /etc/cron.d с пятью строками под разные серии.
*/15 * * * * root /usr/local/bin/zfs-snap.sh tank/share 15min 4
0 * * * * root /usr/local/bin/zfs-snap.sh tank/share hourly 24
59 23 * * * root /usr/local/bin/zfs-snap.sh tank/share daily 31
59 23 * * 1 root /usr/local/bin/zfs-snap.sh tank/share weekly 8
0 0 1 * * root /usr/local/bin/zfs-snap.sh tank/share monthly 12
Политика простая. Частые снимки каждые 15 минут с хранением четырёх штук покрывают последний час. Часовые живут сутки, дневные снимаются в 23:59, чтобы поймать итог рабочего дня, недельные пишутся по понедельникам, месячные в полночь первого числа. Файлу нужны владелец root и права 644, планировщик cron подхватывает его без перезапуска служб.
Второй вариант современнее и точнее по управлению. Юнит /etc/systemd/system/zfs-snap-hourly.service:
[Unit]
Description=Hourly ZFS snapshot of tank/share
[Service]
Type=oneshot
ExecStart=/usr/local/bin/zfs-snap.sh tank/share hourly 24
К нему таймер /etc/systemd/system/zfs-snap-hourly.timer:
[Unit]
Description=Run hourly ZFS snapshot
[Timer]
OnCalendar=hourly
RandomizedDelaySec=120
Persistent=true
[Install]
WantedBy=timers.target
Опция Persistent=true догоняет пропущенный запуск, если сервер был выключен в назначенный час, а RandomizedDelaySec распределяет нагрузку, когда в таймерах крутится много задач. Включение одной командой:
systemctl daemon-reload && systemctl enable --now zfs-snap-hourly.timer
Состояние видно в systemctl list-timers, там же указан следующий запуск и результат предыдущего.
Расход места на диске и поведение массива при частых снимках
Главная тревога сисадмина звучит так: не просадят ли частые снимки скорость записи и не съест ли история весь пул. Практика отвечает спокойно. Создание снимка сводится к записи метаданных, а не данных, поэтому даже на загруженном NFS-хранилище всплеск незаметен. Реальная цена проявляется иначе: чем глубже история, тем больше изменённых блоков удерживается в пуле и тем сильнее фрагментация свободного места, которая со временем замедляет запись.
При выборе глубины истории опытные администраторы держатся простых ориентиров:
- частые снимки нужны лишь на ближайшие часы, хранить их больше 4-8 штук расточительно;
- дневные снимки закрывают сценарий "удалили вечером", месячной глубины хватает почти всем;
- недельные и месячные серии оправданы для давних ошибок, но их объём надо измерять, а не угадывать;
- лимиты стоит менять после месяца наблюдений за колонкой usedbysnapshots, а не на глазок.
Разумный потолок для одного dataset лежит в пределах 100-500 снимков: выше начинается медленное удаление и лишний расход памяти на метаданные.
С освобождением места есть нюанс, который часто ставит в тупик. Удаление снимка возвращает место не мгновенно: блоки остаются занятыми, пока на них есть ссылки у других снимков. Если из тридцати дневных снимков уничтожили самый старый, пул получит обратно только те блоки, которых нет в двадцати девяти оставшихся. Чем глубже история, тем меньше отдача от каждого отдельного удаления, и наоборот, короткая ротация быстро отдаёт место пулу.
Отдельная тема целостность пула. Скраб проверяет контрольные суммы всех блоков и стоит в расписании не реже раза в месяц:
zpool scrub tank
Прогресс и найденные ошибки видны в выводе zpool status. Для SSD-пула в расписание добавляют команду zpool trim tank, а свойство autotrim=on включает мгновенный TRIM при освобождении блоков:
zpool set autotrim=on tank
Готовые инструменты zfs-auto-snapshot и Sanoid с ротацией из коробки
Писать свои скрипты приятно, но есть два зрелых проекта на все случаи. Первый, zfs-auto-snapshot, ставится из репозитория Debian или Ubuntu одной строкой:
apt install zfs-auto-snapshot
Пакет кладёт в /etc/cron.d свои правила и создаёт снимки с префиксом zfs-auto-snap: frequent каждые 15 минут с хранением 4 штук, hourly с хранением 24, daily с 31, weekly с 8, monthly с 12. Названия получаются вида tank/share@zfs-auto-snap_hourly-2026-11-07_12:00. Чтобы исключить dataset из автоматизации, на нём ставится свойство:
zfs set com.sun:auto-snapshot=false tank/tmp
Второй инструмент, Sanoid, заметно мощнее: он ведёт ротацию по шаблонам, следит за свежестью снимков и умеет реплицировать их на другой сервер. Шаблон записывается в /etc/sanoid/sanoid.conf:
[tank/share]
use_template = production
[template_production]
frequent_period = 15
hourly = 36
daily = 30
monthly = 3
autosnap = yes
autoprune = yes
Дефолты в поставке щедрее: hourly 48, daily 90, monthly 6, недельные серии отключены. Проект ещё и следит за свежестью копий: по умолчанию предупреждение приходит, когда последний часовой снимок старше 90 минут, тревога включается, если последний дневной старше 32 часов, а заполнение пула выше 80% даёт предупреждение, выше 95% критическое. Для файлового сервера, где снимки остаются единственной страховкой от человеческих ошибок, такой контроль дешевле любого расследования задним числом. Sanoid запускается из таймера каждые 15 минут: за создание снимков отвечает команда sanoid --take-snapshots, за чистку sanoid --prune-snapshots, за отчёт о свежести sanoid --monitor-snapshots. Репликация вынесена в соседний инструмент syncoid, который переносит снимки по SSH одной строкой:
syncoid tank/share root@backup-server:tank/share
Восстановление файлов и репликация снимков на резервный сервер
Самое приятное в снапшотах наступает после инцидента. Найти, что именно изменилось между снимком и текущим состоянием, позволяет команда сравнения:
zfs diff tank/share@hourly-2026-11-07_1100 tank/share
Строки с плюсом это новые файлы, с минусом удалённые, буква M помечает изменённые. Вернуть один документ можно обычным копированием: каталог .zfs/snapshot доступен внутри каждого dataset, хотя и скрыт от ls без специальной настройки:
cp "/tank/share/.zfs/snapshot/hourly-2026-11-07_1100/report.odt" /tank/share/
Если пострадал целый каталог, быстрее поднять клон снимка как отдельную файловую систему и перенести данные rsync:
zfs clone tank/share@daily-2026-11-06_2359 tank/restore
rsync -av /tank/restore/important/ /tank/share/important/
zfs destroy tank/restore
Клон опирается на блоки снимка, поэтому исходный снимок не уничтожить, пока жив клон.
Полный откат dataset к моменту снимка делает zfs rollback с опцией -r, и перед запуском стоит дважды подумать: изменения после снимка исчезнут, а более новые снимки команда уничтожит сама.
Репликация на второй сервер превращает снимки в настоящий бэкап. Полная передача выглядит так:
zfs send tank/share@daily-2026-11-06_2359 | ssh root@backup zfs receive -F tank/share
Инкрементальная передача через опцию -I экономит канал, отправляя только блоки между двумя снимками:
zfs send -I tank/share@daily-2026-11-05_2359 tank/share@daily-2026-11-06_2359 | ssh root@backup zfs receive -F tank/share
Финальный штрих любой схемы: раз в месяц проводить учения по восстановлению. Снимок, из которого ни разу не доставали файл, остаётся непроверенным, а доверять можно только проверенным копиям. Зато когда приходит письмо от пользователя с фразой "вчера случайно удалил папку", ответ занимает одну команду и десять секунд. Это лучшая награда за час, потраченный на настройку ротации.