rmlint

Z thewoodcraft.org

Znám velmi málo lidí schopných udržet pořádek v uložených souborech a mají můj obdiv. Většina lidí to vzdá a situaci kdy začne zápolit s nedostatkem místa vyřeší tím, že si pořídí větší disk. Jenže takové řešení – obzvláště těm, kterým na datech záleží ­– leze do peněz. Žádná kapacita disku není nekonečná, protože místo zabírají nejenom nově přibývající soubory, ale také soubory zduplikované. Nejběžnější formou zálohy – obzvláště u těch co neznají git – je prostá kopie souboru. A když pak dochází místo řeší situaci tím, že obvykle zmáznou starší zálohy. Ovšem je dost lidí, co jsou schopni bez většího přemýšlení a nostalgie smáznout i data která jim později chybí.

Vzhledem k tomu, že jsem v dávné minulosti několikrát o data přišel, přistupuji k čištění disku velice obezřetně. A protože disková kapacita mého notebooku je poměrně omezená (jak vysvětlím o něco níže), ukládal jsem svoje data na externí média. Ale poté co jsem se vypekl s Blue-Ray mechanikou, jsem dospěl k závěru, že tato forma záloh je o ničem.

Relativně spolehlivé CD má mimo nedostatečné kapacity především tu nevýhodu, že s vypáleným souborem již nelze dále pracovat. Je tak vhodné pro pasivní zálohu, ale rozhodně se nehodí ke skladování velkého archívu dokumentů (elektronické dokumenty, videa, audio), který je třeba průběžně třídit.

Proto jsem před 5 lety soustředil veškerá svá data na centrálním domácím úložišti. S tím, že data postupně přeberu a přírůstek nových dat bude kompenzován průběžným odstraňováním duplicitních souborů. Ale skutek utek a záhy jsem to vzdal. Místa je zatím poměrně dost, tak jsem to dál neřešil.

Mnohem palčivější byl problém s místem na notebooku. Stroj, co mi běží jako centrální úložiště není trvale v provozu, protože zároveň funguje jako pracovní stanice a kvalita mobilního připojení v ČR také není žádná sláva. Proto mám data se kterými průběžně pracuji uložená na notebooku. Na úložišti mám archivní soubory s nimiž běžně nepracuji (audio a video) a odsypané zálohy.

Ovšem i tak čas od času mi na notebooku dojde místo. Co si tedy počít, pokud chci mít k dispozici všechna data, ale nemám čas na to abych v nich udělal pořádek a kapacitu blokového zařízení nelze za rozumný peníz zvětšit?

Toť ožehavá otázka, při které záleží na tom, na jaký souborový systém jste vsadili. Pokud jste vsadili – jako já – na Btrfs, pak máte šanci získat nějaké volné místo, aniž byste museli investovat do koupě nového hardwaru a odmazat některá svá data.

Rekomprese extentů

Defragmentace a rekomprese extentů je prvním krokem, který nic nestojí. S kompresí Btrfs pracuje od jádra verze 2.6.38, ovšem o tom, kdy ji použije na datový obsah extentu rozhoduje výkon vašeho procesoru. Je-li výkon CPU dostatečný a systém stíhá extenty komprimovat v reálném čase, je to v pohodě. Ovšem pokud nestíhá, tak se data ukládají bez komprese. Pochopitelně máte možnosti si ji vynutit, ovšem v případě souborů, které již nějakým způsobem komprimovány jsou, to může být kontraproduktivní – kupř. při skladování videozáznamů, komprimovaných archívů, či obrázků. Jenom se tím ještě více připravíte o výkon CPU.

Mnohem lepší je do výchozího nastavení Btrfs nešťourat a dodatečnou rekompresi využít právě v situaci, že je potřeba uvolnit nějaké místo. Bohužel to má háček – není možné předem zjistit kolik místa se tímto způsobem podaří uvolnit. Jinak je to jednoduché:

Příklad
root@stroj~# btrfs filesystem defrag -v -r -czlib -l 32768 /

Vyždímal jsem takto z cca 200GB dat další 3GB volného místa.

Diskový prostor na mém notebooku

Protože SSD diskům nevěřím, používám od r. 2013 Btrfs v raid1 nad dvěma SSD disky. Původně to bylo 2x60GB – schválně jsem zvolil menší kapacitu, protože čím víc místa je k dispozici, tím víc blbostí si člověk uloží.

Soubory s videem jako potenciální rezerva

Místo neřeším obvykle do okamžiku, kdy dojde. Pro tento případ mívám na disku uložené tak dva tři filmy. Když je dlouhá cesta, je co pustit klukovi a pokud dojde místo, je to nejrychlejší cesta k tomu, jak nějaké získat.

Nevím kdo z vás zažil zaplácnuté Btrfs, každopádně nejde o nic tragického, jak se kdosi v nedávné diskuzi domníval. Důležité je vědět že Btrfs z principu nikdy nedovolí zaplácnout úplně celý disk.

  • Pokud je k dispozici méně volného místa, než kolik zabere soubor který chceme zapsat, tak zápis selže. Ale soubor, jehož velikost bude menší, zapsat půjde bez problému. Spolehlivým způsobem jak si zaplácnout veškeré volné místo, je – průběžně zapisovat data do otevřeného souboru.
  • Jelikož jde o COW systém, tak určitý volný prostor je potřeba i při odstraňování souboru. To znamená, že u zcela zaplněného Btrfs nelze uvolnit místo pouhým smazáním souboru. Existuje však jednoduché východisko (a proto také těch pár velkých souborů s videem): Nejprve soubor "smrsknout" pomocí truncate na nulovou velikost, a teprve potom ho smazat.


Co u mne zabírá místo

Nejvíc místa u mne zabírají tyto věci:

  • git repozitáře
  • dokumentace (většinou soubory v plain textu)
  • komprimované soubory (dokumenty v DJVU či PDF, obrázky, tarbally se zdrojáky,…)
  • Win aplikace v PlayOnLinuxu

Git repozitáře mi v současné době zabírají zhruba 11GB, protože je skladuji s celou historií. Jejich počet průběžně narůstal (s tím, jak vývojáři postupně přecházeli na GIT), takže mi zhruba po roce přestalo 60GB stačit. Navíc původní SSD z r.2012 měly tu nepěknou vlastnost že lagovaly, takže jsem je v lednu 2014 nahradil za 2x120GB SATA III Samsung 840 EVO.

Další upgrade diskového prostoru na úrovni hardware, jsem udělal v létě 2015, kdy jsem místo dvou SATA disků použil duální mSATA řadič s 2x250 GB mSATA Samsung 850 EVO. Což mi umožnilo vrátit do notebookového boxu zpět DVD mechaniku.

Jak jsem narazil na rmlint

Naposledy mi došlo místo minulý týden v pátek. To už jsem měl jednu větší čistku zhruba před půl rokem za sebou. Ovšem když jsem se podíval na kolik by vyšlo zakoupení 2x500GB mSATA, tak jsem si řekl, že to raději zkusím něco jiného. Předchozí aktualizace na úrovni hardware totiž vyšly pokaždé asi na polovinu a investovat 10 tisíc do pět let starého notebooku – který stejně bude nutné koncem roku vyměnit – nemá smysl.

Proto jsem udělal obvyklou čistku s rekompresí extentů a při tom mne napadlo, kouknout se jakpak asi vypadá u Btrfs aktuální situace s deduplikací. A na stránce wiki k Btrfs o deduplikaci jsem narazil na rmlint. Vzhledem k tomu, že je v oficiálním repozitáři Debianu, jsem nelenil, doinstaloval a vyzkoušel.

Co dělá rmlint?

Sám o sobě dělá jen to, že projíždí přehozený adresář, počítá kontrolní součty a hledá duplicitní soubory. Z nich pak jeden použije jako originál (soubor pro který byl shodný kontrolní součet vypočten nejdříve) a s ostatními jsou pro něj duplikáty. Na závěr zobrazí resumé, ze kterého je ihned jasné, jestli má vůbec smysl přistoupit k deduplikaci, nebo ne.

Kromě toho vygeneruje dva soubory: shellový skript rmlint.sh a soubor rmlint.json, ve kterém jsou všechny informace o nalezených duplicitních souborech. Pokud se rozhodnete, že opravdu chcete soubory deduplikovat, stačí spustit vygenerovaný skript rmlint.sh. Podstatná je tedy informace, že k realizaci změn dojde teprve po spuštění vygenerovaného skriptu rmlint.sh. Předtím se do něj můžete podívat a zkontrolovat jestli skript bude skript skutečně dělat to chcete.

Co obsahuje vygenerovaný skript?

Především obsahuje funkce, které se budou průběžně volat, nápovědu a pak seznam souborů k "opracování".

  • Soubory, které byly určeny za originály, zůstávají beze změny
  • Na ostatní se pak aplikuje funkce, která byla zvolena při generování skriptu přes rmlint

Deduplikace

Duplicitní soubory se nejčastěji objeví tak, že se jeden a týž soubor kopíruje z jednoho místa na druhé, a sem tam se i uloží pod jiným jménem.

Pokud rmlint najde takto zduplikovaný soubor, nabízí několik možností, jak situaci vyřešit. Záleží na tom, jaký používáte souborový systém a čeho tím vlastně chcete dosáhnout. U souborových systémů, které nepodpodporují deduplikaci můžete použít náhradní řešení: hardlink, nebo symlink. Jde však o řešení záludné, které se v určitých případech může hodit, ale tam, kde má jít o zálohy to může vést k fatálním důsledkům, pokud někdo do takového souboru hrábne.

Ideálním místem k otestování utility rmlint u mne byly prefixy pro PlayOnLinux, které zabíraly 18GB místa. Tyto prefixy totiž většinou obsahují kromě Win aplikací také nejrůznější systémové knihovny, které ještě nejsou implementované do wine. Spustil jsem tedy následující příkaz:

Příklad
user@stroj:~$ rmlint -T df --config=sh:handler=clone .PlayOnLinux/wineprefix/
…
==> Note: Please use the saved script below for removal, not the above output.
==> In total 42941 files, whereof 14048 are duplicates in 4355 groups.
==> This equals 3,34 GB of duplicates which could be removed.

Wrote a sh file to: /home/user/rmlint.sh
Wrote a json file to: /home/user/rmlint.json
user@stroj:~$ ./rmlint.sh

Poté, co doběhnul vygenerovaný skript. jsem zkontroloval, zda-li skutečně došlo k uvolnění místa. A opravdu – po deduplikaci souborů v adresáři pro PlayOnLinux se uvolnilo dalších 2,5GB.

Použít clone, symlink, reflink nebo hardlink?

Klíčovou roli v tom co nakonec bude dělat vygenerovaný rmlint.sh, hraje zvolený handler. Deduplikace na úrovni souborového systému, která je relativně bez rizika, se udělá pouze v případě, že se použije volba clone nebo reflink. Než se dostanu k tomu, jaký je mezi nimi rozdíl, uvedu v čem se liší reflink (referenční odkaz) od hardlinku (pevný odkaz) a symlinku (symbolický odkaz).

Pevný odkaz - hardlink

U souborových systémů, které neznají reflink, lze řešit problém duplikovaných souborů přes pevné odkazy – hardlinky. Pro lepší představu: jméno souboru v rámci souborového systému, představuje katalogový lístek, který odkazuje na určité místo v souborovém systému – v případě Btrfs jde o metadata, která udržují informaci o datových extentech, které obsahují data souboru. Pevný odkaz (hardlink) je jiný lístek, s jiným jménem, v jiném místě katalogu, který ale odkazuje na stejná metadata a tím i na stejnou sadu datových extentů. Je tedy úplně jedno, jestli do obsahu hrábneme přes jeden, nebo druhý lístek katalogu – výsledek bude v obou případech stejný!

To pochopitelně nemusí vadit v situaci, kdy chceme aby byl jeden a ten samý soubor sdílený z více míst souborového systému. Ovšem v případě duplicitních souborů v zálohách, může mít použití hardlinku fatální důsledky, pokud nejsou tyto soubory přístupné pouze pro čtení.

Symbolický odkaz - symlink

Pevné odkazy (hardlinky) však mají omezení v tom, že se dají použít pouze v rámci jednoho souborového systému. Tam, kde byla adresářová struktura rozložená přes více datových zdrojů (sdílený síťový disk, externí disk, aj.) se místo pevného odkazu používá symbolický odkaz – symlink.

V tomto případě však nejde o katalogový lístek v rámci souborového systému (jako je jméno souboru), ale o speciální soubor, který obsahuje pouze informaci o tom kde se nalézá cíl. Není to tedy odkaz na metadata (jako hardlink), ale na jiný katalogový lístek.

Problém symlinků spočívá v tom, že (na rozdíl od hardlinků) existují v určitém kontextu, který se může změnit – stačí nabindovat adresář do jiného přípojného bodu, nebo se přepnout do chrootu a cesta k cílovému souboru přestane být platná.

Referenční odkaz - reflink

Funguje podobně jako pevný odkaz - hardlink. Také jde o katalogový lístek v rámci souborového systému, který se však od hardlinku liší tím, že má svá vlastní metadata, která však odkazují na stejné datové extenty. V okamžiku, kdy dojde k editaci souboru vytvořeného jako referenční link, zůstane původní sada datových extentů tak jak je a změny se uloží jako nový transparentní datový extent.

Metadata původního souboru, a původní sada extentů zůstane beze změny. Pouze v metadatech "odštípnutého" souboru přibude odkaz na další datový extent. V konečném důsledku všechy datové extenty pro oba soubory zaberou míň místa, než kdybychom vytvořili a upravili plnotučnou kopii souboru.

Poznámka: Na rozdíl od symlinku a hardlinku, vypadá reflink v rámci souborového systému jako zcela nezávislý soubor. Viz následující ukázkový příklad:

Příklad
user@stroj:~$ touch soubor.txt
user@stroj:~$ cp --link soubor.txt soubor_link.txt
user@stroj:~$ cp --reflink soubor.txt soubor_reflink.txt
user@stroj:~$ cp --symbolic-link soubor.txt soubor_symlink.txt
user@stroj:~$ ls -ali soubor*.txt
22890902 -rw-r--r-- 2 want want  0 bře 17 16:28 soubor_link.txt
22890904 -rw-r--r-- 1 want want  0 bře 17 16:26 soubor_reflink.txt
22890907 lrwxrwxrwx 1 want want 10 bře 17 16:27 soubor_symlink.txt -> soubor.txt
22890902 -rw-r--r-- 2 want want  0 bře 17 16:28 soubor.txt
user@stroj:~$ echo obsah >> soubor.txt
user@stroj:~$ ls -ali soubor*.txt
22890902 -rw-r--r-- 2 want want  6 bře 17 16:28 soubor_link.txt
22890904 -rw-r--r-- 1 want want  0 bře 17 16:26 soubor_reflink.txt
22890907 lrwxrwxrwx 1 want want 10 bře 17 16:27 soubor_symlink.txt -> soubor.txt
22890902 -rw-r--r-- 2 want want  6 bře 17 16:28 soubor.txt

Zápisem do souboru soubor.txt byl dotčen pouze soubor_link.txt, který byl vytvořený jako hardlink. Že jde o hardlink na původní soubor.txt lze poznat jednak podle čísla 2 (které udává počet referencí na soubor), ale především podle stejného čísla inodu (22890902). Po zapsání dat do odkazovaného souboru, došlo k jeho "odštípnutí" a rozšíření metadatové sady extentů o nový extent, zatím co původní sada extentů, na kterou odkazují metadata pro soubor_reflink.txt zůstala beze změny.

Symlink soubor_symlink.txt lze identifikovat jednak podle písmena 'l', kterým začínají jeho atributy, ale i podle toho, že je při podrobném výpisu uveden cílový soubor na který odkazuje. Také je z výpisu patrné, že měl po vytvoření nenulovou velikost (protože jde o soubor s datovým obsahem), přestože odkazoval na soubor, který žádný obsah neměl.

Rozdíl mezi clone a reflink

Rozdíl mezi clone a reflink je pouze v tom, jakým způsobem rmlint nahradí duplicitní soubor.

Při clone, volá skript rmlint.sh binární soubor rmlint (přes který byl vygenerován), který na základě předaných parametrů nahradí v metadatech duplicitního souboru původní sadu datových extentů sadou extentů originálního souboru, uvolněné extenty odstraní ale číslo inodu zůstane beze změny.

Při reflinku, se duplicitní soubor nejprve odstraní (včetně metadat) a pak se znovu vytvoří přes cp, jako reflink na originál. Zde tedy platí co jsem zmínil výše – aby mohl být duplicitní soubor odstraněn, musí být k dispozici dostatek místa. A také to sebou může nést i nějaké další vedlejší účinky, protože se změní i číslo inodu.

Klíčový předpoklad pro deduplikaci

Po deduplikaci bodou existovat datové extenty deduplikovaných souborů vždy pouze v jednom exempláři. Je tedy bezpodmínečně nutné udělat maximum pro to, aby nedošlo k jejich poškození.

Protože žádnému fyzickému médiu nelze důvěřovat na 100%, je podle mne bezpodmínečně nutné, aby měl souborový systém datové extenty rozložené přinejmenším na dvě fyzická zařízení, aby v případě, že dojde k poškození jednoho z nich stále byla k dispozici nepoškozená kopie. Jestli se o to postará samo Btrfs (raid1) nebo jestli bude nad nějakým raid polem (pochopitelně vyšším než 0) je už vcelku jedno.

V každém jiném případě zcela reálně hrozí, při chybě na blokovém zařízení, že dojde ke ztrátě dat a nevratnému poškození souborového systému.

Post Scriptum

Jak zjistit, které hardlinky mají stejný cíl?

Příklad
user@stroj:~$ sudo find / -samefile soubor.txt
/home/user/soubor.txt
/home/user/soubor_link.txt

Jak zjistit, které datové extenty patří k souboru

Příklad
user@stroj:~$ sudo btrfs-debug-tree /dev/sda1 | grep -A2 22890902
                tree block key (22890902 INODE_ITEM 0) level 0
                tree block backref root 256
        item 28 key (433267265536 EXTENT_ITEM 4096) itemoff 2516 itemsize 51
--
                location key (22890902 INODE_ITEM 0) type FILE
                transid 1064905 data_len 0 name_len 10
                name: soubor.txt
--
                location key (22890902 INODE_ITEM 0) type FILE
                transid 1064906 data_len 0 name_len 15
                name: soubor_link.txt
--
                location key (22890902 INODE_ITEM 0) type FILE
                transid 1064905 data_len 0 name_len 10
                name: soubor.txt
--
                location key (22890902 INODE_ITEM 0) type FILE
                transid 1064906 data_len 0 name_len 15
                name: soubor_link.txt
--
        key (22890902 INODE_ITEM 0) block 433267253248 (105778138) gen 1066189
        key (22890986 INODE_ITEM 0) block 433404981248 (105811763) gen 1065939
        key (22891077 EXTENT_DATA 0) block 432613801984 (105618604) gen 1065005
--
        item 0 key (22890902 INODE_ITEM 0) itemoff 3835 itemsize 160
                inode generation 1064905 transid 1064909 size 6 nbytes 6
                block group 0 mode 100644 links 2 uid 1001 gid 1001 rdev 0
--
        item 1 key (22890902 INODE_REF 54530) itemoff 3790 itemsize 45
                inode ref index 40018 namelen 10 name: soubor.txt
                inode ref index 40019 namelen 15 name: soubor_link.txt
        item 2 key (22890902 EXTENT_DATA 0) itemoff 3763 itemsize 27
                generation 1064909 type 0 (inline)
                inline extent data size 6 ram_bytes 6 compression 0 (none)