ZFS

A Unix/Linux szerverek üzemeltetése wikiből

A ZFS az Oracle (leánykori nevén Sun) fájlrendszere, amely egyfajta kötetkezelőt és szoftveres RAID-megvalósítást is tartalmaz. Fő előnyei:

  • Rendkívül kényelmes adminisztráció;
  • SSD-k ésszerű használata (cache-ként);
  • ellenőrző összegek (észleli és esetenként javítja az amúgy észrevétlen hibákat);
  • a látszólag különböző rétegbe való funkciók (RAID, kötetkezelés, fájlrendszer) integrálása révén pl. RAID-helyreállításkor csak a foglalt területeket kell szinkronizálnia (és egyéb előnyök is származnak ebből; l. alant);
  • röptömörítés;
  • in-line deduplikáció;
  • stb.

2012-ben Solarishoz, IllumOS-származékokhoz/-szerűségekhez (OpenSolaris, NexentaStor, illumian, Dyson -- ez egy IllumOS-kernelre épülő Debian; kíváncsi vagyok, mi lesz belőle --, SmartOS, OpenIndiana stb.), FreeBSD-hez és Linuxhoz létezik elérhető implementáció. Az Apple egy darabig dolgozott egy Mac OS X-porton, de úgy tűnik, felhagyott vele. A http://open-zfs.org/wiki/Features oldalon nyomon követhető, a nyílt forrású implementációk hol tartanak egymáshoz képest.

Eredetileg a Solarishoz fejlesztették ki; ez akart lenni a Nagy Végső Válasz (majdnem akkora, mint a 42) az összes fájlrendszerproblémára ("The Last Word in Filesystems").

Tartalomjegyzék

1 Alapfogalmak, építőelemek

  • Pool: kb. az LVM VG megfelelője. Fizikai adattárolók (vdevek, l. lejjebb) összessége; az ezekben rendelkezésre álló tárhelyet oszthatjuk ki zvol-ok és zfs-példányok között.
  • vdev: kb. az LVM PV megfelelője. Egy fizikai diszk, vagy egy több diszkből álló tömb.
    • A poolban tárolt adatokat a zfs RAID0-szerűen szétteríti az egyes vdevekre.
    • Egy pool bővíthető újabb vdevekkel;
    • a poolhoz hozzáadott vdevek eltávolítása (speciális esetektől eltekintve) nem lehetséges.
      • Bővítéskor nem történik újracsíkozás; az újonnan felírt adatok azonban már részben az új vdev(ek)re is kerülnek.
      • Maguk a vdevek érdemben nem bővíthetők!
    • vdevek lehetséges típusai:
      1. file (nem javasolt);
      2. diszk (vagy partíció);
        • Ha megnő a mérete, fel tudjuk használni a plusz helyet.
      3. mirror (RAID1, akárhány diszkből);
      4. raidz, raidz1 (kb. RAID5);
      5. raidz2 (kb. RAID6);
      6. raidz3 (csíkozott, paritásos, három paritásblokkot használó elrendezés; így bármely három diszk kiesését túlélheti);
        • A raidz-s vdevekhez sajnos nem tudunk új diszket adni.
      7. spare (melegtartalék);
      8. log (más néven ZIL; a szinkron írások biztonságos gyorsítására, szerializálására szolgáló opcionális írás-"cache", pl. SSD);
        • A log maga is lehet mirror; sőt, ajánlatos mirrorba tenni, hiszen a szinkron írásaink eredménye egy ideig csak a ZIL-ben van meg.
        • A ZIL igazából nem cache, hanem inkább journal: olyan tartós tár, amibe gyorsan ki lehet írni a szinkron írandó adatot és így gyorsan vissza lehet térni az írást kezdeményező alkalmazásba; a ZILbe írt adat pedig akkor sem vész el, ha közben elmegy az áram.
        • Így a ZIL haszna igazából az, hogy a szinkron írásokat is lehet RAMban pufferelni (hiszen a ZILben megvannak arra az esetre, ha elszállunk és törlődik a RAM): tehát a szinkron írások majdnem olyan gyorsak lesznek, mint az aszinkron írások.
        • A rendszer induláskor a ZILben tárolt kijelölt műveleteket el kell végezni a diszkeken is.
      9. cache (a poolhoz tartozó opcionális másodlagos olvasás-cache, pl. SSD).
        • A zfs terminológiájában az ilyen cache az "L2ARC" (Level 2 Adaptive Replacement Cache).
          • Az L2ARC kifejezetten SSD-re van optimalizálva: pl. nagy blokkokban ír.
          • Gond vele, hogy nem perzisztens: reboot után a cache üres, és csak nagyon lassan (adott esetben napok alatt) "melegszik be".
        • A cache jelenleg nem lehet sem mirror, sem raidz[123].
        • A cache alapértelmezés szerint csak a véletlen (nem prefetch-elhető) olvasásokat cache-eli.
  • dataset: olyan objektum, amelynek a pool helyet biztosít. Egy poolban max. 248 darab lehet. Típusai:
    • zvol: kb. az LVM LV megfelelője. Egy blokkeszköz, amin tetszőleges fs-t létrehozhatunk (vagy pl. odaadhatjuk egy virtuális gépnek, mint diszket).
      • Snapshotolható, deduplikálható, röptömöríthető anélkül, hogy a benne levő fs-nek erről tudnia kéne.
      • Kiajánlható iSCSI-val.
      • Csak speciális esetben érdemes használni; többnyire jobb:
    • filesystem: egy zfs-példány.
      • Mountolható, snapshotolható, deduplikálható, röptömöríthető stb.
      • Kiajánlható NFS-sel, CIFS-szel (windows-os share-ként).
    • snapshot: egy zvol vagy filesystem adott pillanatbeli, csak olvasható képe.
    • clone: egy snapshot írhatóvá tett másolata; annyi helyet foglal, amennyi módosítást tárol az eredeti snapshothoz képest.
      • A clone a zfs promote művelettel filesystem-mé léptethető elő; ekkor az eredeti fájlrendszer, amelyről a clone készült, válik klónná (és így megszüntethető). Ez akkor jó, ha pl. a clone-ban kipróbáltunk valamit és elégedettek vagyunk vele, úgyhogy meg szeretnénk tartani.
  • A ZFS logikai/funkcionális részei:
    • ZFS POSIX Layer (ZPL) -- ettől látszik fájlrendszernek.
      • Tranzakciókkal bízza meg az alatta levő réteget; ezek vagy teljes egészükben végbemennek, vagy egyetlen részük sem teljesül.
    • Data Management Unit (DMU).
      • Ez egy általános objektumtároló; ez szavatolja az adatintegritást (ellenőrző összegekkel).
      • Használható a ZPL nélkül is (ez a zvol); pl.
        • közvetlenül épülhet rá adatbázis, vagy
        • csinálhatunk egy zvolban tetszőleges egyéb fs-t (a Solaris/IllumOS swapelni is tud zvolra; a Linux is, de a zvol nem lehet tömörített és resume-olni semmiképpen nem tud róla), vagy
        • exportálhatjuk iSCSI-n.
    • Storage Pool Allocator (SPA).
      • Ez kezeli az egész alatti fizikai diszkeket.
      • Ez tömörít, blokkszinten; emellett automatikusan megtalálja a "lukakat" (csupa nullás területeket) a fájlokban és azoknak még csak nem is allokál helyet (tömörítettet sem).

2 Jellemzői, működése

  • Rettenetesen nagy bír lenni (logikailag több adatot tud tárolni, mint amennyi a Földön található összes anyag segítségével fizikailag tárolható lenne).
    • A meta-adatok is dinamikusan nőnek: akárhány fájl lehet egy könyvtárban, akárhány fájl lehet összesen stb.
    • Emiatt csak 64bites rendszeren ajánlott használni.
  • Nem kell előre eldöntenünk, mi mekkora legyen: a rendelkezésre álló helyből ("pool") a zfs maga gazdálkodik (nyilván állíthatunk be korlátokat).
    • Egy-egy "fájlrendszer", amit létrehozunk, igazából csak a benne tárolt fájlok adminisztratív csoportosítását szolgálja.
    • Nem kell várnunk, hogy a du(1) megnézze, mi foglal sok helyet; ha a releváns könyvtárak külön datasetekben vannak, a df(1) azonnal megmondja.
  • Hierarchikus. A tank poolban lehet egy tank/srv nevű zfs-példány, abban egy tank/srv/www, abban egy tank/srv/www/www.domain.com stb. Ezek nemcsak alkönyvtárak, hanem különálló filerendszerek;
    • önállóan mountolhatóak,
    • snapshotolhatóak
    • kvótázhatóak, és
    • alapbeállításaikat a felmenőiktől öröklik. Ez állati kényelmes: ha sok fájlrendszeren akarunk egy propertyt átállítani, átállítjuk a közös ősükben és meg is vagyunk.
      • Feltéve, hogy az adott property értékét minden leszármazott fájlrendszer a szülőtől örökli; ha kézzel módosítjuk, megszűnik az öröklés (de vissza lehet térni hozzá: zfs inherit propertyneve poolneve/datasetneve).
    • Egyes adminisztrációs feladatok -- Solarison/IllumOS-en -- delegálhatók konkrét felhasználóknak: tehát pl. megengedhetjük, hogy Józsi snapshotolja a saját home-ját.
      • A snapshotok a mountpoint/.zfs/snapshot/snapshotneve könyvtárakon keresztül a rendszergazda közreműködése nélkül is elérhetők.
    • "Policy follows the data": egyetlen zfs mount -a paranccsal be lehet mountolni az összeset egyszerre, akkor is, ha másik gépbe raktuk a poolt, mert a mountpoint is benne tárolódik, nem az fstabban (kivéve, ha legacy-ra állítjuk a mountpointot).
  • Architektúrafüggetlen.
    • A byte-sorrendet úgy kezeli, hogy a blockokban egy bit jelöli, az adott block big vagy little endian;
      • az írás mindig natív byte-sorrenddel történik;
      • olvasáskor byte-csere kell, ha a beolvasott blokk byte-sorrendje nem azonos a CPU-éval.
  • A blokkméret változó (de extenteket nem használ).
    • Ha egy objektum 42k, akkor egy 42k-s blokkban fogja tárolni.
  • NTFS-jellegű ACL-eket valósít meg (NFSv4 ACL; jobb, mint a POSIX ACL).
  • Fájlok tulajdonosa windowsos SID is lehet (nemcsak unixos UID/GID).
  • Tranzakciókezelést használ; a lemezen elvileg mindig csak konzisztens állapot létezik, tehát elvileg sosem kell fsck-zni.
    • Sosem helyben írja felül az adatokat; az új adatokat új helyre írja, majd "átállítja az érvényes adatra mutató pointert".
    • Így automatikusan előáll egy olcsó (értsd: gyors) snapshot is (hiszen az eredeti adatok még megvannak). A snapshotolás overheadje csak annyi, hogy ezeknek a "pointereknek" a snapshot létrehozásakor érvényes állapotát kell megőrizni.
  • Integritásellenőrzés is van benne, így elvileg véd a kábelhibákból és hasonlókból eredő adathibák ellen.
    • Ha olvasáskor hibát talál, a poolban meglevő redundancia segítségével javítja; ha nincs redundancia, I/O hibát kapunk, és nem férünk hozzá a hibás adathoz. Ez nem mindig jó.
  • Blokkszintű deduplikációt valósít meg, ha szeretnénk.
  • "RAID-Z" (RAID5-szerűség); a fő előnyei elvileg:
    • Nincs szükség read-modify-write-ra (mint a RAID5/RAID6 esetében), mert dinamikus csíkszélességet használ, így minden írás mindig teljes csík.
      • Példa: ha négy diszkünk van és 3000 byte-ot írunk, akkor az első három diszkre kiír 1024-1024 byte-ot és a negyedikre ezek paritását. (Azért 1024-et, mert mindig teljes fizikai blokkokat ír; ebben a konkrét esetben 72 byte-ot elpazarol.)
    • A tranzakciókezelésnek köszönhetően elkerüli a "write hole" problémát: amíg nem íródott ki a paritás is és az adat is, addig nem válik érvényessé az új adat (nem mutat rá "pointer").
  • Az integritásellenőrzésnek köszönhetően akkor is kijavítja a hibákat, ha az olvasás látszólag sikerül (tehát amikor egy hagyományos RAID-megvalósítás nem nyúlna a paritáshoz).
  • Resync ("resilvering") esetén csak azokat a blokkokat másolja, amikben tényleg van adat.
  • Érdemes "scrub"-okat (teljes integritásellenőrzést) ütemezni; ilyenkor a háttérben végigolvassa az adatokat és kijavítja az esetleges hibákat, ha van redundancia. Ha nincs, akkor megtudjuk, melyik fájlunk vált olvashatatlanná. A scrub -- legalábbis Linuxon -- elég lassú tud lenni, és sajnos nem lehet részletekben elvégezni.
  • A "fontos" metaadat-blokkokat több példányban tárolja, mint a kevésbé fontosakat.
    • Egyébként egyes fs-ek szintjén is kérhetjük, hogy az adott fs-t két vagy több példányban tárolja; így akár egyetlen diszken is tudjuk a fontos adatainkat -- nyilván jókora sebességveszteség árán -- redundánsan tárolni. Ez a hibás szektorok ellen véd, a teljes diszk kiesése ellen nyilván nem. Főként notebookon lehet hasznos.
  • Automatikusan csíkoz a rendelkezésre álló vdevek fölött.
    • Ha új vdevet rakunk be, arra is elkezd csíkozni és a tranzakció-alapú írások miatt idővel, szép lassan, minden adat az összes diszkre szétterül.
  • (Mostanáig) nagyon komolyan vette/veszi a kompatibilitást/hordozhatóságot:
    • Minden poolnak (és minden zfs-példánynak) van egy verziószáma.
    • Minden megvalósítás egy konkrét verziószámig bezárólag minden verziójú poolt/fs-t képes kezelni és helyben upgrade-elni a legújabb általa ismert verzióra.
    • Mivel azonban az Oracle nem látszik hivatalosan kiadni a legújabb fejlesztéseket (pl. a titkosítást) és általában sem ápol jó viszonyt a nyílt forrású közösséggel, a zfs fejlesztése több vonalon folyik párhuzamosan: az Oracle ott folytatta, ahol a Sun abbahagyta, a nyílt forráskód körül kiépült közösség pedig szintén, csak másképp. Valószínűnek látszik, hogy az Oracle termékekkel létrehozott zpoolokat a szabad termékek (még jó darabig) nem fogják támogatni és természetesen fordítva. Az utolsó hordozhatónak tekinthető pool-verzió talán a 28-as.
  • Úgy néz ki, hogy nemsokára lesz natív titkosítás a ZoL-ban.

2.1 Deduplikáció

  • Többféle hash közül választhatunk, és azt is előírhatjuk, hogy hash-egyezés esetén az adatokat hasonlítsa is össze byte-ról byte-ra, nehogy egy esetleges -- amúgy rendkívül valószínűtlen -- hash-ütközés az adatok sérülését okozza.
  • Nem kötelező az összes adatot deduplikálni; szelektíven is bekapcsolhatjuk (egyes datasetekre külön-külön).
    • De a deduplikációs táblázat az egész poolban közös, tehát a poolon belüli különböző datasetek között is megvalósul a deduplikáció.
  • A deduplikáció rendkívül memóriaigényes:
    • Kb. 3-400 byte memória (L2ARC) kell minden blokkhoz; a blokkméret változó, de legfeljebb 128k; ha kis fájljaink vannak, egy-egy fájl egy blokk (és persze majdnem minden nagy fájl vége egy 128k-nál kisebb blokk).
    • Ha L2ARC-ban van a deduplikációs táblázat (DDT), akkor minden max. 128k-nyi L2ARC-hoz kb. 200 byte memória kell.
    • Ökölszabályok: ne deduplikáljunk, ha
      • nem számítunk legalább 50% hely-megtakarításra ÉS
      • nincs legalább 24, de inkább 32GB RAMunk.
  • A deduplikáció, főként, ha nincs megtámogatva SSD-s cache-sel, jelentősen ronthatja a szekvenciális olvasás sebességét, hiszen fragmentációt okoz.
  • Ha nincs elég RAMunk (vagy SSD-s cache-ünk), a deduplikáció több nagyságrenddel ronthatja az írás sebességét. Ez nem fog azonnal kiderülni, csak akkor, amikor a DDT elér egy bizonyos méretet.
  • Jó tudni: az ARC (az operatív memóriában levő cache) maximális mérete alapértelmezés szerint az összes RAM háromnegyede, vagy az összes RAM mínusz 1GB (amelyik nagyobb). Ennek alapértelmezés szerint maximum 25%-át fordítja metaadatok (mint pl. a DDT) cache-elésére a zfs. Mindkét paraméter állítható (utóbbi neve zfs_arc_meta_limit); ha deduplikálunk, érdemes lehet növelni őket.
  • A DDT nyilván a diszken is helyet foglal; ha sok az egyedi adat, a táblázat könnyen nagyobbra nőhet, mint amennyit a kevés duplikátumon megspórolunk.
  • A kiírt blokkok akkor kapnak DDT-bejegyzést, ha kiírásuk pillanatában aktív a datasetjükben a deduplikáció. Következmények:
    1. A kikapcsolt deduplikáció mellett kiírt blokkok nem lesznek deduplikálva (nyilván).
    2. Ha később bekapcsoljuk a deduplikációt, és az iménti blokkok újabb példányát írjuk ki, a DDT szempontjából ezek lesznek ezeknek a blokkoknak az első példányai, vagyis szintén nem takarítunk meg tárhelyet.
    • Ha utólag akarunk deduplikálni sok adatot, valószínűleg a zfs send/receive (l. később) a legelegánsabb megoldás.
  • A deduplikáció aszinkron művelet; a rendszer a háttérben végzi el a ZIL vagy az írás-puffer ürítésekor.
    • Így a szinkron írások nem kell, hogy megvárják;
    • cserébe a ZIL hamarabb megtelhet, ha sok, amúgy deduplikálható szinkron írást kell tárolnia.

2.1.1 Mennyire valószínűtlen a hash-ütközés?

Hash-ütközésnek nevezzük azt az esetet, amikor két különböző bemenethez ugyanazt a kimenetet rendeli egy hash-függvény. Mivel a lehetséges bemenetek száma végtelen (ill. itt, mivel legfeljebb 128k nagyságúak a blokkok, 256131072 körül van, ami majdnem ugyanaz), a kimeneteké pedig véges, szükségszerűen végtelen sok olyan különböző bemenet-pár van, amelyeknek azonos a hash-e. A deduplikáció szempontjából ez azért releváns, mert alapértelmezés szerint az azonos hash-sel rendelkező adatblokkokat azonos tartalmúnak tekinti; így ütközés esetén a később kiírt deduplikált fájl (egy blokkja) helyén a korábban kiírt ütköző fájl tartalmát fogjuk látni.

Alapértelmezés szerint sha256-os hasht használ a zfs, ha bekapcsoljuk a deduplikációt (amúgy, deduplikáció nélkül, a fletcher-4 nevű checksumot használja). Ez azt jelenti, hogy 2256 különböző hash lehetséges. Ha feltesszük, hogy kb. egyenletes eloszlással rendelődik hash a különféle bemenetekhez (márpedig egy jó hashnél ez nagyjából teljesül), akkor annak az esélye, hogy két különböző bemenethez ugyanaz a hash tartozik, 1:2256-hoz. Ez rettenetesen kicsi valószínűség.

Igen ám, de számolnunk kell a születésnap-paradoxonnal is. Ahogy nő a bemenetek (blokkok) száma, nő az ütközés valószínűsége is, és korántsem lineárisan. Az imént hivatkozott wikipédia-szócikk tartalmaz egy táblázatot, amelyből kiolvashatjuk, hogy ha azt szeretnénk, hogy a hash-ütközés valószínűsége legfeljebb 10-18 legyen (tehát négy nagyságrenddel kisebb, mint a bithiba valószínűsége a merevlemezen -- másképp: tízezer bithibára jusson egy hash-ütközés), akkor legfeljebb 4,8*1029 különböző blokkunk lehet. A minimális blokkméret 512 byte, tehát 223517417907714843750 tebibájtnyi egyedi 512 bájtos blokkokból álló adatban már kb. 10-18 valószínűséggel fordul elő hash-ütközés (ez kb. 203287907 yobibyte; egy yobibyte 1024 zebibyte, egy zebibyte 1024 exbibyte, egy exbibyte 1024 pebibyte, egy pebibyte 1024 tebibyte).

Mivel a valóságban általában 512 byte-nál nagyobbak a blokkok, a fenti egy alsó becslés arra, mennyi adatot kellene tárolnunk ahhoz, hogy a hash-ütközés valószínűsége elérje a 10-18-t. Összehasonlításképpen: egy 2011-es becslés szerint az emberiség 1986. és 2011. között kb. 295 exabyte-nyi adatot hozott létre. Ennek a hétszázbilliószorosát (tényleg nem hétszázmilliárdszorosát: hétszázbilliószorosát) kellene tárolnunk, és még mindig elenyészően kicsi lenne a hash-ütközés valószínűsége.

Ha azonban ezzel együtt is kockázatosnak érezzük a dolgot, előírhatjuk, hogy az azonosnak látszó blokkokat a zfs bitről bitre hasonlítsa össze, mielőtt valóban azonosnak tekinti őket (ez természetesen nem használ a sebességnek).

2.2 Tranzakciókezelés

(A ZFS -- The Last Word in Filesystems c. prezentáció 9. oldalán van egy jó ábra, ami alapján könnyebb megérteni, hogy működik.)

A lényeg:

  • Az adatok egy fastruktúrában tárolódnak.
  • Ha a fa egy levelét módosítani kell, allokálunk egy új adatterületet és oda írjuk ki a módosított levelet.
  • Ezután a levél szülőjében kell(ene) átírni azt a pointert, amely az érvényes adatra mutat, úgy, hogy az új blokkra mutasson; csakhogy ez is egy ugyanolyan módosítás, mint az előző, úgyhogy itt is új blokkot allokálunk, amibe beleírjuk az új szülő-csomópontot, az új gyermek-pointerrel.
  • És így tovább a gyökérig (ez az "überblock").
  • Azokat a blokkokat, amelyekre már nem mutat pointer, szabadnak kell jelölni; ha ezt nem tesszük meg, kész is a snapshotunk (hiszen nem írtunk felül semmit).
    • Persze a blokkok felszabadítása is trükkös kell, hogy legyen, különben lehetne, hogy éppen arra nem kerül sor egy áramkimaradás miatt.
  • További előny: ha van sok szabad hely, a véletlen írások (az überblokk -- vagy a rá mutató "pointer" -- frissítését leszámítva) szekvenciális írássá alakulnak.

2.3 zfs send/receive

Célja: zfs hatékony másolása egyik gépről a másikra (vagy egyik poolból a másikba).

Alapelv: snapshotot másol (így biztosítva a másolat konzisztenciáját), de:

  • inkrementális módja is van, amikor két snapshot közti különbséget másoljuk;
    • a különbség előállításának költsége a változások mennyiségével arányos (nem az adatok teljes mennyiségével).
  • zvol esetén a receive művelet időtartama alatt a zvol nem használható; fájlrendszer típusú dataset esetén igen.

Példák (egyelőre nem próbáltam ki őket):

  • Teljes backup készítése hétfőn:
zfs snapshot tank/home@hetfo
zfs send tank/home@hetfo >/backup/home_teljes_hetfo.zfssend
  • Inkrementális backup készítése kedden:
zfs snapshot tank/home@kedd
zfs send tank/home@hetfo tank/home@kedd >/backup/home_inkr_kedd-hetfo.zfssend
  • Differenciális backup készítése szerdán:
zfs snapshot tank/home@szerda
zfs send tank/home@hetfo tank/home@szerda >/backup/home_diff_szerda-hetfo
  • Megj.: az "inkrementális" és "differenciális" backup fogalmának értelmezése nem egységes (az egyik termék így használja, a másik esetleg pont fordítva vagy legalábbis másképp). Itt a Bacula szóhasználatához igyekeztem igazodni.
  • Percenkénti távoli replikáció:
    zfs send -i tank/home@10:13 tank/home@10:14 | ssh masikgep zfs receive -d /tank
    • A zfs send és az ssh közé érdemes lehet beépíteni valamit a pipeline-ba, ami pufferel; pl. pv, buffer, mbuffer. Ez sokat tud gyorsítani az átvitelen, mert így a send és a receive folyamat párhuzamosan fog dolgozni, nem pedig lényegében felváltva, mint az egyszerű pipe esetén, ami jó esetben is csak kb. 64KiB-nyi adatot pufferel.

3 ZFS Linux alatt

  • Linuxra párhuzamosan készült egy userspace és egy kernelspace megvalósítás.
    • A kernelspace implementáció licencproblémák miatt sosem kerülhet bele a hivatalos kernelbe (az OpenSolarist, aminek a ZFS a része, a CDDL alatt teszik elérhetővé, ez pedig jogilag nem kompatibilis a GPL-lel). Az egyetlen megoldás az lenne, hogy valaki nulláról újraírja a ZFS-t (úgy, hogy az adatstruktúrák és algoritmusok ugyanazok, de a kód teljesen új).
    • A FUSE-os megoldásnak nincsenek ilyen gondjai, mivel a GPL-nek nem kell megfelelnie.
    • Jelen sorok írásakor (2012. őszén) a kernelspace-verzió használata az ajánlott; a zfs-fuse fejlesztése megtorpant, a fejlesztők már szintén a zfsonlinuxot (a kernelbeli megvalósítást) használják.
      • A zfs-fuse-t ugyanakkor egyszerűbb telepíteni, úgyhogy a zfs kipróbálására nagyon is alkalmas.
      • Vigyázat! A zfs-fuse 2012. vége felé aktuális, 0.7.0-ás verziója bizonyos terhelések mellett instabil (nálam nem okozott adatvesztést, de rendelkezésreállási problémákat igen).
      • A zfs-fuse régebbi pool- és zfs-verziókat implementál, mint a zfsonlinux, így a zfsonlinuxszal létrehozott poolt nem használhatjuk vele (de fordítva igen).

4 Esettanulmányok

4.1 Linuxos gép telepítése zfs-sel

Figyelem! Ez a példa 2016-ra elavult. Ha lesz időm, frissítem.

Tegyük fel, hogy van egy gépünk, benne két 256GB-os SSD-vel, 16+GB RAM-mal, és hogy erre Debian/GNU Linuxot szeretnénk telepíteni. Az alábbiakban leírom ennek a folyamatnak néhány lépését és elmagyarázom a tervezői döntéseket.

  1. Bootoljunk be valamilyen live médiumról (pl. GRML-es USB-kulcsról).
    • Haladók készíthetnek maguknak saját, a zfs-támogatást eleve tartalmazó grml-t; kulcsszó: "grml remastering". Aki látott már FAI-t, annak ismerős lesz.
  2. Rakjuk fel a futó grml-re a http://ppa.launchpad.net/zfs-native repositoryból a következő csomagokat:
    apt-get install zfsutils zfs-dkms spl-dkms
    • Előfordulhat, hogy ehhez az Ubuntuban levő libc6-ot is fel kell raknunk (ha újabb, mint ami a grml-ünkön van); másik lehetőség a zfs-es csomagok újrafordítása a grml-es környezetben.
  3. Particionáljuk a diszkeket (SSD-ket); lehetőleg használjunk GPT-t.
    • A biztonság kedvéért csináljunk külön partíciókat a /boot-ot tartalmazandó RAID1-tömb számára. Elvileg ugyan tud LVM-ről bootolni a grub, de az ördög nem alszik.
    • Ne felejtsünk el egy párszáz szektorból álló, bios_grub flaggel ellátott partíciót csinálni a diszk elejére (mondjuk a 34-edik szektortól az 511-edikig), hogy a grub tudja hova másolni magát.
    • A RAID-tömbünket alkotandó partíciókat lássuk el a raid flaggel.
    • Mivel a zfsonlinux "out of tree" modul, előfordul, hogy pl. kernelupgrade után nem fordul le helyesen; emiatt hasznos, ha be tudunk bootolni nélküle is. (Ez 2016-ra már nem probléma.)
    • Ezért csináljunk még egy RAID1-tömböt (az első a /boot helye lesz), amin majd a root filerendszer, a /usr ill. a /var nagy része kap helyet, LVM-ben.
    • A maradék helyen hagyjunk egy-egy partíciót, amit majd a zfs fog használni (a továbbiakban feltételezem, hogy ez a két partíció az sda4 ill. az sdb4).
  4. Hozzuk létre azt a könyvtárat, amibe majd telepítünk:
    mkdir /t
  5. Hozzuk létre a zpoolt:
    zpool create -o ashift=12 -o autoexpand=on -R /t tank mirror /dev/sd[ab]4 -O atime=off -O compression=gzip-9 -O devices=off -O setuid=off -O exec=off -O dedup=on -O com.sun:auto-snapshot=false -O xattr=off -O mountpoint=/ -O canmount=off
    • Magyarázat: ashift=12 -- csak a pool létrehozásakor állítható property; azt adja meg (log2 alapon), mekkora "szektormérettel" dolgozzon a zfs.
      • Azért állítjuk 4k-ra, mert lehet, hogy később kerül 4k-s szektorokat használó diszk a poolba.
      • Azért nem állítjuk még nagyobbra, mert az SSD-nek valószínűleg 128k körüli (vagy még nagyobb) méret kedvezne, azzal viszont túl sok helyet pazarolnánk el.
    • -o autoexpand=on -- ha megnő a poolt alkotó vdev, a zfs kezdje el használni a több helyet anélkül, hogy ezt külön kérnénk.
    • -R /t -- állítsuk az altroot property-t /t-re. Így, amíg a live rendszeren dolgozunk, a /t (valójában /t/mountpoint, de a mountpoint "/" lesz) alatt jönnek létre az új zfs-eink, de ha később a valódi rendszert bootoljuk be, minden az elvárt helyén lesz.
    • tank -- így fogják hívni a létrehozandó poolt. A "tank" név dokumentációs konvenció; a valóságban ne így nevezzük a poolt, hanem utaljon a név a funkciójára.
    • mirror /dev/sd[ab]4 -- egy tükrözött (tehát RAID1-szerű) vdevet adunk meg, amit a felsorolt két partíció fog alkotni.
    • -O atime=off -- alapból kikapcsoljuk az atime update-eket az uj fs-eken (ahol kell, majd külön bekapcsoljuk). Ezzel részben nyerünk egy kis teljesítőképességet, részben pedig elkerüljük az SSD túlzott "koptatását".
      • -o-val a poolra, -O-val pedig a pool gyökerében levő zfs-példányra vonatkozó opciókat (propertyket) állítjuk be.
      • A pool gyökerében beállított tulajdonságok (néhány kivételtől eltekintve) öröklődnek a gyermek-fájlrendszerekre; az alapértelmezéseket így csak egyszer, a pool gyökerén kell beállítanunk, de ahol akarjuk, felülbírálhatjuk őket.
    • -O compression=gzip-9 -- a tömörítés a mai CPU-k korában majdnem ingyen van; az SSD-n viszont egyrészt aránylag szűk a hely, másrészt minél kevesebbet írunk rá, annál hosszabb lesz az élettartama, tehát duplán megéri tömöríteni. Ha valamelyik konkrét fájlrendszernél rettentően számít a sebesség, akkor ott érdemes lehet simán on-ra állítani a tömörítést (az sokkal gyorsabb, mint a gzip-9, de még mindig tömörít), vagy akár ki is kapcsolni.
    • -O devices=off -- alapból nem akarjuk értelmezni a device node-okat a poolban levő fájlrendszerekben; ahol mégis, ott majd kifejezetten bekapcsoljuk.
    • -O setuid=off -- alapból nem szeretnénk, ha a setuid/setgid bitek hatásosak lennének (ahol kell, majd bekapcsoljuk).
    • -O exec=off -- alapból nem kell, hogy a fájlrendszerekben levő fájlok végrehajthatóak legyenek.
    • -O dedup=on -- ez talán a legmegkérdőjelezhetőbb döntés. A deduplikáció bekapcsolása általában nem javasolt, mert nagyon memóriaigényes és súlyos lassuláshoz vezethet (ha a DDT nem fér el az ARC-ben, vagy pláne ha az L2ARC-ben sem). Miért kapcsoltam be mégis:
      • Adjunk felső becslést a DDT memóriaigényére! Az SSD aránylag kicsi (256GB), és nem is az egészet használjuk zfs-sel. A DDT mérete szempontjából a kis blokkméret a legrosszabb (az egyedi blokkok számával arányos a mérete). Az ashift=12 miatt a legkisebb lehetséges blokkméret 4k. Ha a 256GB-ot 256 gibibyte-nak vesszük, 67108864 darab 4k-s blokk fér el rajta. Ha ezt felszorozzuk 400-zal, azt kapjuk, hogy a DDT mérete legfeljebb 25GiB lehet. A valóságban ennél jóval kisebb táblára számíthatunk, mivel nagy fájlok is lesznek az SSD-n (amelyeknél így 4k-nál jóval nagyobb, általában 128kiB lesz a blokkméret) és várhatóan nem lesz mind egyedi. Ha minden blokk 128k-s lenne, az 1GiB-ot sem érné el a DDT mérete.
      • Ha nem fér el a DDT az ARC-ben (RAM-ban), akkor még mindig elférhet az L2ARC-ben. Itt most nem lesz L2ARC, mert nincs a két SSD-nél kisebb, de jóval gyorsabb tárolónk. Így, ha a DDT egy része kiszorul a RAMból, a diszkről kell be-beolvasgatni; ez általában nagyon lassú lenne a sok seek miatt, de itt most a diszk egy SSD, ami szinte ingyen seekel, tehát arra számítok, hogy a DDT olvasgatása nem lassítja le nagyon az írást.
      • Szintén az SSD-nek köszönhetően a deduplikáció okozta fragmentációtól sem várok jelentős lassulást.
      • Az SSD élettartama tekintetében két hatás áll egymással szemben: egyfelől a DDT karbantartásával összefüggő írási műveletek csökkentik az élettartamot, másfelől viszont a sikeres deduplikációnak köszönhetően kevesebb adatot kell kiírni. Mivel minden új blokk kiírásakor módosul a DDT, extrém esetektől eltekintve nem valószínű, hogy a deduplikációval megtakarított írásokat is figyelembe véve összességében kevesebbet írnánk, mint kikapcsolt deduplikációval.
      • Másfelől viszont valamennyi helyet megtakaríthatunk, és így több szabad hellyel gazdálkodhat az SSD wear leveling logikája -- arról nem is beszélve, hogy lassabban futunk ki a helyből. (Kivéve, ha döntően egyediek a blokkok, mert akkor a DDT feleslegesen foglalja a helyet.)
        • Fontos: ha nem SSD-nk lenne, nem lenne elég "valamennyi" helyet megtakarítani ahhoz, hogy megérje deduplikálni. "Sok" megtakarítás kellene hozzá.
        • Nehéz előre megmondani, mennyire lesznek deduplikálhatóak az adatok.
    • -O xattr=off -- alapból nem szeretnénk bővített attribútumokat használni. Ezekre többnyire nincs szükségünk (ha pedig igen, akkor tudunk róla). 2012. vége felé egyébként a zfsonlinuxban elég lassú az xattr-megvalósítás; dolgoznak egy jobb megoldáson, ami a ZFS 24-es poolverziójában bevezetett "rendszerattribútumokat" használná erre a célra, de egyelőre nincs kész.
    • -O mountpoint=/ -- azt gondolhatnánk, hogy így majd zfs lesz a root filerendszerünk, de nem; l. az alábbi opciót is:
    • -O canmount=off -- így maga a pool gyökere nem fog bemountolódni, csak öröklik tőle a beállításokat a gyermek-filerendszerek.
    • -O com.sun:auto-snapshot=false -- ez nem "gyári", "natív" property (tulajdonság), hanem a zfs-auto-snapshot script működését befolyásolja (tetszőleges saját címkéket helyezhetünk el a zpool-beli objektumokon namespace:label=value jelleggel; ezek szintén öröklődnek a gyermek-objektumokra). Azt kérjük, hogy alapértelmezés szerint ne snapshotolja a fájlrendszereinket a zfs-auto-snapshot; amelyikről szeretnénk rendszeresen snapshotot csinálni, azon majd true-ra állítjuk ezt a property-t.
  6. Hozzuk létre a /tmp fájlrendszert:
    zfs create -o refquota=4G -o atime=on -o exec=on tank/tmp
    • Itt kell az atime, mert a tmpreaper(8) az alapján fogja eldönteni, mit törölhet.
    • És általában kell az exec is; sokszor akarhatnak a felhasználók a /tmp-ből futtatni valamit.
    • A /tmp-vel még annyi bonyodalom van, hogy a zfs alapból nem hajlandó olyan könyvtárra mountolni fs-t, amely nem üres; márpedig mire odáig jutunk a bootfolyamatban, hogy a /tmp-t bemountolnánk, addigra elképzelhető, hogy lesz már benne valami szemét. Erre a következő megoldások vannak:
      1. A tank/tmp mountpoint propertyjét legacy-re állítjuk és az fstabból mountoljuk, mint a többi fájlrendszert. Hátrány: ha valamilyen hiba miatt nem áll rendelkezésre a zfs kernelmodul, a mount nem fog sikerülni és emiatt rossz esetben megakad a bootfolyamat. Előny: egyszerű.
      2. Egy initscriptbe, vagy pl. az rc.local-ba (vagy, ha runitot használunk, a /etc/runit/1-be) belerakjuk, hogy
        mkdir -p /tmp.orig; mountpoint -q /tmp && mount --move /tmp /tmp.orig && mountpoint -q /tmp.orig && umount -l /tmp.orig; find /tmp -delete && zfs mount tank/tmp
      3. Az iménti két módszer kombinációja: a tank/tmp mountpoint propertyjét legacy-re állítjuk, de scriptből mountoljuk be boot közben, nem az fstabból. Ennek az a hátránya, hogy ha a tmp alatt további zfs-eink is vannak (pl. minden vagy néhány felhasználónak saját), akkor azokat is mind "kézzel" kell bemountolni, míg az előző megoldásban zfs mount tank/tmp helyett írhatunk zfs mount -a-t, és minden bemountolódik.
        • Ugyanakkor sajnos előfordulhat, hogy ha korábban már kiadtunk egy zfs mount -a-t úgy, hogy a /tmp nem volt üres, akkor a /tmp nem mountolódott be, a gyermekei viszont igen. Erre a problémára szerintem nincs szép megoldás. Ronda megoldásnak jó, ha egy ciklussal umountoljuk az összes, a /tmp alá mountolódó zfs-t, mielőtt a /tmp-be mountolandó zfs-t mountolnánk, majd zfs mount -a-val megint bemountoljuk őket.
  7. Hozzunk létre még néhány fájlrendszert:
    • zfs create -o exec=on -o setuid=on -o mountpoint=/usr -o canmount=off tank/usr
    • zfs create -o refquota=1G -o com.sun:auto-snapshot=true tank/usr/share
      • Magát a tank/usr fájlrendszert nem fogjuk mountolni (canmount=off), de a gyermekei öröklik az exec=on és a setuid=on beállítást, meg persze a mountpointot. A canmount attribútum nem öröklődik (és egyébként a refquota sem).
        • A /usr így vagy a root fájlrendszer része lesz, vagy egy külön LVM LV, mondjuk xfs-sel.
        • A /usr/share nem létfontosságú a bootoláshoz, és egy tipikus Debianon a gzip-9 tömörítéssel simán elérhetünk 50+% megtakarítást, ami az SSD-n nem mindegy.
    • zfs create -o refquota=250M -o com.sun:auto-snapshot=true tank/usr/lib32
    • zfs create -o refquota=250M -o com.sun:auto-snapshot=true tank/usr/local
    • zfs create -o refquota=2G tank/usr/local/src
    • zfs create -o refquota=32M -o com.sun:auto-snapshot=true tank/srv
    • zfs create -o refquota=5G tank/srv/postgres
    • zfs create -o refquota=5G tank/srv/www
    • zfs create -o refquota=256M tank/srv/ldap
  8. debootstrappel telepítsük a /t könyvtárba a Debianunkat, konfiguráljuk stb.

4.2 Hatékony backup zfs-re

Néhány óra alatt össze lehet dobni egy backup-megoldást, ami a következőket csinálja:

  • A backupokat tároló gépen (a továbbiakban: backup-szerver) van egy zpool, bekapcsolt tömörítéssel (és, ha az erőforrások engedik, deduplikációval).
    • Minden mentendő fájlrendszerhez tartozik egy külön zfs-példány (filesystem típusú dataset).
    • Értelmes elrendezés pl. az, ha van egy /backup pool, ami alatt minden kliensgépnek van egy könyvtára (ami akár lehet külön fs is), és ezeken belül az egyes kliensek egyes fájlrendszereinek is van egy-egy könyvtára (ami viszont határozottan külön fs).
  • A backup-szerveren fut egy rsync daemon, ami -- alkalmas hitelesítéssel stb. -- a klienseknek írásra kiajánlja ezeket a könyvtárakat.
    • Az rsync daemon fake-super beállítással fut; így mezei userként is minden attribútumot, tulajdonságot, ACL-t stb. képes tárolni, csak xattr-támogatás kell hozzá.
    • A kliensek, amikor menteni akarnak, rsync --inplace -aH --partial jellegű parancssorral töltik fel az adataikat.
    • Az rsync post-xfer exec beállítása segítségével minden transzfer végeztével lefuttatunk egy scriptet, ami snapshotolja az éppen írt fájlrendszert.
      • A snapshoton beállítunk néhány saját propertyt, pl. hogy -- az rsync visszatérési értéke alapján -- sikeres volt-e a mentés, ill. hogy mikor jár le a snapshot élettartama.
  • cronból időnként töröljük a lejárt snapshotokat.
  • A backupok helyreállításához ugyanezeket a könyvtárakat csak olvasásra is kiajánljuk, post-xfer exec script nélkül.

A megoldás sokban hasonlít pl. a dirvish-re; két fő előnye, hogy

  • a régi backupok letörlése sokkal gyorsabb (egy snapshot megszüntetése nagyságrendekkel kevesebb idő, mint egy sokezer fájlt tartalmazó könyvtárfa törlése); és hogy
  • ha egy fájlnak csak egy része változott (vagy pláne: csak a jogai), a dirvish az egészről új példányt tárolna, mi pedig a snapshotolásnak (és esetleg a deduplikációnak) köszönhetően nem tároljuk duplán a változatlan részeket.

A saját megoldásomat közreadtam a githubon.

5 Források

6 Potenciális zh-kérdések

  • Mik a zfs fő újdonságai, előnyei a "hagyományos" fájlrendszerekhez képest?
  • Mi a különbség az LVM és a zfs snapshot-megoldása között? Melyik hogyan működik? Melyik gyorsabb és miért?
  • Mi szól a zfs esetében a deduplikáció használata ellen és mi szól mellette?
  • Mi a különbség a zfs kontextusában a volume és a filesystem között?
  • Mi a különbség a RAID6 és a RAIDZ2 között?
Személyes eszközök