Trvalé ukládání dat a souborová API pro Linux

Já, při zkoumání trvanlivosti ukládání dat v cloudových systémech, jsem se rozhodl ověřit si, zda rozumím základním věcem. Já jsem začal čtením specifikace NVMe , abych pochopil, jaké záruky týkající se trvalého ukládání dat (to znamená – záruky, že data budou dostupná po selhání systému) nám NVMe disky poskytují. Učinil jsem následující hlavní závěry: je třeba považovat data za poškozená od okamžiku, kdy byla zadána příkaz k zápisu dat, a do okamžiku, kdy skončí jejich zápis na médium. Nicméně v většině programů pro zápis dat se zcela běžně používají systémové volání.

V tomto materiálu zkoumám mechanismy trvalého ukládání dat poskytované souborovými API pro Linux. Zdá se, že by to mělo být jednoduché: program vyvolá příkaz write(), a po dokončení této operace budou data spolehlivě uložena na disku. Ale write() pouze zkopíruje data aplikace do jádrového cache umístěného v operační paměti. Aby bylo možné donutit systém k zápisu dat na disk, je třeba použít některé další mechanismy.

Trvalé ukládání dat a souborová API pro Linux

Celkově tento materiál představuje soubor poznámek o tom, co jsem se dozvěděl o tématu, které mě zajímá. Pokud bych měl velmi stručně shrnout to nejdůležitější, pak platí, že pro organizaci trvalého ukládání dat je třeba používat příkaz fdatasync() nebo otevírat soubory s příznakem O_DSYNC. Pokud vás zajímají podrobnosti o tom, co se děje s daty na cestě od programového kódu k disku, podívejte se na tento článek.

Zvláštnosti použití funkce write()

Systémové volání write() je definováno v standardu IEEE POSIX jako pokus o zápis dat do souborového deskriptoru. Po úspěšném dokončení write() operace by měly vrátit přesně ty byty, které byly předtím zapsány, a to i v případě, že k datům přistupují jiné procesy nebo vlákna (tady je příslušná část standardu POSIX). Zde, v sekci, věnované interakci vláken s běžnými souborovými operacemi, je poznámka, v níž se uvádí, že pokud každý z dvou vláken volá tyto funkce, musí každé volání vidět buď všechny vyznačené důsledky vyplývající z provádění jiného volání, nebo vůbec žádné důsledky. To umožňuje dojít k závěru, že všechny operace vstupu/výstupu při práci se soubory musí držet zámek na zdroji, se kterým pracují.

Znamená to, že operace write() je atomická? Z technického hlediska - ano. Operace čtení dat musí vracet buď vše, nebo nic z toho, co bylo zapsáno pomocí write(). Ale operace write(), dle standardu, nemusí nutně končit tím, že zapíše vše, co jí bylo nabídnuto k zápisu. Je jí dovoleno provést zápis pouze části dat. Například, můžeme mít dvě vlákna, z nichž každé připojuje 1024 bajtů k souboru popsanému stejným souborovým descriptor. Z hlediska standardu je akceptovatelným výsledkem, když každá z operací zápisu může připojit k souboru pouze jeden bajt. Tyto operace zůstanou atomární, ale po jejich dokončení budou data, která byla zapsána do souboru, promíchána. Tady je velmi zajímavá diskuse na toto téma na Stack Overflow.

Funkce fsync() a fdatasync()

Nejjednodušší způsob, jak převést data na disk, je volání funkce fsync(). Tato funkce požaduje od operačního systému přenos všech modifikovaných bloků z cache na disk. Zahrnuje to i všechna metadata souboru (čas přístupu, čas modifikace souboru atd.). Domnívám se, že potřeba těchto metadat vzniká zřídka, takže pokud víte, že pro vás nejsou důležitá, můžete používat funkci fdatasync(). V nápovědě o fdatasync() se uvádí, že během práce této funkce se ukládá na disk takový objem metadat, který "je nezbytný pro správné provedení následujících operací čtení dat". A to je přesně to, co zajímá většinu aplikací.

Jedním z problémů, které mohou nastat, je, že tyto mechanismy nezaručují, že soubor bude možné najít po případném selhání. Zejména když se vytváří nový soubor, je nutné zavolat fsync() pro adresář, který jej obsahuje. Jinak se po chybě může stát, že tento soubor neexistuje. Důvodem je, že v UNIXu, kvůli používání tvrdých odkazů, může soubor existovat v několika adresářích. Proto při vyvolání fsync() pro soubor neexistuje žádný způsob, jak zjistit, o které konkrétní adresáře je také třeba vyprázdnit na disk (tady o tom se můžete dočíst podrobněji). Zdá se, že souborový systém ext4 je schopen automaticky aplikovat fsync() na adresáře obsahující odpovídající soubory, ale v případě jiných souborových systémů tomu tak být nemusí.

Tento mechanismus může být různě implementován v různých souborových systémech. Použil jsem blktrace k zjištění, jaké diskové operace jsou používány v souborových systémech ext4 a XFS. Obě vydávají běžné příkazy pro zápis na disk jak pro obsah souborů, tak pro protokol souborového systému, vyprázdňují mezipaměť a končí činnost provedením FUA zápisu (Force Unit Access, zápis dat přímo na disk, obcházením mezipaměti) do protokolu. Pravděpodobně tak činí, aby potvrdily provedení operace. Na discích, které FUA nepodporují, to vyvolává dva vyprázdnění mezipaměti. Moje experimenty ukázaly, že fdatasync() o něco rychleji fsync(). Nástroj blktrace ukazuje, že fdatasync() obvykle zapisuje na disk méně dat (v ext4 fsync() zapisuje 20 KiB, a fdatasync() — 16 KiB). Kromě toho jsem zjistil, že XFS je o něco rychlejší než ext4. A zde s pomocí blktrace se mi podařilo zjistit, že fdatasync() vyprázdňuje na disk méně dat (4 KiB v XFS).

Nejednoznačné situace vznikající při používání fsync()

Mohu si vzpomenout na tři nejednoznačné situace, které se týkají fsync(), se kterými jsem se setkal v praxi.

První takový případ nastal v roce 2008. Tehdy rozhraní Firefox 3 „zamrzlo“, pokud se prováděl zápis na disk velkého množství souborů. Problém spočíval v tom, že v implementaci rozhraní pro uložení informací o jeho stavu byla použita databáze SQLite. Po každé změně, která se v rozhraní odehrála, byla vyvolána funkce fsync(), což zajišťovalo dobré záruky trvalého uložení dat. Ve souborovém systému ext3, který byl tehdy používán, funkce fsync() ukládala na disk všechny "špinavé" stránky v systému, a ne jen ty, které se vztahovaly k příslušnému souboru. To znamenalo, že kliknutí na tlačítko v prohlížeči Firefox mohlo iniciovat zápis megabajtů dat na magnetický disk, což mohlo trvat mnoho sekund. Řešení problému, jak jsem pochopil z tohoto materiálu, spočívalo v přesunutí práce s databází do asynchronních úloh na pozadí. To znamená, že dříve v prohlížeči Firefox byla zavedena přísnější pravidla pro trvanlivost ukládání dat, než bylo skutečně potřeba, a specifika souborového systému ext3 tuto problematiku ještě zhoršila.

Druhá nesrovnalost nastala v roce 2009. Tehdy, po pádu systému, uživatelé nového souborového systému ext4 čelili problému, že mnoho nedávno vytvořených souborů má nulovou délku, což se u staršího souborového systému ext3 nestalo. V předchozím odstavci jsem zmiňoval, že ext3 ukládala na disk příliš mnoho dat, což výrazně zpomalovalo fsync()jeho výkon. Aby se situace zlepšila, v ext4 se na disk ukládají pouze ty "špinavé" stránky, které se vztahují k určitému souboru. Data ostatních souborů zůstávají v paměti mnohem déle, než bylo možné při použití ext3. To bylo provedeno za účelem zlepšení výkonu (ve výchozím stavu data zůstávají v tomto stavu 30 sekund, tento čas lze nastavit pomocí dirty_expire_centisecs; tady o tom si můžete přečíst více). To znamená, že velké objemy dat mohou být nenávratně ztraceny po pádu. Řešením tohoto problému je použití fsync() v aplikacích, které potřebují zajistit trvalé ukládání dat a maximálně je chránit před důsledky výpadků. Funkce fsync() funguje při použití ext4 mnohem efektivněji než při použití ext3. Nevýhodou tohoto přístupu však zůstává, že jeho použití, stejně jako dříve, zpomaluje provádění některých operací, jako je instalace programů. Podrobnosti o tom naleznete zde a zde.

Třetí problém, který se týká fsync(), vznikl v roce 2018. Tehdy, v rámci projektu PostgreSQL, bylo zjištěno, že pokud funkce fsync() narazí na chybu, označuje "špinavé" stránky jako "čisté". V důsledku toho následující volání fsync() S takovými stránkami se nic nedělá. Z tohoto důvodu jsou upravené stránky uchovávány v paměti a nikdy nejsou zapisovány na disk. To je skutečná katastrofa, protože aplikace si bude myslet, že nějaká data byla zapsána na disk, ale ve skutečnosti to tak nebude. Takové selhání fsync() se stává zřídka, aplikace v takových situacích skoro nic nemůže udělat pro řešení problému. Dnes, když se to stane, PostgreSQL a jiné aplikace havarují. Zde, v materiálu "Can Applications Recover from fsync Failures?" je tento problém zkoumán podrobně. V současné době je nejlepším řešením tohoto problému použití Direct I/O s příznakem O_SYNC nebo s příznakem O_DSYNC. Při tomto přístupu systém oznámí chyby, které mohou vzniknout při provádění konkrétních operací zápisu dat, ale tento přístup vyžaduje, aby aplikace spravovala buď samotné vyrovnávací paměti. Podrobnosti o tom naleznete zde a zde.

Otevření souborů s použitím příznaků O_SYNC a O_DSYNC

Vraťme se k diskusi o mechanismech Linuxu, které zajišťují trvalé uložení dat. Konkrétně se jedná o použití příznaku O_SYNC nebo příznaku O_DSYNC při otevírání souborů pomocí systémového volání open(). Při tomto přístupu se každá operace zápisu dat provádí tak, jako by po každém příkazu write() systému byly dány příslušné příkazy fsync() a fdatasync(). V specifikace POSIX se nazývá „Synchronized I/O File Integrity Completion“ a „Data Integrity Completion“. Hlavní výhodou tohoto přístupu je, že k zajištění integrity dat je třeba provést pouze jeden systémový hovor, a ne dva (například — write() a fdatasync()). Hlavní nevýhodou tohoto přístupu je, že všechny operace zápisu využívající odpovídající popisovač souboru budou synchronizovány, což může omezit možnosti strukturování kódu aplikace.

Použití Direct I/O s příznakem O_DIRECT

Systémové volání open() podporuje příznak O_DIRECT, který je určen k tomu, aby, obcházel cache operačního systému, prováděl operace vstupu a výstupu přímo s diskem. To ve většině případů znamená, že zápisové příkazy vydané programem budou přímo překládány na příkazy určené pro práci s diskem. Ale obecně, tento mechanismus není náhradou funkcí fsync() nebo fdatasync(). Skutečnost je taková, že samotný disk může odložit nebo kešovat odpovídající příkazy pro zápis dat. A co je horší, v některých speciálních případech se operace I/O prováděné s použitím flagu O_DIRECT, se přenášejí na tradiční vyrovnávací operace. Nejjednodušší způsob, jak tento problém vyřešit, je použít pro otevření souborů také flag O_DSYNC, což znamená, že za každou zápisovou operací následuje volání fdatasync().

Ukázalo se, že do souborového systému XFS byla nedávno přidána "rychlá cesta" pro O_DIRECT|O_DSYNC-zápis dat. Pokud přepisují blok s použitím O_DIRECT|O_DSYNC, pak XFS místo vyprázdnění cache provede příkaz FUA-zápisu, pokud to zařízení podporuje. Ověřil jsem to pomocí nástroje blktrace v systému Linux 5.4/Ubuntu 20.04. Tento přístup by měl být efektivnější, protože při jeho použití se na disk zapíše minimální množství dat a provádí se pouze jedna operace, nikoli dvě (zápis a vyprázdnění cache). Našel jsem odkaz na patch jádra z roku 2018, ve kterém je tento mechanismus realizován. Obsahuje diskusi o aplikaci této optimalizace i v jiných souborových systémech, ale co vím, XFS je zatím jediný souborový systém, který to podporuje.

Funkce sync_file_range()

V Linuxu existuje systémové volání sync_file_range(), které umožňuje vyprázdnit na disk pouze část souboru, a ne celý soubor. Toto volání iniciuje asynchronní vyprázdnění dat a nečeká na jeho dokončení. Ale v dokumentaci k sync_file_range() se uvádí, že tento příkaz je "velmi nebezpečný". Doporučuje se jím nezabývat. Vlastnosti a nebezpečí sync_file_range() jsou velmi dobře popsány v tomto materiálu. Konkrétně se zdá, že toto volání používá RocksDB k řízení toho, kdy jádro vyprázdní "špinavá" data na disk. Ale přitom tam, pro zajištění trvalého ukládání dat, je také používán fdatasync(). V kódě RocksDB jsou zajímavé komentáře na toto téma. Například se zdá, že volání sync_file_range() při použití ZFS nevede k vyprázdnění dat na disk. Zkušenost mi říká, že kód, který se používá zřídka, může obsahovat chyby. Proto bych doporučil nepoužívat toto systémové volání bez krajní potřeby.

Systémová volání, které pomáhají zajistit trvalé ukládání dat

Dosel jsem к závěru, že pro provádění operací vstupu/výstupu, které zajišťují spolehlivé uchovávání dat, je možné používat tři přístupy. Všechny tyto přístupy vyžadují volání funkce fsync() pro adresář, ve kterém byl soubor vytvořen. Zde jsou tyto přístupy:

  1. Volání funkce fdatasync() nebo fsync() po funkci write() (lepší je používat fdatasync()).
  2. Práce s deskriptorem souboru otevřeným s příznakem O_DSYNC nebo O_SYNC (lepší — s příznakem O_DSYNC).
  3. Použití příkazu pwritev2() s příznakem RWF_DSYNC nebo RWF_SYNC (preferovaně — s příznakem RWF_DSYNC).

Poznámky k výkonu

Nevěnoval jsem se pečlivému měření výkonu různých mechanismů, které jsem prozkoumal. Zjistil jsem, že rozdíly v rychlosti jejich práce jsou velmi malé. To znamená, že se mohu mýlit a že za jiných podmínek může totéž vykázat jiné výsledky. Nejprve se zmíním o tom, co má větší vliv na výkon, a poté o tom, co má na výkon menší vliv.

  1. Přepisování dat v souboru je rychlejší než připojení dat k souboru (zisk v výkonu může být 2-100%). Připojení dat k souboru vyžaduje další změny v metadatech souboru, dokonce i po systémovém volání fallocate(), ale rozsah tohoto efektu se může měnit. Doporučuji pro zajištění nejlepšího výkonu volat fallocate() pro předběžné přidělení potřebného prostoru. Poté je třeba toto místo výslovně naplnit nulami a zavolat fsync(). Díky tomu budou odpovídající bloky v souborovém systému označeny jako 'přidělené', a ne jako 'nepřidělené'. To přináší malé (asi 2%) zlepšení výkonu. Kromě toho u některých disků může být první operace přístupu k bloku pomalejší než ostatní. To znamená, že vyplnění místa nulami může vést k významnému (asi 100%) zlepšení výkonu. To se zejména může stát u disků AWS EBS (toto jsou neoficiální údaje, které jsem nemohl potvrdit). Totéž platí i pro úložiště GCP Persistent Disk (a toto je už oficiální informace, kterou potvrdily testy). Jiní odborníci učinili podobná pozorování, která se týkají různých disků.
  2. Čím méně systémových volání — tím vyšší výkon (zisk může být přibližně 5%). Zdá se, že volání open() s příznakem O_DSYNC nebo volání pwritev2() s příznakem RWF_SYNC rychleji volání fdatasync(). Podezřívám, že to má co do činění s tím, že při takovém přístupu hraje roli to, že pro vyřešení stejného úkolu je potřeba provést méně systémových volání (jedno volání místo dvou). Ale rozdíl v výkonu je velmi malý, takže se na něj můžete klidně vykašlat a v aplikaci použít to, co nezkomplikuje její logiku.

Pokud vás zajímá téma trvalého ukládání dat — zde je několik užitečných materiálů:

  • Metody přístupu k I/O — přehled základních mechanismů vstupu/výstupu.
  • Zajištění, že data dosáhnou disku — vyprávění o tom, co se děje s daty na cestě od aplikace k disku.
  • Kdy byste měli provádět fsync pro obsahující adresář — odpověď na otázku, kdy je třeba aplikovat fsync() pro adresáře. Pokud to shrnout jednou větou, měl byste to dělat při vytváření nového souboru, a důvodem této doporučení je, že v Linuxu může existovat mnoho odkazů na jeden a tentýž soubor.
  • SQL Server na Linuxu: Interna FUA — zde je popsáno, jak je trvalé ukládání dat realizováno v SQL Serveru na platformě Linux. Existují některá zajímavá srovnání mezi systémovými voláními Windows a Linux. Jsem si téměř jistý, že právě díky tomuto materiálu jsem se dozvěděl o FUA optimalizaci XFS.

Ztratili jste někdy data, která jste považovali za bezpečně uložená na disku?

Trvalé ukládání dat a souborová API pro Linux

Trvalé ukládání dat a souborová API pro Linux

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster