Wybór systemu plików dla serwerów AI: XFS, ZFS, ext4 i dlaczego nie ma tu Btrfs
Jeśli właśnie wydano €40 tys. na GPU, system plików, na który trafią dane, zasługuje na więcej niż pięć minut namysłu. Instalator Ubuntu bez wahania rozkłada ext4 na wszystkim; dla wielu obciążeń AI jest to właściwa odpowiedź, dla niektórych nie. Zły system plików na wolumenie z 50 TB danych oznacza utratę przepustowości, utratę danych albo DataLoader stojący bezczynnie za wąskim gardłem metadanych.
Omawiamy tu, jaki system plików umieścić w którym miejscu serwera AI klasy Kentino (4–8 GPU, NVMe systemowe, NVMe scratch, wolniejsza warstwa masowa). Ubuntu 22.04 / 24.04, jądro 6.x.
Wersja skrócona
| Punkt montowania | Obciążenie | System plików | Dlaczego |
|---|---|---|---|
/ (boot, root) |
System, pakiety, logi | ext4 | Nudny, sprawdzony, odzyskiwalny |
/scratch (NVMe RAID0) |
Aktywne shardy treningowe, zapisy tymczasowe | XFS | Przepustowość, równoległe I/O na katalogach |
/data (RAID10) |
Zbiory treningowe, checkpointy modeli | XFS lub ZFS | XFS dla surowej szybkości; ZFS, gdy potrzebne są migawki i sumy kontrolne |
/archive (RAID-Z2/6) |
Zakończone przebiegi, zimne składowanie zbiorów | ZFS | Kompresja, sumy kontrolne, migawki |
/home |
Katalogi domowe, notebooki | ext4 | Małe pliki, niewielka rywalizacja o dostęp |
Wersja na jeden akapit: ext4 na system, XFS na gorący scratch, ZFS tam, gdzie integralność znaczy więcej niż ostatnie 10% przepustowości, i żadnego Btrfs na produkcji.
ext4 — nudne ustawienie domyślne, które zwykle jest trafne
ext4 jest domyślnym systemem plików w Ubuntu, Debianie, Fedorze i RHEL od ponad dekady. Dobrze poznany, odzyskiwalny za pomocą fsck, 16 TB na plik, 1 EB na wolumen. Nic ekscytującego — i o to właśnie chodzi.
ext4 warto użyć na partycji głównej (każdy live USB Ubuntu potrafi ją naprawić; znacznie mniej osób wie, jak zaimportować zpool), na /home oraz na magazynach obrazów maszyn wirtualnych w małej i średniej skali.
Gdzie ext4 przestaje być właściwym wyborem: szeroko równoległe I/O (dziennik szereguje aktualizacje metadanych — dwudziestu pracowników DataLoadera na jednym wolumenie ext4 utknie na dzienniku na długo przed wysyceniem NVMe); pojedyncze ogromne pliki (obsługiwane, ale alokacja ekstentów jest mniej przyjazna niż w XFS dla sekwencyjnych plików >100 GB — shardy treningowe, checkpointy, korpusy wideo); migawki (brak natywnych; cienkie migawki LVM działają, ale są toporne).
mkfs.ext4 -L root /dev/nvme0n1p2
Jeśli ktoś przyłapie się na egzotycznym strojeniu ext4 pod obciążenie AI, wybrał zły system plików.
XFS — właściwa odpowiedź dla gorącego scratcha i danych treningowych
XFS powstał w SGI jako system plików do sekwencyjnego I/O o wysokiej przepustowości na dużych macierzach. To dziedzictwo pasuje do obciążeń AI niemal idealnie: duże pliki (shardy treningowe, parquet, archiwa tar webdataset, checkpointy), odczyty sekwencyjne, wielu równoległych czytelników i tolerancja na założenie, że „obciążenie odtworzy te dane”.
Co XFS robi, a czego ext4 nie: grupy alokacji — wolumen dzieli się na 4–32 niezależne grupy, które czytają i zapisują równolegle bez globalnej blokady dziennika, czyli dokładnie to, czego potrzebuje wielowątkowy DataLoader; opóźniona i spekulatywna prealokacja, dzięki której duże zapisy sekwencyjne trafiają w ciągłe ekstenty; brak praktycznego limitu rozmiaru pliku (8 EB); oraz xfs_growfs do niezawodnego rozszerzania jedną komendą po powiększeniu macierzy RAID.
Czego nie robi: brak natywnych migawek, brak sum kontrolnych danych (metadane chronione CRC od 2014 roku), brak zmniejszania — tylko powiększanie.
Rozsądne mkfs.xfs dla wolumenu scratch na 8× NVMe RAID0, chunk 64 KiB:
# su = stripe unit = rozmiar chunka; sw = liczba dysków danych
mkfs.xfs -f -d su=64k,sw=8 -l size=512m -L scratch /dev/md0
# largeio + swalloc: zachowanie dostosowane do I/O >1 MB
# allocsize=1g: agresywna prealokacja pod duże zapisy sekwencyjne
# noatime: oszczędza jeden zapis na odczyt przy dużym obciążeniu DataLoadera
mount -o noatime,largeio,swalloc,allocsize=1g /dev/md0 /scratch
Najbardziej liczą się noatime i largeio. XFS na małych plikach jest akceptowalny, ale to nie jego mocna strona — przy setkach milionów obrazów <16 KB lepiej przepakować je do webdataset / parquet / lmdb niż zmieniać system plików (patrz niżej).
ZFS — właściwa odpowiedź, gdy liczy się integralność, migawki lub kompresja
ZFS to system plików, menedżer wolumenów i warstwa programowego RAID w jednym:
- Sumy kontrolne end-to-end. Bit rot wykrywany przy odczycie i cicho korygowany przy redundancji. Dla zbioru 50 TB leżącego dwa lata na talerzach ma to znaczenie; dla tymczasowego scratcha na NVMe przez sześć tygodni nie ma.
- Migawki i klony. Atomowe, COW, praktycznie darmowe. Migawka przed przebiegiem, odgałęziony klon, wycofanie — to przepływ pracy, dla którego ZFS istnieje.
-
Wbudowana kompresja.
lz4domyślnie włączony;zstddla lepszych współczynników. Na tekście, JSON-ie i parquecie kompresja zwykle zwiększa efektywną przepustowość, bo koszt CPU jest mniejszy niż zaoszczędzone I/O. - ARC — własny cache odczytu w RAM, niezależny od page cache Linuksa. Słynna pułapka.
- Natywny RAID-Z — bez mdadm.
Czego ZFS nie robi dobrze w AI: zjada RAM (domyślny ARC to ~50% pamięci systemowej — na serwerze z 512 GB to 256 GB, których jądro nagle nie ma, a zadania treningowe liczące na page cache lub huge pages są ubijane przez OOM; ARC trzeba ograniczyć); nie jest najszybszy przy surowych odczytach sekwencyjnych (dobrze dostrojony XFS na NVMe RAID0 bije ZFS o 15–30% — sumy kontrolne i COW kosztują realne cykle); ignoruje O_DIRECT (całe I/O przechodzi przez ARC z założenia — w porządku dla większości obciążeń, ale to twarda niekompatybilność z GPUDirect Storage).
Strojenie ARC — jedyne pokrętło, które trzeba ustawić
Na świeżej instalacji Ubuntu + ZFS na maszynie z 512 GB ZFS zajmuje 256 GB na ARC. zfs_arc_max należy ustawić przy instalacji, a nie po pierwszym OOM.
# Ograniczenie ARC do 64 GB. W bajtach, nie w GB. 64 * 1024^3 = 68719476736
echo "options zfs zfs_arc_max=68719476736" | sudo tee /etc/modprobe.d/zfs.conf
echo 68719476736 | sudo tee /sys/module/zfs/parameters/zfs_arc_max # zastosuj teraz
sudo update-initramfs -u # utrwal
Zasada kciuka: ograniczyć ARC do 10–20% pamięci RAM na serwerze AI. Klasyczne wymiarowanie „2 GiB bazowo + 1 GiB na każdy TiB” dotyczy maszyn storage'owych; na serwerze AI większość RAM-u jest zarezerwowana na wagi modelu, aktywacje i page cache frameworka.
Rozsądny układ ZFS dla warstwy danych serwera AI
# 8 × 16 TB SAS w dwóch vdev-ach RAID-Z2 (po 6+2) — dwa vdev-y dają 2× IOPS
zpool create -o ashift=12 \
-O compression=zstd -O atime=off -O xattr=sa -O recordsize=1M \
data \
raidz2 /dev/sd[a-h] \
raidz2 /dev/sd[i-p]
# Zbiory danych wymiarowane pod swoje obciążenie
zfs create -o recordsize=1M data/datasets # duże shardy treningowe
zfs create -o recordsize=128k data/checkpoints # mieszane rozmiary
zfs create -o recordsize=16k data/metadata # małe pliki (json, yaml)
zfs snapshot data/datasets@pre-run-2026-05-14
zfs rollback data/datasets@pre-run-2026-05-14
ashift=12 = bloki 4 KiB — pomyłka przy tworzeniu puli jest nie do naprawienia bez jej zniszczenia. recordsize=1M dla obciążeń z dużymi plikami (domyślne 128 KiB jest za małe dla wielogigabajtowych shardów). xattr=sa zapisuje atrybuty rozszerzone inline, znacznie szybciej niż domyślnie.
Btrfs — na produkcji AI zasadniczo odradzamy
Btrfs ma koncepcyjnie ten sam zestaw funkcji co ZFS — COW, migawki, sumy kontrolne, wbudowany RAID. Na papierze się zgadza; w praktyce nie polecamy go na serwerach Kentino. RAID5/6 ma znane błędy prowadzące do utraty danych, które sam projekt oznacza jako nieprzeznaczone na produkcję (RAID1/10 są stabilne, tryby parzystości nie); wydajność mocno spada przy wzorcu fragmentacji, jaki generują obciążenia AI (checkpointy gradientów, cache'e); wydajność migawek załamuje się powyżej kilkuset migawek, a przepływ pracy „migawka na eksperyment” osiąga ten pułap w ciągu kwartału; narzędzia do replikacji, monitoringu i harmonogramowania scrubów są mniej dojrzałe niż w ZFS czy XFS. Na laptopie w porządku. Dla serwera z wieloma GPU i dziesiątkami TB danych treningowych — XFS albo ZFS.
Układy RAID — z czym łączyć system plików
| Poziom RAID | Zastosowanie | Redundancja | Uwagi |
|---|---|---|---|
| RAID0 | Scratch (tymczasowy, odtwarzalny) | Brak | Tylko dla danych możliwych do odtworzenia |
| RAID10 | Zbiory treningowe, checkpointy | 1 na każde lustro | Najlepsza szybkość i bezpieczeństwo gorących danych |
| RAID-Z1 / RAID5 | Unikać w nowych wdrożeniach | 1 dysk | Czasy odbudowy dysków 16 TB+ czynią Z1 ryzykownym |
| RAID-Z2 / RAID6 | Archiwum, zimne składowanie zbiorów | 2 dyski | Właściwy wybór dla dużych wolumenów |
| RAID-Z3 | Szerokie macierze (12+ dysków) | 3 dyski | Tylko gdy wymusza to szerokość macierzy |
Przy dyskach 16 TB+ odbudowa pojedynczego dysku może trwać 24–72 godziny, a druga awaria wśród dysków z tej samej partii w tym oknie nie jest zaniedbywalna. Pojedyncza parzystość nie jest już bezpiecznym ustawieniem domyślnym. Lepiej użyć Z2/RAID6 albo luster.
Małe pliki kontra duże pliki
Pytanie, które zastawia pułapkę częściej niż sam wybór systemu plików: jak zachowuje się system plików przy 50 milionach obrazów po 4 KB w drzewie katalogów? Odpowiedź dla każdego systemu plików opisanego tutaj: źle.
Metadane na plik (i-węzeł, dentry, znaczniki czasu, atrybuty rozszerzone) zajmują 200–500 bajtów niezależnie od rozmiaru pliku — 50 milionów drobnych plików pochłania 10–25 GB metadanych, zanim zapisze pierwszy bajt danych obrazu. Wzorzec open/read/close dociąża stronę metadanych i zatrzymuje pracowników DataLoadera.
Rozwiązaniem nie jest system plików. Małych plików nie należy przechowywać jako plików. Trzeba je spakować do webdataset / tar / parquet / lmdb / hdf5 / safetensors i strumieniować sekwencyjnie — WebDataset z PyTorcha i NVIDIA DALI właśnie tego oczekują. System plików widzi wtedy niewielką liczbę dużych plików, z czym każdy z opisanych tu systemów radzi sobie dobrze. Jeśli pojedyncze pliki muszą pozostać w takiej skali, ZFS z recordsize=16k i xattr=sa jest opcją najmniej złą, ale prawdziwą odpowiedzią pozostaje przepakowanie.
Bezpośrednie I/O, page cache i O_DIRECT
PyTorch opiera się na linuksowym page cache przy powtarzanych epokach zbioru — epoka 1 z dysku, epoki 2..N z RAM-u. Niektóre obciążenia używają O_DIRECT, aby ominąć cache przy dużych odczytach danych dotykanych raz. GPUDirect Storage od NVIDII idzie dalej, przesyłając dane przez DMA z NVMe prosto do GPU.
ext4 i XFS respektują O_DIRECT poprawnie — wyrównane bezpośrednie I/O, bez page cache. ZFS ignoruje O_DIRECT (całe I/O przez ARC, z założenia): w porządku dla większości obciążeń, ale to twarda niekompatybilność z GPUDirect Storage. Wyrównanie ma znaczenie — bufor i offset muszą być wyrównane do rozmiaru bloku urządzenia (zwykle 4 KB).
Jeśli w planach jest GPUDirect Storage zasilający maszynę z 8 GPU pełnym pasmem, /data musi być na XFS na NVMe. Jeśli ktoś nie wie, czym jest GPUDirect Storage, a trening jest ograniczony mocą obliczeniową, na razie nie jest to potrzebne.
Multipath NVMe
Dwuportowe dyski NVMe U.2 / E1.S prezentują się jako dwie ścieżki PCIe każdy. Linuksowy sterownik NVMe obsługuje natywny multipath (nvme_core.multipath=Y, domyślnie w nowoczesnych jądrach), dając przełączanie awaryjne i równoważenie obciążenia metodą round-robin. Na poziomie systemu plików jest to niewidoczne, ale ma znaczenie przy awariach (bez tego awaria ścieżki lub HBA powoduje zniknięcie dysku i panikę systemu plików) oraz dla pasma (dysk U.2 z multipathem osiąga ~14 GB/s wobec 7 GB/s na jednej ścieżce).
cat /sys/module/nvme_core/parameters/multipath # oczekiwane: Y
nvme list-subsys
Dla konsumenckich NVMe M.2 (konfiguracje desktopowe klasy 5090) multipath nie ma zastosowania — jest jedna ścieżka. Dla maszyn 8-GPU na EPYC z korporacyjnymi U.2 / E1.S w backplane z dwoma kontrolerami warto go zostawić włączonego.
Narzut sum kontrolnych
| Operacja | Koszt CPU na GB, jeden rdzeń | Uwagi |
|---|---|---|
| ZFS fletcher4 (domyślnie) | ~50–100 ms | Domyślna suma kontrolna bloku danych |
| Kompresja ZFS lz4 | ~150–250 ms | Zwykle zwraca się dzięki mniejszemu I/O |
| Kompresja ZFS zstd | ~400–800 ms | Lepszy współczynnik, wyższy koszt |
| XFS / ext4 (bez sum kontrolnych danych) | 0 | Tylko metadane |
Na hoście z 64-rdzeniowym EPYC koszt jest niewidoczny. Na 16-rdzeniowym Xeonie pod ciągłym obciążeniem ZFS potrafi zabrać 5–10% efektywnej mocy CPU — to, czy ma to znaczenie, zależy od tego, czy trening jest ograniczony przez CPU na preprocessingu (często tak przy wizji), czy przez GPU (często tak przy dużych LLM).
Uczciwe ujęcie: ZFS wyłapuje bit rot, którego inaczej nikt by nie zauważył, dopóki model nie wytrenuje się na subtelnie uszkodzonych danych. Dla zbioru budowanego przez pół roku 5–10% jest tego warte. Dla tymczasowego scratcha odtwarzanego co tydzień nie jest.
NFS dla zbiorów współdzielonych między węzłami klastra
Gdy serwerów jest więcej niż jeden, pytanie o to, gdzie mieszka zbiór danych, robi się ciekawe. Trzy wzorce:
- Kopia na każdym węźle. Proste, marnuje pojemność. Wykonalne przy <1 TB i maksymalnie 4 węzłach.
- NFS z dedykowanego serwera plików. Jedna kanoniczna kopia. Wąskim gardłem jest sieć — minimum 25 GbE, a 100 GbE przy treningu wielowęzłowym.
- Równoległy system plików (BeeGFS, Lustre, WekaFS). Większy wysiłek, skaluje się tam, gdzie NFS już nie daje rady.
Dla 1–4 węzłów właściwy jest NFS. Serwer: XFS na RAID10 z NVMe, eksport przez NFSv4. Klient: montowanie z nconnect=8, aby zrównoleglić ruch na wiele połączeń TCP.
# Serwer /etc/exports
/data/shared 10.0.10.0/24(rw,async,no_subtree_check,no_root_squash)
# Klient
mount -t nfs -o vers=4.2,nconnect=8,proto=tcp,rsize=1048576,wsize=1048576 \
storage01:/data/shared /mnt/shared
NFS po 100 GbE z nconnect=8 daje stabilnie 8–10 GB/s — wystarczająco dla 16 GPU przy typowym treningu wizyjnym lub LLM. Powyżej tego BeeGFS / Lustre — to temat na osobny artykuł.
Pełny przykład: 8 GPU na EPYC, 24 NVMe + 8 SAS
2× 480 GB NVMe (boot) → md RAID1 → ext4 → /
8× 7.68 TB NVMe (scratch) → md RAID0 → XFS → /scratch
8× 7.68 TB NVMe (data) → md RAID10 → XFS → /data
8× 16 TB SAS (archive) → 2× RAID-Z2 → ZFS → /archive
ZFS ARC: 64 GB z 512 GB RAM. Multipath NVMe: włączony. Eksport NFS: /data po 100 GbE.
mkfs.ext4 -L root /dev/md0
mkfs.xfs -f -d su=64k,sw=8 -l size=512m -L scratch /dev/md1
mkfs.xfs -f -d su=64k,sw=4 -l size=512m -L data /dev/md2
zpool create -o ashift=12 \
-O compression=zstd -O atime=off -O xattr=sa -O recordsize=1M \
archive raidz2 /dev/sd[a-h]
echo "options zfs zfs_arc_max=68719476736" > /etc/modprobe.d/zfs.conf
update-initramfs -u
Surowa przepustowość NVMe tam, gdzie jest potrzebna, redundancja tam, gdzie danych nie da się odtworzyć, i skompresowana, kontrolowana sumami warstwa masowa na całą resztę.
Co zrobić dalej
Przed sformatowaniem czegokolwiek warto odpowiedzieć:
-
Jak duży jest największy zbiór danych, w TB? To wymiaruje
/datai mówi, czy potrzebna jest warstwa archiwalna. -
Czy dane treningowe da się odtworzyć ze źródła prawdy? Jeśli tak, RAID0 na scratch wystarczy. Jeśli nie, minimum RAID10, a na
/datawarto rozważyć ZFS. - Ile GPU czyta jednocześnie z tego samego wolumenu? Powyżej ~8 równoległych procesów DataLoadera grupy alokacji XFS zwracają się wobec ext4.
- Czy potrzebne są migawki? „Odgałęzić zbiór i wycofać zmiany” → ZFS. W przeciwnym razie XFS jest szybszy i prostszy.
- Jaki jest budżet RAM i ile może zabrać ZFS? Decyzja przed instalacją, nie po pierwszym OOM.
Wybór systemu plików nie jest najciekawszą decyzją dotyczącą serwera AI, ale jedną z niewielu naprawdę trudnych do cofnięcia. Warto wybrać świadomie, sformatować raz i zająć się czymś innym.
To część Kentino Wiki, serii referencyjnej o mocy obliczeniowej AI, pamięci masowej i systemach, które je łączą. Komentarze i sprostowania mile widziane pod adresem info@kentino.com.