Konverze Debianu ze souborového systému EXT4 na moderní ZFS

19. 9. 2023
Doba čtení: 22 minut

Sdílet

 Autor: Depositphotos
Virtuální Privátní Servery (VPS) typicky využívají pro kořenový souborový systém EXT4. Článek ukáže, jak takový systém zkonvertovat na ZFS přímo z instalace, bez vnější pomoci jako live CD nebo konzole.

Motivace

Co se dozvíte v článku
  1. Motivace
  2. Vytyčení postupu
  3. Prostředí
  4. Příprava
  5. Instalace ZFS
  6. Start do kořenového systému na ZFS
  7. Nové superschopnosti
  8. Využívání místa ZFS
  9. Specifika při provozu s PostgreSQL
  10. Pokročilé nastavení ZFS pro provoz databází
  11. Video návod

OpenZFS je moderní, ale konzervativní Copy on Write souborový systém, který značně zjednodušuje správu produkčních i vývojových systémů. Hlavními výhodami jsou obsáhlá kontrola integrity, pokročilá a flexibilní správa úložiště včetně RAID a díky takto získané redundanci možnost řadu chyb automaticky opravit. Velká výhoda jsou díky CoW levné snapshoty, které je dokonce možné serializovat a poslat třeba na záložní systém, dále lze posílat třeba i jen inkrementální změny mezi snapshoty. Pro provoz databází se hodí transparentní komprese, možnosti cachování a kontrola integrity, díky čemuž lze vypnout některé duplicitní garance v databázových systémech a zvýšit tak výkon.

Souborový systém OpenZFS má od Debian 9 Stretch balíček využívající Dynamic Kernel Module System (DKMS) zjednodušující praktické nasazení. Od verze OpenZFS 2.0.0 je vývoj OpenZFS unifikovaný, původní upstream v projektu navazujícím na OpenSolaris Illumos je sesazen a projekt se nyní soustředí na plnou podporu Linuxu a FreeBSD. V Debianu 12 Bookworm je dostupná verze 2.1.11, verze 2.2.0 je už v procesu vydání a přináší řadu vítaných vylepšení. I když OpenZFS není součástí Linuxového jádra, není důvod se jeho nasazení bát.

Nevýhodou OpenZFS je především nízká rozšířenost ve standardních obrazech VPS a oproti klasickým souborovým systémům obsáhlejší možnosti nastavení a inherentní komplexita. Přinejmenším nastavení recordsize je pro dostatečný výkon např. databází skutečně nutné upravit. Cílem článku je vyřešit první problém a zmínit se o esenciálních nastaveních, aby nevznikaly zcela zavádějící benchmarky.

Vytyčení postupu

Pro největší přenositelnost postupu je důležité, abychom nevyžadovali připojení live CD nebo přístup ke konzoli. V každém prostředí tyto obezličky nemusí být dostupné, nebo je všechno trochu jiné a tomu se chceme vyhnout. Postup by měl ideálně fungovat jak na virtuálním, tak fyzickém hardwaru.

Abychom mohli nahradit kořenový souborový systém, potřebujeme si udělat na disku místo pro nový oddíl se ZFS, který bude ideálně na původním místě oddílu, kde byl EXT4. Za běhu EXT4 ale zmenšit nelze a podobně riskantní/ těžko realizovatelné je zmenšování a přesouvání oddílu za běhu. Naštěstí díky tomu, že Linux nejdříve načítá initramfs čistě v paměti, můžeme tyto přípravné operace naskriptovat a spustit během startu operačního systému.

Nabootujeme do stavu, kdy máme na konci disku malý oddíl s kořenovým souborovým systémem a poměrně dost volného místa před ním. Tam si vytvoříme oddíly pro ZFS, nainstalujeme ZFS a nakopírujeme tam pro jednoduchost pomocí rsync strukturu z běžící instalace o které víme, že funguje a má obecně správnou konfiguraci. Alternativně bychom mohli například pomocí debootstrap vytvořit novou instalaci. Přesně tak postupuje oficiální průvodce instalací „Root on ZFS“.

Prostředí

Všechny kroky byly vyzkoušeny na VPS CX21 od německého poskytovatele Hetzner, který má sdílená 2 vCPU Intel, 4 GB RAM, 40 GB SSD a Debian 12 Bookworm na EXT4. Takto vypadá výchozí rozložení oddílů na disku.

# sfdisk -l
Disk /dev/sda: 38.15 GiB, 40961572864 bytes, 80003072 sectors
Disk model: QEMU HARDDISK
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 73F71C96-9D37-4A17-8BC7-3E325EA7B8A4

Device      Start      End  Sectors  Size Type
/dev/sda1  503808 80003038 79499231 37.9G Linux filesystem
/dev/sda14   2048     4095     2048    1M BIOS boot
/dev/sda15   4096   503807   499712  244M EFI System

Příprava

Iniciální výpočty pro správné zarovnání bloků

ZFS používá standardně velikost záznamů (recordsize) 128KB, tedy 128 * 1024 bajtů = 131072 bajtů (bytů). Tyto se dále rozkládají na bloky, jejichž velikost se nastavuje parametrem ashift, který udává dvojkový exponent. Pro staré enterprise disky je vhodný ashift 9, tedy 29 = 512, pro skoro cokoliv aktuálního je lepší ashift 12, tedy 212 = 4096 či dokonce pro některá SSD 213 = 8192 bajtů.

Rychlým výpočtem se můžeme přesvědčit, jestli je počátek zarovnaný na základní velikost bloku, abychom při čtení jednoho bloku nemuseli fakticky číst dva. To je dobrá poučka pro jakýkoliv souborový systém. Počátek našeho oddílu s root souborovým systémem /dev/sda1 vypočteme v bajtech takto 503808 (počet sektorů) * 512 (velikost sektoru) = 257949696 a nasekáme si ho na 4096 bajtové sektory 257949696 / 4096 = 62976.0 krásně tedy vidíme, že nemáme desetinnou část či zbytek. Mohli bychom také podělit počet sektorů 8 (protože 4096 / 512 = 8) a došli bychom ke stejnému výsledku, jen možná méně zřejmým postupem. Pokud si nejste jistí, nastavte ashift 12.

Příprava rekonfigurace oddílů

Prozatím odstraníme balíček cloud-initramfs-growroot, aby nám nezvětšoval oddíly, které chceme ponechat malé.

# apt remove cloud-initramfs-growroot

Zazálohujeme si tabulku oddílů do souboru:

# sfdisk -d /dev/sda > /sda.dump
# cat /sda.dump
label: gpt
label-id: 73F71C96-9D37-4A17-8BC7-3E325EA7B8A4
device: /dev/sda
unit: sectors
first-lba: 34
last-lba: 80003038
sector-size: 512

/dev/sda1 : start=      503808, size=    79499231, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=31597F60-F5A2-4F6D-A3D4-8BDEDC216B20
/dev/sda14 : start=        2048, size=        2048, type=21686148-6449-6E6F-744E-656564454649, uuid=3F9FF4F9-B2A4-4D3C-ACE2-4163A280082F
/dev/sda15 : start=        4096, size=      499712, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, uuid=45B4FA81-9DD6-40BF-A229-40B23495966A

Budeme chtít zmenšit oddíl tak, aby se nám do něj vešel souborový systém. Kolik aktuálně souborový systém zabírá zjistíme pomocí programu df. Já se rozhodl štědře ponechat ~ 4 GB prostoru, protože po úspěšné instalaci tento oddíl odstraníme, nebude nás to mrzet. Velikost size= 79499231 na řádku /dev/sda1 upravíme na (4*1024**3) / 4096 = 1048576.0, což zaokrouhlíme na nějaký mírně vyšší násobek 8, třeba 1048600 a následně vynásobíme 8 = 8388800. To je náš počet 512 sektorů. Soubor adekvátně upravíme.

# sed -i -e '/\/dev\/sda1 :/ s/size=[ 0-9]\+,/size= 8388800,/' /sda.dump

Pozor na mezeru za sda1 , pokud jako zde ostatní oddíly také obsahují 1 ve svém názvu.

Dále připravíme skript, který připraví správný initramfs obraz a skript, který bude během bootu systému spuštěn a který vykoná potřebné úpravy.

# cat <<EOF > /etc/initramfs-tools/hooks/resizefs
#!/bin/sh

set -e

PREREQS=""

prereqs() { echo "\$PREREQS"; }

case "\$1" in
    prereqs)
    prereqs
    exit 0
    ;;
esac

. /usr/share/initramfs-tools/hook-functions
cp /sda.dump \${DESTDIR}/sda.dump
chmod 644 \${DESTDIR}/sda.dump

copy_exec /sbin/e2fsck
copy_exec /sbin/resize2fs
copy_exec /sbin/sfdisk

exit 0
EOF
# chmod +x /etc/initramfs-tools/hooks/resizefs

Pozor na správné escapování znaku $ v tzv. here-docu.

Tento skript následně vykoná zmenšení. Zde si musíme spočítat první sektor, na který chceme zmenšený oddíl přesunout. Nejlepší je vzít poslední sektor disku, který je uvedený v dumpu tabulky oddílu jako last-lba (80003038) a odečíst od tohoto čísla velikost zmenšeného oddílu (8388800, což je opticky pro účely návodu trochu naobtíž dost podobné číslo). Zajímavé je, že poslední blok nebývá skutečný konec disku, ale často nějakých 34 bloků dříve. Prý to je kvůli zarovnání na 1 MiB/ 2048 sektorů.

Náš výpočet tedy bude 80003038–8388800 = 71614238. Pro jistotu můžeme zaokrouhlit „dolů“, tedy směrem k počátku disku, doporučil bych zarovnání na 2048, ale to protože oddíl budeme odstraňovat, je to spíše kosmetická záležitost.

# cat <<EOF > /etc/initramfs-tools/scripts/local-premount/resizefs
#!/bin/sh

set -e

PREREQS=""

prereqs() { echo "\$PREREQS"; }

case "\$1" in
    prereqs)
    prereqs
    exit 0
    ;;
esac

# simple device example
/sbin/e2fsck -yf /dev/sda1
/sbin/resize2fs /dev/sda1 4G # see size info below
/sbin/e2fsck -yf /dev/sda1
/sbin/sfdisk --force /dev/sda < /sda.dump
echo "71614238," | /sbin/sfdisk --force --move-data /dev/sda -N 1

EOF
# chmod +x /etc/initramfs-tools/scripts/local-premount/resizefs

Podobným způsobem by bylo možné upravit třeba i konfiguraci RAID nebo LVM. Více informací v odpovědi na ServerFault, odkud skripty pocházejí.

Aby nám do těchto operací nezasahoval případně přítomný cloud init, můžeme odpovídající části zakomentovat. V případě Hetznera nechceme nic z growpart, resizefs, ani  disk_setup.

# sed -i -e '/growpart\|resizefs\|disk_setup/ s/^#*/#/' /etc/cloud/cloud.cfg
# sed -i -e '/growpart\|resizefs\|disk_setup/ s/^#*/#/' /etc/cloud/cloud.cfg.d/90-hetznercloud.cfg
# cat /etc/cloud/cloud.cfg
# cat /etc/cloud/cloud.cfg.d/90-hetznercloud.cfg

Poté vytvoříme obraz initramfs pro všechny kernely. Na čisté instalaci to typicky bude jeden.

# update-initramfs -u -k all

Ověříme si, že obraz obsahuje nástroje, které jsme si přáli.

# lsinitramfs /boot/initrd.img-6.1.0-9-amd64 | grep -E "sfdisk|e2fsck|resize2fs"

Pro jistotu updatujeme bootloader grub. V některých případech totiž nebyl obraz initramfs zaregistrován. Poté můžeme restartovat a proces rekonfigurace tak spustit.

# update-grub
# reboot

Po startu se můžeme přesvědčit, že je vše přesunuté dle očekávání.

# fdisk -l

Oddíl /dev/sda1 by měl být nyní 4 GB veliký a měl by začínat ke konci disku. Pokud ano, můžeme po sobě uklidit, tedy smazat skripty pro start a přegenerovat initramfs.

# rm /etc/initramfs-tools/scripts/local-premount/resizefs
# rm /etc/initramfs-tools/hooks/resizefs
# update-initramfs -u -k all

Zatím nebudeme znovu přidávat automatickou konfiguraci oddílů a souborových systémů v cloud init.

Instalace ZFS

Nainstalujeme potřebné balíčky

# apt update && apt install --yes zfsutils-linux zfs-dkms linux-headers-amd64

gdisk již s cloud init nainstalovaný je, debootstrap nepotřebujeme, pokud zkopírujeme soubory z běžící instalace pomocí rsync. Se složitějšími úpravami úložišť je vždy lepší odkazovat se na neměnná označení. Najdeme označení disku v /dev/disk/by-id  a označení si uložíme do proměnné prostředí  DISK.

# ls -l /dev/disk/by-id/
# DISK=/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_33543806

Pozor, aby označení nekončilo např. na -part1, to by byl oddíl na disku. Pokud takové označení chybí, konzultujte příručku OpenZFS.

Než začneme připravovat oddíly, můžeme si všimnout, že ve virtuálním stroji je připravený oddíl pro EFI. Podle všeho však Hetzner na platformě x86_64 resp. AMD64 stále startuje stroje pomocí virtuálního BIOSu. Zmiňuje se o tom i oficiální dokumentace . Rada, že pokud je dostupný speciální soubor /sys/firmware/efi, tak že systém nastartoval pomocí UEFI se nezdá být přesná. Oddíly pro BIOS boot a EFI system každopádně zachováme, dohromady zabírají 250 MB, což můžeme snad oželet. Na strojích Ampere Altra, tedy ARM64, už se používá UEFI.

Vytvoříme oddíl pro tzv. boot pool – bpool, který bude mít omezenější vlastnosti a plnotučný oddíl root pool – rpool pro kořenový souborový systém. Pro boot pool volíme pro jistotu větší velikost, než doporučuje příručka.

# sgdisk    -n3:0:+2G   -t3:BF01 $DISK
# sgdisk    -n4:0:0     -t4:BF00 $DISK

Vynutíme přečtení tabulky oddílů, což ihned ověříme.

# partx --update $DISK
# fdisk -l

Můžeme ověřit, že začátek čtvrtého oddílu, který je zamýšlen pro kořenový souborový systém začíná na celém 4K sektoru (nebo jiném podle ashift) způsobem popsaným výše. Zcela určitě se nový oddíl pro boot pool překrývá s bývalým umístěním kořenového oddílu, to můžeme snadno ověřit nástrojem wipefs, který bez vlaječek jen informuje. Název nástroje je v tomto směru nešťastný. Alternativně můžeme prostě prvních pár bloků přepsat nástrojem dd. wipefs mimochodem umí také signaturu souborového systému zálohovat.

## wipefs ${DISK}-part3
# wipefs --all --force ${DISK}-part3
## dd if=/dev/zero of=${DISK}-part3 bs=1M count=1 status=progress

Načteme jádrový modul zfs.

# modprobe zfs

Nainicializujeme boot pool.

# zpool create \
    -o ashift=12 \
    -o autotrim=on -d \
    -o cachefile=/etc/zfs/zpool.cache \
    -o feature@async_destroy=enabled \
    -o feature@bookmarks=enabled \
    -o feature@embedded_data=enabled \
    -o feature@empty_bpobj=enabled \
    -o feature@enabled_txg=enabled \
    -o feature@extensible_dataset=enabled \
    -o feature@filesystem_limits=enabled \
    -o feature@hole_birth=enabled \
    -o feature@large_blocks=enabled \
    -o feature@livelist=enabled \
    -o feature@lz4_compress=enabled \
    -o feature@spacemap_histogram=enabled \
    -o feature@zpool_checkpoint=enabled \
    -O devices=off \
    -O acltype=posixacl -O xattr=sa \
    -O compression=lz4 \
    -O normalization=formD \
    -O relatime=on \
    -O canmount=off -O mountpoint=/boot -R /mnt \
    bpool ${DISK}-part3

Nainicializujeme root pool pro kořenový souborový systém, standardně zapneme LZ4 kompresi, což prakticky vždy pomůže a díky včasnému rozpoznání (ne)komprimovatelnosti se zbytečně neztrácí čas se špatně komprimovatelnými daty (early abort funkce, kterou např. algoritmus zstd bude podporovat až od vydání 2.2.0).

# zpool create \
    -o ashift=12 \
    -o autotrim=on \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto \
    -O compression=lz4 \
    -O normalization=formD \
    -O relatime=on \
    -O canmount=off -O mountpoint=/ -R /mnt \
    rpool ${DISK}-part4

Vytvoříme tzv. datasety jako kontejnery pro další datasety. Zde se držíme prakticky doslovně oficiální příručky.

# zfs create -o canmount=off -o mountpoint=none rpool/ROOT
# zfs create -o canmount=off -o mountpoint=none bpool/BOOT

Vytvoříme další datasety, nyní už ty, které budeme přímo používat.

# zfs create -o canmount=noauto -o mountpoint=/ rpool/ROOT/debian
# zfs mount rpool/ROOT/debian

# zfs create -o mountpoint=/boot bpool/BOOT/debian

Vytvoříme další datasety pro typické adresáře.

# zfs create                    rpool/home
# zfs create -o mountpoint=/root rpool/home/root
# chmod 700 /mnt/root
# zfs create -o canmount=off    rpool/var
# zfs create -o canmount=off    rpool/var/lib
# zfs create                    rpool/var/log
# zfs create                    rpool/var/spool

# zfs create -o com.sun:auto-snapshot=false rpool/var/cache
# zfs create -o com.sun:auto-snapshot=false rpool/var/lib/nfs
# zfs create -o com.sun:auto-snapshot=false rpool/var/tmp
# chmod 1777 /mnt/var/tmp

# zfs create -o canmount=off rpool/usr
# zfs create                rpool/usr/local

# zfs create rpool/var/mail
# zfs create rpool/var/www

# zfs create rpool/opt

Nyní můžeme připojit souborové systémy a vytvořit dodatečné adresáře v nich.

# mkdir -p /mnt/run
# mount -t tmpfs tmpfs /mnt/run
# mkdir -p /mnt/run/lock
# mkdir -p /mnt/boot/efi

Pokud systém bootuje z UEFI místo BIOSu, potřebujeme připojit oddíl s EFI tak, abychom na něj viděli z budoucího systému.

# mount | grep /boot/efi

Což vypíše něco podobného

/dev/sda15 on /boot/efi type vfat
(rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortna
me=mixed,utf8,errors=remount-ro)

Nyní přesně tento oddíl odpojíme a připojíme do dříve vytvořené složky v  /mnt/boot/efi.

# umount /boot/efi
# mount ${DISK}-part15 /mnt/boot/efi/

Z běžícího systému zkopírujeme nastavení. Jedná se v podstatě o klonování nebo zálohu systému „chudého muže“, protože na vytíženém systému by nemusela být kopie konzistentní.


# rsync -qaAHSX --
exclude={"/boot/efi/*","/dev/*","/proc/*","/sys/*","/tmp/*","/run/*","/
mnt/*","/media/*","/srv","/lost+found"} / /mnt

ZFS mimochodem umí dělat atomické snapshoty, což je pro zálohy komplexnějších systémů správné řešení a přesně jeden z důvodů, proč instalaci procházíme.

Po dokončení kopie se můžeme připojit do tohoto nového systému a doupravit náš budoucí systém.


# mount --make-private --rbind /dev  /mnt/dev
# mount --make-private --rbind /proc /mnt/proc
# mount --make-private --rbind /sys  /mnt/sys
# mount --make-private --rbind /run  /mnt/run
# chroot /mnt /usr/bin/env DISK=$DISK bash --login

Doinstalujeme další balíčky a řekneme, že se má přegenerovat initramfs (soubor initrd).


# apt install --yes zfs-initramfs zfs-zed
# echo REMAKE_INITRD=yes > /etc/dkms/zfs.conf

Pokud vyskakují varovná hlášení cryptsetup, můžete je ignorovat, protože cryptsetup nerozumí ZFS.

Můžeme také zakomentovat


# sed  -i -e '/ext4/ s/^#*/#/' /etc/fstab

či odstranit (!)


# sed -i '/ext4/d' /etc/fstab

řádky s EXT4 z /etc/fstab, protože ZFS si veškeré tyto informace udržuje ve svých strukturách.

Přidáme a zapneme službu, která umožní připojení boot poolu.


# cat <<EOF > /etc/systemd/system/zfs-import-bpool.service
[Unit]
DefaultDependencies=no
Before=zfs-import-scan.service
Before=zfs-import-cache.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/zpool import -N -o cachefile=none bpool
# Work-around to preserve zpool cache:
ExecStartPre=-/bin/mv /etc/zfs/zpool.cache /etc/zfs/preboot_zpool.cache
ExecStartPost=-/bin/mv /etc/zfs/preboot_zpool.cache /etc/zfs/zpool.cache

[Install]
WantedBy=zfs-import.target
EOF
# cat /etc/systemd/system/zfs-import-bpool.service
# systemctl enable zfs-import-bpool.service

Pro některé nativní NVMe disky toto může selhat, případně je nutné přidat -d $DISK-part3 do zpool import příkazu v uvedené službě (nahraďte $DISK za cestu  /dev/disk/by-id/...).

Můžeme zapnout tmpfs, což se vzájemně vylučuje s /tmp  datasetem, který jsme ale nezakládali.


# cp /usr/share/systemd/tmp.mount /etc/systemd/system/
# systemctl enable tmp.mount

Podíváme se, jestli systém vidí grub, měli bychom dostat odpověď „zfs“.


# grub-probe /boot

Znovu sestavíme initramfs, obraz initrd.


# update-initramfs -c -k all

Cryptsetup bude nejspíš protestovat:


cryptsetup: ERROR: Couldn't resolve device rpool/ROOT/debian
cryptsetup: WARNING: Couldn't determine root device

Nyní ještě upravíme nastavení GRUB. Odstraníme parametr quiet z GRUB_CMDLINE_LINUX_DEFAULT  a odkomentujeme GRUB_TERMINAL=console. Něco z toho je již ve virtuálních strojích Hetznera takto upraveno.


# sed -i 's%^GRUB_CMDLINE_LINUX=""%GRUB_CMDLINE_LINUX="root=ZFS=rpool/ROOT/debian"%' /etc/default/grub

Také můžeme třeba snížit GRUB_TIMEOUT z pěti na dvě sekundy pro mírné zrychlení bootu.


# sed -i '/GRUB_TIMEOUT=/ s/GRUB_TIMEOUT=5/GRUB_TIMEOUT=2/' /etc/default/grub
# cat /etc/default/grub

Nyní už jen zupgradujeme grub. Ujistíme se, že proměnná prostředí DISK je nastavená a můžeme grub pro jistotu přeinstalovat.


# update-grub
# echo $DISK
# grub-install $DISK

Upravíme pořadí připojení.


# mkdir -p /etc/zfs/zfs-list.cache
# touch /etc/zfs/zfs-list.cache/bpool
# touch /etc/zfs/zfs-list.cache/rpool
# zed -F &

Zmáčkneme ještě jednou ENTER a poté se přesvědčíme, že cache nejsou prázdné.


# cat /etc/zfs/zfs-list.cache/bpool
# cat /etc/zfs/zfs-list.cache/rpool

Poté můžeme proces na pozadí přesunout do popředí (foreground) a pomocí Ctrl-C ukončit.


# fg
# Ctrl-C

Poté už jen upravíme cesty, aby neobsahovaly dočasnou cestu s /mnt.


# sed -Ei "s|/mnt/?|/|" /etc/zfs/zfs-list.cache/*

Vytvoříme snapshoty.


# zfs snapshot bpool/BOOT/debian@install
# zfs snapshot rpool/ROOT/debian@install

A opustíme prostředí chroot.


# exit

Odpojíme souborové systémy a vyexportujeme naše pooly.


# mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | \
    xargs -i{} umount -lf {}
# zpool export -a

Pokud toto selže pro rpool, může se stát, že import poolu při bootu selže a bude nutné jej násilně naimportovat pomocí zpool import -f rpool, což je ale nutné udělat z řádky initramfs. I přestože je rpool zaměstnaný „busy“, neměl by reboot být problematický. Při testování systém bez problémů i přes toto hlášení nastartoval.


# reboot

Start do kořenového systému na ZFS

Systém by měl nabootovat do systému na ZFS. O tom se můžeme přesvědčit.


$ uptime && uname -a
$ zpool list
$ zfs list
$ fdisk -l

Nyní po sobě uklidíme a získáme tak zpět místo stále ještě alokované původnímu systému na zmenšeném oddílu. Nejprve si znovu uložíme do proměnné prostředí označení disku.


# ls -l /dev/disk/by-id/
# DISK=/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_33543806

Poté si zazálohujeme tabulku oddílů a smažeme oddíl s původním systémem.


# sfdisk -d $DISK > /sda_before_cleanup.dump
# sfdisk --delete $DISK 1

Rozložení disku je nutné znovu načíst.


# partx --update $DISK
# fdisk -l

Volitelně můžeme zpátky vrátit schopnost zvětšení oddílů při restartu pomocí cloud init.


# sed -i -e '/growpart/ s/^#//' /etc/cloud/cloud.cfg
# sed -i -e '/growpart/ s/^#//' /etc/cloud/cloud.cfg.d/90-hetznercloud.cfg
# apt install cloud-initramfs-growroot

Je možné oddíly zvětšit za běhu. Trochu neintuitivně se tak činí jakoby smazáním oddílu a vytvořením většího oddílu se stejným počátkem.


# sgdisk -d 4 $DISK
# sgdisk    -n4:0:0     -t4:BF00 $DISK
# partx --update $DISK

Pokud načtení selhalo, můžeme zkusit reformátovat po restartu.


partx: /dev/sda: error updating partition 4

Auto-rozšíření/ expanze je na poolu standardně vypnutá, což můžeme ale vyřešit manuálně. Můžeme se přesvědčit, že ZFS skutečně vidí větší pool.


# zpool get all | grep autoex
# zpool list
# zpool online -e rpool $DISK-part4
# fdisk -l
# zpool list

Je možné, že fdisk varuje např.:


Partition table entries are not in disk order.
GPT PMBR size mismatch (20971519 != 576716799) will be corrected by write.
The backup GPT table is not on the end of the device.

Je možné, že se podobná věc nestane, pokud se použije nástroj growpart. Také je možné tabulku oddílů upravit manuálně.


# sgdisk -e $DISK

Může být nutné provést restart, aby se změny projevily. Při běžném použití by ale na běh nemělo mít toto varování žádný vliv.

Nové superschopnosti

ZFS je dost možná jeden z nejkomplexnějších FOSS souborových systémů a správců úložiště, vykazuje aspekty specializované databáze. Proto je dobré si rychle připomenout užitečné příkazy.

Zobrazení základních údajů o úložišti, včetně toho, zdali byly detekovány či opraveny nějaké chyby.


# zpool status

Zajímavý je specifický příkaz zfs iostat, který je v principu podobný iostat z balíčku sysstat. Umí vypsat například histogram latencí přístupů do ZFS a na úložiště:


$ zpool iostat -w -T d

Nebo průměrné latence a informace k frontám.


$ zpool iostat -lq -T d

Obojí se může hodit při analýze problémů. Je možné taky připojit nakonec číslo a vypisovat statistiky průběžně v určitém intervalu. Naopak přepínač -H zobrazí údaje ve formátu vhodném pro skripty, typicky jsou údaje oddělené jedním tabulátorem.

Pro detailnější výpis údajů o datasetech a snapshotech, včetně úrovně kompresse a dalších detailů je možné použít např.


$ zfs list -o name,compressratio,used,usedbysnapshots,available,written,compression,recordsize,creation,mountpoint -t filesystem,snapshot

Vlastnosti jsou zdokumentované v zfsprops(7). Pro výpisy o cachovací vrstvě Advanced Replacement Cache lze použít skript  arc_summary.


# arc_summary

Nebo stručnější výpis či graf.


# arc_summary -s arc
# arc_summary -s archits
# arc_summary -g

Velikost ARC můžeme upravit zápisem do souboru /sys/module/zfs/parameters/zfs_arc_max v bajtech, či nastavením parametrů modulu, zde nastavujeme maximální velikost ARC na 12 Gigabajtů.


# cat /etc/modprobe.d/zfs.conf
options zfs zfs_arc_max=12884901888

Využívání místa ZFS

Čím plnější souborový systém je, tím těžší je najít dostatečně velké prázdné místečko pro uložení bloku a rychlost může klesnout. ZFS se snaží předejít problému, kdy je souborový systém zcela zaplněn pomalejší, ale úspornější alokační strategií, pokud se systém téměř plný. V současnosti je tato hranice nastavena na zbývající 4% volného místa. Podle způsobu využití a schopnosti reagovat může dávat smysl nepřekročit např. 92% zaplnění.

Dá se to považovat za určitou daň za výkon a CoW vlastnosti, kdy zvláště se snapshoty, klony apod. může být skutečné uvolnění místa složitější – smazání souboru nemusí nutně místo uvolnit, ba naopak může způsobit další obsazení místa v případě, že je soubor něčím v ZFS stále využíván. Předejít se dá tomuto problému tzv. rezervací místa. Standardně ale ZFS alokuje tzv. „slop“ (jakýsi přeliv). Konkrétní velikost je znovu exponent k dvojkovému základu pojmenovaný metaslab_shift s tím, že se jedná o 1/32 z kapacity poolu zaokrouhleno nahoru, ale ne více než 128 GB (237).


# zdb
# zdb -C rpool
# zfs list -o space

V našem případě je pro bpool slop 128 MB a pro rpool 8 GB, tedy ca. 6 % a resp. 4 % celkové kapacity poolů. Algoritmus a jeho konstanty lze nastavit parametry modulu, ale to je vysoko nad rámec článku. Nakonec sloupeček Available ve výstupu zfs list je metrika, kterou je dobré se řídit. V Debianu 12 reportuje podobné hodnoty také příkaz df -h, ale tento nástroj s ohledem na ZFS komunita obecně nedoporučuje používat.

Specifika při provozu s PostgreSQL

PostgreSQL v Debianu typicky instaluje datové soubory do složky /var/lib/postgresql. Pro ni můžeme tedy před instalací databáze vytvořit speciální dataset. Zde je určité vhodné vzít v potaz, že databáze typicky přepisuje stránky, které jsou v případě PostgreSQL velikosti 8 KB, ale velikost záznamu ZFS má standardně velikost 128 KB. Zatímco PostgreSQL je pro změnu velikosti stránky nutné rekompilovat, ZFS umí velikost záznamu měnit za běhu. Jen stávající data taková změna z důvodu CoW neovlivní (což lze vyřešit například kopií pomocí rsync někam bokem např. do jiného datasetu a poté přepsáním).

Pokud bychom nezměnili standardní velikost záznamu, znamenalo by čtení/ zápis 8 KB potenciálně čtení a zápis až 128 KB, tedy tzv. 16× zesílení/ amplifikace. Na druhou stranu komprese v ZFS se vždy realizuje na záznamu (recordsize). Čím větší záznam, tím větší okno pro kompresi. Pokud třeba víte, že nějaká tabulka v databázi slouží pro ukládání neměnných dat, vyčleňte ji do vlastního datasetu s jiným nastavením velikosti záznamu pro lepší kompresi či menší režii. Podobně se doporučuje vyčlenit do vlastního datasetu Write Ahead Log databáze, aby zbytečně nenafukoval snapshoty. Tomu se zde nebudeme věnovat.

Právě jako kompromis mezi lepší kompresí a potenciálním zesílením zvolíme jako velikost záznamu 16K místo 8K. Pokud se díky kompresi podaří zmenšit výsledná data na menší počet bloků (nastavených pomocí ashift), tak je možné, že se bude realizovat menší počet zápisů a záznam bude menší, nejméně však jeden blok. Lze vycházet z toho, že data v databázi budou komprimovat v poměru 2:1 a bude tedy potenciálně možné zapsat dvě stránky za cenu jedné.

Také můžeme vycházet z toho, že většina dat v databázi nějak komprimovatelná bude a využít tedy např. algoritmu zstd, který je dostatečně rychlý a nemá v Debianu 12 zmíněnou funkci „early abort“, ale šetří místem o něco více, než LZ4. Menší databáze na disku šetří zdroje i mimo diskový úložný prostor, protože i ARC a případně SSD cache L2ARC udržuje data v komprimované podobě. Jen data, která se opravdu často čtou (např. index v databázi) udržuje v dekomprimované podobě, standardně se jedná o maximálně 1/32 velikosti ARC.

Nakonec je také dobré vypnout zaznamenávání metadat o přístupu k souborům, protože bychom zbytečně zapisovali data, která nás skoro určitě nezajímají.


# zfs create -o atime=off -o compression=zstd-1 -o recordsize=16K rpool/var/lib/postgresql

S tímto základním nastavením by už měl PostgreSQL v menších nasazeních fungovat dostatečně slušně, aby bylo možné nasbírat zkušenosti a případně zvážit další nastavení. Jednoduše můžeme vytvořit snapshot databáze.


# zfs snapshot rpool/var/lib/postgresql@$(date +"%4Y-%m-%dT%H%M%S")

a případně se ke snapshotu vrátit


# systemctl stop postgresql
# zfs rollback rpool/var/lib/postgresql@2023-07-11T094501
# systemctl start postgresql

Pokročilé nastavení ZFS pro provoz databází

Pro pokročilé nastavení se lze zaměřit na nastavení primarycache a logbias. Standardní je nastavení all resp. latency. Databáze si ale data typicky cachuje dostatečně dobře sama a může tedy dávat smysl nastavit v ZFS cachování metadata. Potom lze zmenšit velikost ARC a naopak zvětšit shared_buffers v nastavení PostgreSQL. Podobně může dávat smysl nastavit logbias na throughput, což ale může vést k fragmentaci bloků na disku. CoW má obecně tendenci fragmentovat, není potřeba tedy tuto potenciálně nepříjemnou vlastnost posilovat.

Dále dává smysl nastavit v postgresql.conf   full_page_writes na „off“, tyto garance poskytuje již ZFS. Další tipy lze zvážit např. v prezentaci a související přednášce Seana Chittendena, který navrhuje například snížit při nastavení modulu ZFS zfs_txg_timeout ze standardních pěti sekund na jednu sekundu a na datasetu databáze nastavit sync=disabled.

Za cenu potenciální ztráty až jedné sekundy dat se prý může zvednout výkon vícenásobně tím, že synchronní zápisy budou interně realizovány jako asynchronní. Toto nastavení nelze doporučit, ale pro lidi, co přesně ví, co dělají to může značně snížit nároky na hardware.

Na fyzickém hardwaru se můžete rozhodnout, přidat pro ZFS Intent Log speciální zařízení SLOG, dle dostupnosti a finančních možností zařízení s co nejnižší latencí zápisu a nejvyšší výdrží, třeba RAM-disk se zálohou proudu pomocí ultra-kondenzátorů, SLC SSD, či před ukončením výroby Intel Optane.

Nakonec od kernelu 6.1 lze nezávisle na ZFS otestovat vliv MGLRU na výkon databáze. Řada těchto rad se zrovna tak vztahuje i na provoz databáze MySQL/ MariaDB, která používá standardně 16 KB stránky, tam by se asi hodilo ponechat recordsize na této velikosti.

Video návod

Standardní instalaci Debianu 12 na souborovém systému EXT4 jsme bez vnější pomoci převedli na souborový systém OpenZFS. Máme dostatečné vybavení na získávání dalších zkušeností se ZFS například jako úložištěm pro databázi PostgreSQL.

ict ve školství 24

Návod byl autorem článku skoro identicky s obsahem článku natočen v anglickém jazyce:

Autor článku

Pracuje na OrgPad.com jako spoluzakladatel a člověk zodpovědný za infrastrukturu. Rád programuje v Clojure a ClojureScriptu.