Пятничный вечер, один неверный 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-хранилище всплеск незаметен. Реальная цена проявляется иначе: чем глубже история, тем больше изменённых блоков удерживается в пуле и тем сильнее фрагментация свободного места, которая со временем замедляет запись.

При выборе глубины истории опытные администраторы держатся простых ориентиров:

  1. частые снимки нужны лишь на ближайшие часы, хранить их больше 4-8 штук расточительно;
  2. дневные снимки закрывают сценарий "удалили вечером", месячной глубины хватает почти всем;
  3. недельные и месячные серии оправданы для давних ошибок, но их объём надо измерять, а не угадывать;
  4. лимиты стоит менять после месяца наблюдений за колонкой 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

Финальный штрих любой схемы: раз в месяц проводить учения по восстановлению. Снимок, из которого ни разу не доставали файл, остаётся непроверенным, а доверять можно только проверенным копиям. Зато когда приходит письмо от пользователя с фразой "вчера случайно удалил папку", ответ занимает одну команду и десять секунд. Это лучшая награда за час, потраченный на настройку ротации.