Odolné úložiště dat a Linux File API

Já, zkoumající stabilitu datových úložišť v cloudových systémech, jsem se rozhodl otestovat, abych se ujistil, že rozumím základním věcem. já začalo přečtením specifikace NVMe abychom pochopili, jaké záruky týkající se perzistence dat (tj. záruk, že data budou dostupná po selhání systému), nám dávají NMVe disky. Učinil jsem následující hlavní závěry: musíte uvažovat o poškození dat od okamžiku zadání příkazu k zápisu dat až do okamžiku, kdy jsou zapsána na paměťové médium. Ve většině programů se však k zápisu dat celkem bezpečně používají systémová volání.

V tomto článku prozkoumám mechanismy persistence poskytované linuxovými souborovými API. Zdá se, že zde by mělo být vše jednoduché: program volá příkaz write()a po dokončení operace tohoto příkazu budou data bezpečně uložena na disk. Ale write() pouze zkopíruje data aplikace do mezipaměti jádra umístěné v paměti RAM. Aby bylo možné přinutit systém zapisovat data na disk, musí být použity některé další mechanismy.

Odolné úložiště dat a Linux File API

Obecně je tento materiál souborem poznámek vztahujících se k tomu, co jsem se naučil na téma, které mě zajímá. Pokud budeme mluvit velmi stručně o tom nejdůležitějším, ukáže se, že pro uspořádání udržitelného ukládání dat musíte použít příkaz fdatasync() nebo otevřít soubory s příznakem O_DSYNC. Pokud máte zájem dozvědět se více o tom, co se děje s daty na cestě z kódu na disk, podívejte se na tento článek.

Vlastnosti použití funkce write().

Systémové volání write() definované ve standardu IEEE POSIX jako pokus o zápis dat do deskriptoru souboru. Po úspěšném dokončení prac write() operace čtení dat musí vracet přesně ty bajty, které byly dříve zapsány, a to i v případě, že se k datům přistupuje z jiných procesů nebo vláken (zde odpovídající část standardu POSIX). Zde, v části o interakci vláken s běžnými operacemi se soubory je poznámka, která říká, že pokud dvě vlákna volají tyto funkce každé, pak každé volání musí buď vidět všechny naznačené důsledky, ke kterým vede provedení druhého volání, popř. nevidím vůbec žádné následky. To vede k závěru, že všechny souborové I/O operace musí mít zámek na zdroji, se kterým se pracuje.

Znamená to, že operace write() je atomový? Z technického hlediska ano. Operace čtení dat musí vrátit buď všechno, nebo nic z toho, čím bylo zapsáno write(). Ale operace write(), v souladu se standardem nemusí končit po zapsání všeho, co byla požádána, aby zapsala. Je povoleno zapisovat pouze část dat. Například bychom mohli mít dva proudy, z nichž každý připojuje 1024 bajtů k souboru popsanému stejným deskriptorem souboru. Z hlediska standardu bude výsledek přijatelný, když každá z operací zápisu může k souboru připojit pouze jeden bajt. Tyto operace zůstanou atomické, ale po jejich dokončení budou data, která zapisují do souboru, neuspořádaná. zde je velmi zajímavá diskuze na toto téma na Stack Overflow.

funkce fsync() a fdatasync().

Nejjednodušší způsob, jak vyprázdnit data na disk, je zavolat funkci fsync(). Tato funkce žádá operační systém, aby přesunul všechny upravené bloky z mezipaměti na disk. To zahrnuje všechna metadata souboru (čas přístupu, čas úpravy souboru atd.). Věřím, že tato metadata jsou potřeba jen zřídka, takže pokud víte, že to pro vás není důležité, můžete funkci použít fdatasync(). V Pomoc na fdatasync() říká, že při provozu této funkce se na disk ukládá takové množství metadat, které je „nezbytné pro správné provedení následujících operací čtení dat“. A to je přesně to, na čem většině aplikací záleží.

Jeden problém, který zde může nastat, je, že tyto mechanismy nezaručují, že soubor bude po případném selhání nalezen. Zejména, když je vytvořen nový soubor, je třeba zavolat fsync() pro adresář, který jej obsahuje. V opačném případě se po havárii může ukázat, že tento soubor neexistuje. Důvodem je to, že pod UNIXem může díky použití pevných odkazů existovat soubor ve více adresářích. Proto při volání fsync() neexistuje způsob, jak by soubor věděl, která data adresáře by měla být také vyprázdněna na disk (zde můžete si o tom přečíst více). Vypadá to, že souborový systém ext4 je schopen automaticky platí fsync() do adresářů obsahujících odpovídající soubory, ale u jiných souborových systémů tomu tak být nemusí.

Tento mechanismus může být implementován odlišně v různých souborových systémech. Použil jsem blktrace se dozvíte, jaké diskové operace se používají v souborových systémech ext4 a XFS. Oba vydávají obvyklé příkazy pro zápis na disk pro obsah souborů i žurnál systému souborů, vyprázdní mezipaměť a ukončí se provedením zápisu FUA (Force Unit Access, zápis dat přímo na disk, vynechání mezipaměti) do žurnálu. Pravděpodobně to dělají jen proto, aby potvrdili skutečnost transakce. Na jednotkách, které nepodporují FUA, to způsobí dvě vyprázdnění mezipaměti. Moje experimenty to ukázaly fdatasync() trochu rychleji fsync(). Utility blktrace to naznačuje fdatasync() obvykle zapisuje méně dat na disk (v ext4 fsync() zapisuje 20 KiB a fdatasync() - 16 kB). Také jsem zjistil, že XFS je o něco rychlejší než ext4. A tady s pomocí blktrace to dokázal zjistit fdatasync() vyprázdní méně dat na disk (4 kB v XFS).

Nejednoznačné situace při použití fsync()

Napadají mě tři nejednoznačné situace fsync()se kterými jsem se v praxi setkal.

K prvnímu takovému incidentu došlo v roce 2008. V té době rozhraní Firefoxu 3 „zamrzlo“, pokud se na disk zapisovalo velké množství souborů. Problém byl v tom, že implementace rozhraní využívala k ukládání informací o jeho stavu databázi SQLite. Po každé změně, která nastala v rozhraní, byla funkce volána fsync(), což dávalo dobré záruky stabilního ukládání dat. V tehdy používaném souborovém systému ext3 je funkce fsync() vyprázdnit na disk všechny „špinavé“ stránky v systému, a nejen ty, které souvisely s odpovídajícím souborem. To znamenalo, že kliknutí na tlačítko ve Firefoxu může způsobit zápis megabajtů dat na magnetický disk, což může trvat mnoho sekund. Řešení problému, jak jsem pochopil to materiálu, bylo přesunout práci s databází do asynchronních úloh na pozadí. To znamená, že Firefox dříve zaváděl přísnější požadavky na perzistenci úložiště, než bylo skutečně nutné, a funkce souborového systému ext3 tento problém jen prohloubily.

Druhý problém nastal v roce 2009. Poté, po pádu systému, uživatelé nového souborového systému ext4 zjistili, že mnoho nově vytvořených souborů má nulovou délku, ale u staršího souborového systému ext3 se to nestalo. V předchozím odstavci jsem mluvil o tom, jak ext3 vysypal na disk příliš mnoho dat, což hodně zpomalilo. fsync(). Aby se situace zlepšila, ext4 vyprázdní pouze ty "špinavé" stránky, které jsou relevantní pro konkrétní soubor. A data ostatních souborů zůstávají v paměti mnohem déle než u ext3. To bylo provedeno za účelem zlepšení výkonu (ve výchozím nastavení zůstávají data v tomto stavu po dobu 30 sekund, můžete to nakonfigurovat pomocí dirty_expire_centisecs; zde o tom můžete najít více informací). To znamená, že velké množství dat může být po havárii nenávratně ztraceno. Řešením tohoto problému je použití fsync() v aplikacích, které potřebují poskytovat stabilní úložiště dat a co nejvíce je chránit před následky poruch. Funkce fsync() pracuje mnohem efektivněji s ext4 než s ext3. Nevýhodou tohoto přístupu je, že jeho použití stejně jako dříve zpomaluje některé operace, například instalaci programů. Viz podrobnosti o tom zde и zde.

Třetí problém týkající se fsync(), vznikl v roce 2018. Poté bylo v rámci projektu PostgreSQL zjištěno, že pokud funkce fsync() narazí na chybu, označí „špinavé“ stránky jako „čisté“. V důsledku toho následující hovory fsync() s takovými stránkami nic nedělejte. Z tohoto důvodu se upravené stránky ukládají do paměti a nikdy se nezapisují na disk. To je skutečná katastrofa, protože aplikace si bude myslet, že nějaká data jsou zapsána na disk, ale ve skutečnosti tomu tak nebude. Taková selhání fsync() jsou vzácné, aplikace v takových situacích nemůže udělat pro boj s problémem téměř nic. V těchto dnech, když k tomu dojde, PostgreSQL a další aplikace se zhroutí. Zde, v článku "Mohou se aplikace zotavit ze selhání fsync?" je tento problém podrobně prozkoumán. V současnosti je nejlepším řešením tohoto problému použití přímého I/O s příznakem O_SYNC nebo s vlajkou O_DSYNC. S tímto přístupem bude systém hlásit chyby, které mohou nastat při provádění specifických operací zápisu dat, ale tento přístup vyžaduje, aby aplikace spravovala vyrovnávací paměti sama. Přečtěte si o tom více zde и zde.

Otevírání souborů pomocí příznaků O_SYNC a O_DSYNC

Vraťme se k diskusi o linuxových mechanismech, které poskytují trvalé ukládání dat. Totiž, mluvíme o použití vlajky O_SYNC nebo vlajka O_DSYNC při otevírání souborů pomocí systémového volání OTEVŘENO(). S tímto přístupem se každá operace zápisu dat provádí jako po každém příkazu write() systém dostává, respektive příkazy fsync() и fdatasync(). V Specifikace POSIX toto se nazývá "Dokončení integrity synchronizovaného I/O souboru" a "Dokončení integrity dat". Hlavní výhodou tohoto přístupu je, že k zajištění integrity dat je třeba provést pouze jedno systémové volání, nikoli dvě (např. write() и fdatasync()). Hlavní nevýhodou tohoto přístupu je, že všechny operace zápisu pomocí odpovídajícího deskriptoru souboru budou synchronizovány, což může omezit schopnost strukturovat kód aplikace.

Použití přímého I/O s příznakem O_DIRECT

Systémové volání open() podporuje vlajku O_DIRECT, který je navržen tak, aby obcházel mezipaměť operačního systému, prováděl I/O operace a interagoval přímo s diskem. To v mnoha případech znamená, že příkazy k zápisu vydané programem budou přímo převedeny na příkazy zaměřené na práci s diskem. Obecně však tento mechanismus nenahrazuje funkce fsync() nebo fdatasync(). Faktem je, že samotný disk může zpoždění nebo mezipaměť příslušné příkazy pro zápis dat. A co je ještě horší, v některých speciálních případech I/O operace prováděné při použití příznaku O_DIRECT, přenos do tradičních operací s vyrovnávací pamětí. Nejjednodušší způsob, jak tento problém vyřešit, je použít příznak k otevření souborů O_DSYNC, což bude znamenat, že po každé operaci zápisu bude následovat volání fdatasync().

Ukázalo se, že souborový systém XFS nedávno přidal „rychlou cestu“ pro O_DIRECT|O_DSYNC- datové záznamy. Pokud je blok přepsán pomocí O_DIRECT|O_DSYNC, pak XFS místo vyprázdnění mezipaměti provede příkaz FUA write, pokud jej zařízení podporuje. Ověřil jsem to pomocí utility blktrace na systému Linux 5.4/Ubuntu 20.04. Tento přístup by měl být efektivnější, protože zapisuje minimální množství dat na disk a používá jednu operaci, nikoli dvě (zápis a vyprázdnění mezipaměti). Našel jsem odkaz na patch 2018 jádro, které implementuje tento mechanismus. O aplikaci této optimalizace na jiné souborové systémy se diskutuje, ale pokud vím, XFS je zatím jediný souborový systém, který ji podporuje.

funkce sync_file_range().

Linux má systémové volání rozsah_synchronních_souborů(), který umožňuje vyprázdnit na disk pouze část souboru, nikoli celý soubor. Toto volání zahájí asynchronní vyprázdnění a nečeká na jeho dokončení. Ale v odkazu na sync_file_range() tento příkaz je prý „velmi nebezpečný“. Nedoporučuje se jej používat. Vlastnosti a nebezpečí sync_file_range() velmi dobře popsáno v tohle materiál. Konkrétně se zdá, že toto volání používá RocksDB k řízení toho, kdy jádro vyprázdní „špinavá“ data na disk. Ale zároveň tam, aby bylo zajištěno stabilní ukládání dat, se také používá fdatasync(). V kód RocksDB má k tomuto tématu několik zajímavých komentářů. Například to vypadá jako hovor sync_file_range() při použití ZFS nevyprázdní data na disk. Zkušenosti mi říkají, že zřídka používaný kód může obsahovat chyby. Proto bych nedoporučoval používat toto systémové volání, pokud to není nezbytně nutné.

Systémová volání pomáhají zajistit stálost dat

Došel jsem k závěru, že existují tři přístupy, které lze použít k provádění perzistentních I/O operací. Všechny vyžadují volání funkce fsync() pro adresář, kde byl soubor vytvořen. Jedná se o tyto přístupy:

  1. Volání funkce fdatasync() nebo fsync() po funkci write() (je lepší použít fdatasync()).
  2. Práce s deskriptorem souboru otevřeným s příznakem O_DSYNC nebo O_SYNC (lépe - s vlajkou O_DSYNC).
  3. Použití příkazů pwritev2() s vlajkou RWF_DSYNC nebo RWF_SYNC (nejlépe s vlajkou RWF_DSYNC).

Poznámky k výkonu

Neměřil jsem pečlivě výkon různých mechanismů, které jsem zkoumal. Rozdíly, které jsem zaznamenal v rychlosti jejich práce, jsou velmi malé. To znamená, že se mohu mýlit a že za jiných podmínek může stejná věc vykazovat různé výsledky. Nejprve budu mluvit o tom, co ovlivňuje výkon více, a poté o tom, co ovlivňuje výkon méně.

  1. Přepisování dat souboru je rychlejší než přidávání dat do souboru (zvýšení výkonu může být 2–100 %). Připojení dat k souboru vyžaduje další změny v metadatech souboru, a to i po systémovém volání fallocate(), ale velikost tohoto efektu se může lišit. Pro nejlepší výkon doporučuji zavolat fallocate() k předběžnému přidělení požadovaného prostoru. Pak musí být tento prostor explicitně vyplněn nulami a volán fsync(). To způsobí, že odpovídající bloky v systému souborů budou označeny jako „přidělené“ místo „nepřidělené“. To poskytuje malé (asi 2%) zlepšení výkonu. Některé disky mohou mít také pomalejší operaci přístupu k prvnímu bloku než jiné. To znamená, že vyplnění prostoru nulami může vést k výraznému (asi 100%) zlepšení výkonu. To se může stát zejména u disků. AWS EBS (jedná se o neoficiální údaje, nepodařilo se mi je potvrdit). Totéž platí pro skladování. Trvalý disk GCP (a to je již oficiální informace, potvrzená testy). Ostatní odborníci udělali totéž pozorovánísouvisející s různými disky.
  2. Čím méně systémových volání, tím vyšší výkon (zisk může být asi 5 %). Vypadá to jako hovor open() s vlajkou O_DSYNC nebo zavolejte pwritev2() s vlajkou RWF_SYNC rychlejší hovor fdatasync(). Mám podezření, že zde jde o to, že u tohoto přístupu hraje roli skutečnost, že k vyřešení stejného úkolu je třeba provést méně systémových volání (jedno volání místo dvou). Rozdíl ve výkonu je ale velmi malý, takže jej můžete snadno ignorovat a v aplikaci použít něco, co nevede ke komplikaci její logiky.

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

  • Přístupové metody I/O — přehled základů vstupních/výstupních mechanismů.
  • Zajištění, aby se data dostala na disk - příběh o tom, co se děje s daty na cestě z aplikace na disk.
  • Kdy byste měli fsync obsahující adresář - odpověď na otázku, kdy se přihlásit fsync() pro adresáře. Stručně řečeno, ukázalo se, že to musíte udělat při vytváření nového souboru a důvodem tohoto doporučení je, že v Linuxu může být mnoho odkazů na stejný soubor.
  • SQL Server na Linuxu: FUA Internals - zde je popis, jak je implementováno trvalé ukládání dat v SQL Serveru na platformě Linux. Zde je několik zajímavých 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, o kterých jste si mysleli, že jsou bezpečně uložena na disku?

Odolné úložiště dat a Linux File API

Odolné úložiště dat a Linux File API

Zdroj: www.habr.com