Корневая файловая система на ZFS давно перестала быть забавой для серверов: снимки перед каждым обновлением, откат за секунды, посекторная проверка контрольных сумм и сжатие, которое просто возвращает часть места. Arch Linux с его роллинг-релизом выигрывает от этого сильнее многих дистрибутивов, потому что свежие пакеты прилетают практически ежедневно, и именно здесь неудачное обновление случается чаще всего. Проблема одна: официальный установочный образ о ZFS ничего не знает. Ниже разобран весь маршрут с готовыми командами, от разметки диска и создания пула до генерации начального загрузчика и отказоустойчивости на уровне снимков.
Загрузочный носитель с поддержкой ZFS и репозиторий archzfs
Модули ZFS обязаны присутствовать в ядре live-окружения, иначе создать пул попросту не получится. Официальный ISO их не содержит, поэтому существует два рабочих варианта.
Первый путь, самый надёжный, это собственный образ на базе archiso. Понадобится хост с Arch Linux и пакетом archiso, из штатного releng-профиля копируется заготовка:
cp -r /usr/share/archiso/configs/releng/ archlive && cd archlive
В файле packages.x86_64 строку linux заменяют на ядро LTS и добавляют пакеты ZFS:
linux-lts
linux-lts-headers
zfs-utils
zfs-dkms
В archlive/airootfs/etc/pacman.conf дописывается репозиторий archzfs, сообщество, которое собирает бинарные пакеты OpenZFS для Arch:
[archzfs]
Server = https://github.com/archzfs/archzfs/releases/download/experimental
Репозиторий подписан, ключ добавляют и подписывают локально:
curl -o /tmp/archzfs.asc https://raw.githubusercontent.com/archzfs/archzfs-keyring/master/keyring/packager/archzfs/3A9917BF0DED5C13F69AC68FABEC0A1208037BE9/3A9917BF0DED5C13F69AC68FABEC0A1208037BE9.asc
pacman-key -a /tmp/archzfs.asc
pacman-key --lsign-key 3A9917BF0DED5C13F69AC68FABEC0A1208037BE9
Дальше сборка и запись на флешку:
mkarchiso -v -w /tmp/archiso-tmp -o out archlive
dd if=out/archlinux-2026.01.01-x86_64.iso of=/dev/sdX bs=4M status=progress oflag=sync
Второй путь короче: загрузиться с официального ISO, добавить тот же репозиторий в live-окружении и собрать модуль на месте через DKMS. Ловушка в том, что свежему ядру ISO нужны подходящие заголовки, а самое свежее ядро порой выходит за пределы диапазона, который поддерживает текущий OpenZFS, и сборка падает. Ядро LTS дольше живёт внутри этого диапазона, поэтому его и рекомендуют для корня на ZFS.
После загрузки с носителя проверка проста:
# в live-окружении
modprobe zfs
zpool status
Пустой вывод без ошибок значит, что модуль на месте и можно двигаться дальше.
Разметка GPT и создание пула с правильными свойствами
Схема разделов тут на удивление скромная, вся красота прячется внутри пула. Таблица GPT, системный раздел EFI размером 1 ГиБ и один большой раздел типа Solaris Root с кодом bf00:
# в live-окружении
sgdisk -n1:0:+1G -t1:ef00 -n2:0:0 -t2:bf00 /dev/nvme0n1
mkfs.fat -F 32 -n EFI /dev/nvme0n1p1
Перед созданием пула стоит заглянуть в физический размер сектора диска командой lsblk -S -o NAME,PHY-SEC. Значение 4096 означает параметр ashift=12, значение 512 даёт ashift=9. Подвох в том, что многие SSD и NVMe рапортуют о 512 байтах, будучи внутри четырикилобайтными, и заниженный ashift необратимо режет производительность. ashift фиксируется при создании пула навсегда, поэтому при сомнениях выбирают 12.
Устройство всегда указывают через /dev/disk/by-id, а не через /dev/nvme0n1p2: имена после смены портов и порядка загрузки способны измениться, и импорт пула сломается в самый обидный момент. Само имя находят в каталоге /dev/disk/by-id. Команда создания пула выглядит так:
zpool create -f \
-o ashift=12 \
-o autotrim=on \
-O acltype=posixacl \
-O relatime=on \
-O xattr=sa \
-O dnodesize=auto \
-O normalization=formD \
-O mountpoint=none \
-O canmount=off \
-O devices=off \
-O compression=zstd \
-R /mnt \
rpool /dev/disk/by-id/nvme-Samsung_SSD_980_1TB_S6RGNF0R123456-part2
Здесь каждый флаг отвечает за годы будущей жизни системы. autotrim=on включает TRIM для SSD, acltype=posixacl и xattr=sa нужны пакетам вроде systemd-journald, relatime=on бережёт диск от лишних записей времени доступа, compression=zstd сжимает данные на уровне по умолчанию 3 и на типичном корне экономит полтора-два объёма, почти не тратя процессор. Параметры -O наследуют все датасеты, созданные внутри пула позже. Кто планирует загрузчик GRUB, указывает ещё -o compatibility=grub2: GRUB читает ZFS только с ограниченным набором функций пула. ZFSBootMenu, о котором пойдёт речь ниже, таким ограничениям не подчиняется.
Собственное шифрование OpenZFS добавляется теми же флагами при создании пула:
-O encryption=aes-256-gcm \
-O keyformat=passphrase \
-O keylocation=prompt \
Пароль система спросит при каждом старте, зато диск можно уносить куда угодно без следов в открытом виде.
Датасеты для корня, home и системных каталогов
В ZFS вместо разделов живут датасеты, и грамотный их набор определяет, насколько удобно будет откатываться. Ключевая идея проста: система и личные данные раскладываются по разным веткам, чтобы снимок корня не тащил за собой сотни гигабайт фотографий, а откат системы не стирал почту и конфиги программ.
Набор для повседневной машины выглядит так:
# в live-окружении
zfs create -o mountpoint=none rpool/data
zfs create -o mountpoint=none rpool/ROOT
zfs create -o mountpoint=/ -o canmount=noauto rpool/ROOT/arch
zfs create -o mountpoint=/home rpool/data/home
zfs create -o mountpoint=/var -o canmount=off rpool/var
zfs create -o mountpoint=/var/log rpool/var/log
zfs create -o mountpoint=/var/lib -o canmount=off rpool/var/lib
zfs create rpool/var/lib/docker
Датасет корня создаётся с canmount=noauto: он монтируется вручную или загрузчиком, а не автоматически при импорте, что и даёт возможность выбирать, какая из веток ROOT станет корнем при старте. Каталоги /var и /var/lib лишь держат дочерние датасеты, сами они монтироваться не могут. Журнал systemd вынесен отдельно не случайно: journald требует posix ACL на каталоге журнала, иначе служба споткнётся при старте, а заодно журналы переживают откат корня, что помогает потом разбираться, почему система отказалась подниматься. Иерархию проверяют командой zfs list -r rpool, в выводе должны быть все восемь датасетов с правильными точками монтирования.
Проверка пула, bootfs и перенос hostid в новую систему
Пул сейчас смонтирован в live-окружении, и если оставить всё как есть, новая система получит его в состоянии, удобном только для текущей сессии. Проверка экспортом и импортом это лечит и заодно отлавливает ошибки с именами устройств:
# в live-окружении
zpool export rpool
zpool import -d /dev/disk/by-id -R /mnt rpool -N
Ключ -N импортирует пул без монтирования датасетов, их поднимают по одному:
zfs mount rpool/ROOT/arch
zfs mount -a
При включённом шифровании перед монтированием добавляют zfs load-key rpool и вводят пароль. Дальше пулу объясняют, какая ветка станет корнем, и сохраняют карту устройств:
zpool set bootfs=rpool/ROOT/arch rpool
zpool set cachefile=/etc/zfs/zpool.cache rpool
mkdir -p /mnt/etc/zfs
cp /etc/zfs/zpool.cache /mnt/etc/zfs/
Теперь hostid, штука, о которой в обычной установке никто не вспоминает. При загрузке корня с ZFS система должна импортировать пул до того, как полностью поднимется сама, и идентифицирует владельца пула по hostid. В initramfs файла /etc/hostid ещё нет, поэтому его записывают заранее и кладут в будущую систему:
zgenhostid $(hostid)
cp /etc/hostid /mnt/etc/
Альтернатива, если файл не хочется держать, это параметр ядра spl.spl_hostid со значением из вывода команды hostid.
Установка пакетов, mkinitcpio и генерация начального загрузчика
Базовая система ставится на смонтированный пул штатным pacstrap, часовые пояса, локали и пароль root настраивают по классической схеме официального руководства:
# в live-окружении
pacstrap -K /mnt base linux-lts linux-lts-headers linux-firmware base-devel vim
arch-chroot /mnt
Внутри chroot повторяют подключение archzfs: тот же блок [archzfs] в /etc/pacman.conf, тот же импорт ключа из файла, затем pacman -Sy. После этого ставят инструменты ZFS, и хук DKMS тут же собирает модуль под установленное ядро LTS:
# внутри chroot
pacman -Sy zfs-utils zfs-dkms
Наступает момент, где установки чаще всего ломаются. Файл /etc/mkinitcpio.conf правят в двух местах: модуль zfs попадает в MODULES, а хук zfs встаёт в HOOKS до filesystems. Initramfs на базе systemd с корнем ZFS несовместим, поэтому хук systemd меняют на udev, оставляя busybox-схему:
MODULES=(zfs)
HOOKS=(base udev autodetect modconf block keyboard zfs filesystems)
Хук keyboard стоит до zfs нарочно: при шифровании пароль вводится ещё в initramfs, и без клавиатуры там делать нечего. Образ пересобирают командой mkinitcpio -P.
Для загрузчика самый современный вариант это ZFSBootMenu, отдельная EFI-программа с текстовым меню, которая сама импортирует пул, показывает загрузочные окружения и умеет стартовать прямо со снимка. Ставится из AUR, вместе с ним нужен dracut, через который ZBM собирает свой образ:
# внутри chroot
pacman -S --needed dracut
git clone https://aur.archlinux.org/zfsbootmenu.git /tmp/zfsbootmenu
cd /tmp/zfsbootmenu && makepkg -si
Генерация и запись на раздел EFI:
generate-zbm
mkdir -p /boot/efi/EFI/ZBM
cp /zfsbootmenu/release/zfsbootmenu.EFI /boot/efi/EFI/ZBM/
efibootmgr -c -d /dev/nvme0n1 -p 1 -L "ZFSBootMenu" -l '\EFI\ZBM\zfsbootmenu.EFI'
Раздел EFI перед этим монтируют в /boot/efi, ядра остаются прямо на ZFS, чему ESP в 1 ГиБ вполне рад. Параметр root= в конфигурации ядра тут не нужен, ZBM подставляет его сам для выбранного окружения. Любители systemd-boot или GRUB поступают иначе: раздел EFI монтируют в /boot и держат ядра на FAT32, а в параметрах ядра прописывают root=ZFS=rpool/ROOT/arch rw, для GRUB дополнительно требуется compatibility=grub2 ещё на этапе создания пула.
Перед перезагрузкой не помешает свериться с коротким списком:
- модуль zfs занесён в MODULES, а хук zfs стоит в HOOKS до filesystems;
- файлы zpool.cache и hostid лежат в /mnt/etc;
- свойство bootfs указывает на rpool/ROOT/arch;
- EFI-файл загрузчика записан и запись в efibootmgr создана;
- пул после всех манипуляций экспортирован командой zpool export rpool.
Пятый пункт не для красоты: неэкспортированный пул откажется импортироваться в initramfs, и система молча уйдёт в консоль busybox, где легко застрять на вечер.
Первый запуск, службы ZFS и настройка кеша ARC
После перезагрузки ZFSBootMenu показывает найденные окружения, через пару секунд стартует выбранное, и машина попадает в обычный Arch. Автомонтированием пула при загрузке управляет связка служб systemd:
# на установленной системе
systemctl enable zfs.target zfs-import-cache.service zfs-mount.service zfs-import.target
systemctl enable --now zfs-zed.service
Служба zed это наблюдатель за событиями пула: она приносит уведомления о результатах проверок и ошибках, без неё система молча пережила бы то, о чём стоило бы кричать.
По умолчанию кеш ARC забирает до половины оперативной памяти, что серверу полезно, а ноутбуку с 16 ГиБ и спящим режимом менее приятно. Верхнюю границу задают явно:
# /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=8589934592
Здесь 8589934592 это 8 ГиБ. Текущие пределы видны в /proc/spl/kstat/zfs/arcstats, строка c_max покажет лимит в байтах, а size подскажет фактический расход. Файл modprobe.d попадает в initramfs через хук modconf, поэтому после правки снова запускают mkinitcpio -P.
Проверка целостности выполняется командой zpool scrub rpool, и на одиночном диске она честно скажет о найденных повреждениях, хотя починить их без избыточности не сможет: самовосстановление контрольных сумм работает только на зеркалах и raidz. Автоматизируют проверку парой юнитов systemd:
# /etc/systemd/system/zpool-scrub@.service
[Unit]
Description=Scrub ZFS pool %i
[Service]
Type=oneshot
ExecStart=/usr/bin/zpool scrub %i
# /etc/systemd/system/zpool-scrub-rpool.timer
[Timer]
OnCalendar=*-*-05 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
После systemctl enable --now zpool-scrub-rpool.timer каждая проверка оставляет след в выводе zpool status, по которому видно, когда диск последний раз обследовался:
pool: rpool
state: ONLINE
scan: scrub repaired 0B in 00:03:41 with 0 errors on Mon Feb 05 03:03:41 2026
Снимки и откат как ежедневная страховка системы
Снимок ZFS занимает секунды и места ровно на объём изменившихся блоков, поэтому стрелять им можно хоть перед каждым pacman -Syu. Ритуал обновления на такой системе выглядит очень успокаивающе:
# на установленной системе
zfs snapshot rpool/ROOT/arch@pre-update
pacman -Syu
Обновление прошло криво и система не грузится? ZFSBootMenu загружает выбранный снимок напрямую: его достаточно указать в меню, ZBM сам клонирует датасет и стартует с него, после чего кривое обновление откатывают уже изнутри. Сам откат корня одной командой возвращает систему в состояние на момент снимка, а zfs diff показывает список изменённых, созданных и удалённых файлов, что помогает понять, что именно сломало загрузку:
zfs list -t snapshot
zfs rollback rpool/ROOT/arch@pre-update
Для смелых есть промежуточный вариант: zfs clone создаёт из снимка полноценную ветку, в которой можно спокойно поэкспериментировать с рискованным пакетом, не трогая рабочую систему.
Ручные снимки хороши для точечных операций, но по-настоящему надёжно работают по расписанию. За это отвечает sanoid из AUR, сервис с декларативным конфигом, который сам создаёт и сам же удаляет устаревшие снимки:
# /etc/sanoid/sanoid.conf
[template_production]
frequently = 4
hourly = 24
daily = 14
monthly = 3
autosnap = yes
autoprune = yes
[rpool/ROOT]
use_template = production
recursive = yes
[rpool/data]
use_template = production
recursive = yes
Такая политика хранит четыре пятнадцатиминутных, 24 часовых, 14 дневных и 3 месячных снимка, а таймер sanoid.timer, идущий в пакете, будит службу каждые 15 минут. Старое альтернативное решение zfs-auto-snapshot делает то же самое, но с меньшей гибкостью.
Одно честное предостережение: снимки на том же пуле не считаются резервной копией, пул умирает вместе со всеми своими снимками разом. Поэтому данные реплицируют на второй диск командами zfs send и zfs receive, и тут же вспоминают про syncoid, компаньона sanoid, который ведёт инкрементальные копии сам:
zpool create -o ashift=12 -O compression=zstd backup /dev/disk/by-id/ata-WDC_WD40EFRX-68N32N0_WD-WCC7K1234567
syncoid --recursive --sendoptions=w rpool/data backup/data
Флаг --sendoptions=w передаёт сырые зашифрованные блоки, и резервный пул хранит данные зашифрованными, не зная пароля. Пару таких syncoid-команд для веток ROOT и data вешают на ночной таймер, и машина получает полную копию со всей историей снимков.
Система, собранная по этой схеме, меняет само ощущение от роллинг-релиза: обновление перестаёт быть прыжком в неизвестность, потому что позади каждого pacman -Syu стоит снимок, а по ночам снимки реплицируются на второй диск. Затраченный час на установку возвращается тем, что самая неприятная ситуация, сломанное после обновления утро, решается перезагрузкой в снимок и одной командой отката.