Odolné ukladanie dát a Linux File API

Pri skúmaní udržateľnosti ukladania dát v cloudových systémoch som sa rozhodol otestovať, aby som sa uistil, že rozumiem základným veciam. ja začal prečítaním špecifikácie NVMe aby sme pochopili, aké záruky týkajúce sa perzistencie údajov (to znamená záruky dostupnosti údajov po zlyhaní systému) nám poskytujú disky NMVe. Urobil som tieto hlavné závery: musíte brať do úvahy poškodené údaje od okamihu zadania príkazu na zápis údajov až po ich zapísanie na pamäťové médium. Vo väčšine programov sa však systémové volania celkom bezpečne používajú na zapisovanie údajov.

V tomto článku skúmam mechanizmy pretrvávania, ktoré poskytujú rozhrania API súborov Linux. Zdá sa, že tu by malo byť všetko jednoduché: program volá príkaz write()a po dokončení operácie tohto príkazu budú údaje bezpečne uložené na disku. ale write() skopíruje iba údaje aplikácie do vyrovnávacej pamäte jadra umiestnenej v RAM. Aby bol systém nútený zapisovať dáta na disk, je potrebné použiť niektoré dodatočné mechanizmy.

Odolné ukladanie dát a Linux File API

Vo všeobecnosti je tento materiál súborom poznámok týkajúcich sa toho, čo som sa naučil na tému, ktorá ma zaujíma. Ak hovoríme veľmi stručne o tom najdôležitejšom, ukáže sa, že na organizáciu trvalo udržateľného ukladania údajov musíte použiť príkaz fdatasync() alebo otvárať súbory s príznakom O_DSYNC. Ak máte záujem dozvedieť sa viac o tom, čo sa deje s dátami na ceste z kódu na disk, pozrite sa na toto článok.

Vlastnosti použitia funkcie write().

Systémové volanie write() definované v norme IEEE POSIX ako pokus o zápis údajov do deskriptora súboru. Po úspešnom ukončení prác write() operácie čítania údajov musia vrátiť presne tie bajty, ktoré boli predtým zapísané, a to aj vtedy, keď sa k údajom pristupuje z iných procesov alebo vlákien (tu zodpovedajúca časť štandardu POSIX). Tu, v časti o tom, ako vlákna interagujú s normálnymi operáciami so súbormi, je poznámka, ktorá hovorí, že ak dve vlákna volajú tieto funkcie, potom každé volanie musí vidieť buď všetky určené dôsledky druhého volania, alebo žiadne. dôsledky. To vedie k záveru, že všetky I/O operácie so súbormi musia mať uzamknutý prostriedok, s ktorým pracujú.

Znamená to, že operácia write() je to atomove? Z technického hľadiska áno. Operácie čítania údajov musia vrátiť buď všetko, alebo nič z toho, čo bolo napísané write(). Ale operácia write(), v súlade so štandardom nemusí skončiť po zapísaní všetkého, čo bola požiadaná zapísať. Je povolené zapisovať len časť údajov. Napríklad by sme mohli mať dva prúdy, z ktorých každý pridáva 1024 bajtov k súboru opísanému rovnakým deskriptorom súboru. Z hľadiska štandardu bude výsledok prijateľný, keď každá operácia zápisu môže do súboru pripojiť iba jeden bajt. Tieto operácie zostanú atomické, ale po ich dokončení budú údaje, ktoré zapisujú do súboru, zmiešané. Tu veľmi zaujímavá diskusia na túto tému na Stack Overflow.

funkcie fsync() a fdatasync().

Najjednoduchší spôsob, ako vyprázdniť dáta na disk, je zavolať funkciu fsync(). Táto funkcia žiada operačný systém, aby presunul všetky upravené bloky z vyrovnávacej pamäte na disk. To zahŕňa všetky metadáta súboru (čas prístupu, čas úpravy súboru atď.). Verím, že tieto metadáta sú potrebné len zriedka, takže ak viete, že to pre vás nie je dôležité, môžete túto funkciu použiť fdatasync(). V Pomoc na fdatasync() hovorí, že počas činnosti tejto funkcie sa na disk ukladá také množstvo metadát, ktoré je „nevyhnutné pre správne vykonanie nasledujúcich operácií načítania údajov“. A práve na tom väčšine aplikácií záleží.

Jeden problém, ktorý tu môže nastať, je, že tieto mechanizmy nezaručujú, že súbor bude možné nájsť aj po prípadnom zlyhaní. Najmä pri vytváraní nového súboru by ste mali zavolať fsync() pre adresár, ktorý ho obsahuje. V opačnom prípade sa po havárii môže ukázať, že tento súbor neexistuje. Dôvodom je to, že v systéme UNIX môže v dôsledku použitia pevných odkazov súbor existovať vo viacerých adresároch. Preto pri volaní fsync() neexistuje spôsob, ako by súbor vedel, ktoré údaje adresára by sa mali tiež vyprázdniť na disk (tu Môžete si o tom prečítať viac). Zdá sa, že súborový systém ext4 je schopný automaticky platiť fsync() do adresárov obsahujúcich príslušné súbory, ale nemusí to tak byť pri iných súborových systémoch.

Tento mechanizmus môže byť implementovaný odlišne v rôznych súborových systémoch. použil som blktrace sa dozviete, aké diskové operácie sa používajú v súborových systémoch ext4 a XFS. Obaja vydávajú zvyčajné príkazy na zápis na disk pre obsah súborov aj pre žurnál súborového systému, vyprázdnia vyrovnávaciu pamäť a ukončia sa vykonaním zápisu FUA (Force Unit Access, zápis údajov priamo na disk, obídenie vyrovnávacej pamäte) do žurnálu. Pravdepodobne to robia len preto, aby potvrdili skutočnosť transakcie. Na jednotkách, ktoré nepodporujú FUA, to spôsobí dve vyprázdnenia vyrovnávacej pamäte. Moje experimenty to ukázali fdatasync() trochu rýchlejšie fsync(). Utility blktrace to naznačuje fdatasync() zvyčajne zapisuje menej údajov na disk (v ext4 fsync() píše 20 KiB, a fdatasync() - 16 kB). Tiež som zistil, že XFS je o niečo rýchlejší ako ext4. A tu s pomocou blktrace sa to podarilo zistiť fdatasync() vyprázdni menej dát na disk (4 KiB v XFS).

Nejednoznačné situácie pri používaní fsync()

Napadajú ma tri nejednoznačné situácie fsync()s ktorými som sa v praxi stretol.

Prvý takýto prípad sa stal v roku 2008. Potom rozhranie Firefoxu 3 zamrzlo, ak sa na disk zapísalo veľké množstvo súborov. Problém bol v tom, že implementácia rozhrania využívala na ukladanie informácií o jeho stave databázu SQLite. Po každej zmene, ktorá nastala v rozhraní, bola funkcia zavolaná fsync(), čo dáva dobré záruky stabilného ukladania dát. V súborovom systéme ext3 sa potom používa funkcia fsync() vyprázdniť na disk všetky „špinavé“ stránky v systéme, a nielen tie, ktoré súviseli s príslušným súborom. To znamenalo, že kliknutie na tlačidlo vo Firefoxe môže spôsobiť zápis megabajtov dát na magnetický disk, čo môže trvať niekoľko sekúnd. Riešenie problému, ako som pochopil to materiálu, bolo presunúť prácu s databázou do asynchrónnych úloh na pozadí. To znamená, že Firefox implementoval prísnejšie požiadavky na trvalosť úložiska, než bolo skutočne potrebné, a funkcie súborového systému ext3 tento problém len prehĺbili.

Druhý problém nastal v roku 2009. Potom, po páde systému, používatelia nového súborového systému ext4 čelili skutočnosti, že veľa novovytvorených súborov malo nulovú dĺžku, čo sa však nestalo so starším súborovým systémom ext3. V predchádzajúcom odseku som hovoril o tom, ako ext3 vyprázdnil príliš veľa dát na disk, čo veci značne spomalilo. fsync(). Na zlepšenie situácie ext4 vyprázdni iba tie "špinavé" stránky, ktoré sú relevantné pre konkrétny súbor. A dáta iných súborov zostávajú v pamäti oveľa dlhšie ako pri ext3. Toto bolo urobené s cieľom zlepšiť výkon (v predvolenom nastavení zostanú údaje v tomto stave 30 sekúnd, môžete to nakonfigurovať pomocou dirty_expire_centisecs; tu O tom môžete nájsť ďalšie materiály). To znamená, že veľké množstvo dát môže byť po zlyhaní nenávratne stratené. Riešením tohto problému je použitie fsync() v aplikáciách, ktoré potrebujú poskytovať stabilné úložisko dát a čo najviac ich chrániť pred následkami porúch. Funkcia fsync() pracuje oveľa efektívnejšie s ext4 ako s ext3. Nevýhodou tohto prístupu je, že jeho použitie, ako predtým, spomaľuje niektoré operácie, napríklad inštaláciu programov. Pozrite si podrobnosti o tomto tu и tu.

Tretí problém týkajúci sa fsync(), vznikla v roku 2018. Potom sa v rámci projektu PostgreSQL zistilo, že ak funkcia fsync() narazí na chybu, označí „špinavé“ stránky ako „čisté“. V dôsledku toho nasledujúce hovory fsync() s takýmito stránkami nič nerobte. Z tohto dôvodu sa upravené stránky ukladajú do pamäte a nikdy sa nezapisujú na disk. To je skutočná katastrofa, pretože aplikácia si bude myslieť, že nejaké dáta sú zapísané na disk, no v skutočnosti to tak nebude. Takéto zlyhania fsync() sú zriedkavé, aplikácia v takýchto situáciách nemôže urobiť takmer nič v boji proti problému. V týchto dňoch, keď sa to stane, PostgreSQL a ďalšie aplikácie padajú. Tu, v článku "Môžu sa aplikácie zotaviť po zlyhaniach fsync?" je tento problém podrobne preskúmaný. V súčasnosti je najlepším riešením tohto problému použitie priameho vstupu/výstupu s príznakom O_SYNC alebo s vlajkou O_DSYNC. S týmto prístupom bude systém hlásiť chyby, ktoré sa môžu vyskytnúť počas špecifických operácií zápisu, ale tento prístup vyžaduje, aby aplikácia sama spravovala vyrovnávacie pamäte. Prečítajte si o tom viac tu и tu.

Otváranie súborov pomocou príznakov O_SYNC a O_DSYNC

Vráťme sa k diskusii o mechanizmoch Linuxu, ktoré poskytujú stabilné ukladanie dát. Totiž, hovoríme o používaní vlajky O_SYNC alebo vlajka O_DSYNC pri otváraní súborov pomocou systémového volania otvorené(). Pri tomto prístupe sa každá operácia zápisu údajov vykonáva ako po každom príkaze write() systém dostáva, resp. príkazy fsync() и fdatasync(). V Špecifikácie POSIX toto sa nazýva "Dokončenie integrity synchronizovaného I/O súboru" a "Dokončenie integrity údajov". Hlavnou výhodou tohto prístupu je, že na zabezpečenie integrity údajov je potrebné vykonať iba jedno systémové volanie a nie dve (napríklad − write() и fdatasync()). Hlavnou nevýhodou tohto prístupu je, že všetky operácie zápisu pomocou zodpovedajúceho deskriptora súboru budú synchronizované, čo môže obmedziť schopnosť štruktúrovať kód aplikácie.

Použitie priameho I/O s príznakom O_DIRECT

Systémové volanie open() podporuje vlajku O_DIRECT, ktorý je navrhnutý tak, aby obchádzal vyrovnávaciu pamäť operačného systému, vykonával I/O operácie a interagoval priamo s diskom. To v mnohých prípadoch znamená, že príkazy na zápis vydané programom budú priamo preložené do príkazov zameraných na prácu s diskom. Vo všeobecnosti však tento mechanizmus nenahrádza funkcie fsync() alebo fdatasync(). Faktom je, že samotný disk môže oneskorenie alebo vyrovnávaciu pamäť zodpovedajúce príkazy na zápis údajov. A aby toho nebolo málo, v niektorých špeciálnych prípadoch I/O operácie vykonávané pri použití príznaku O_DIRECT, vysielať do tradičných operácií s vyrovnávacou pamäťou. Najjednoduchší spôsob, ako vyriešiť tento problém, je použiť príznak na otvorenie súborov O_DSYNC, čo bude znamenať, že po každej operácii zápisu bude nasledovať volanie fdatasync().

Ukázalo sa, že súborový systém XFS nedávno pridal „rýchlu cestu“ pre O_DIRECT|O_DSYNC- záznam údajov. Ak je blok prepísaný pomocou O_DIRECT|O_DSYNC, potom XFS namiesto vyprázdnenia vyrovnávacej pamäte vykoná príkaz zápisu FUA, ak to zariadenie podporuje. Overil som si to pomocou utility blktrace v systéme Linux 5.4/Ubuntu 20.04. Tento prístup by mal byť efektívnejší, keďže pri jeho použití sa na disk zapíše minimálne množstvo údajov a použije sa jedna operácia namiesto dvoch (zápis a vyprázdnenie vyrovnávacej pamäte). Našiel som odkaz na náplasť jadro 2018, ktoré implementuje tento mechanizmus. Existuje určitá diskusia o aplikácii tejto optimalizácie na iné súborové systémy, ale pokiaľ viem, XFS je zatiaľ jediný súborový systém, ktorý ju podporuje.

funkcia sync_file_range().

Linux má systémové volanie rozsah_synchronizovaných_súborov(), ktorý umožňuje vyprázdniť na disk iba časť súboru, nie celý súbor. Toto volanie spustí asynchrónne vyprázdnenie a nečaká na jeho dokončenie. Ale v odkaze na sync_file_range() tento príkaz je vraj „veľmi nebezpečný“. Neodporúča sa ho používať. Vlastnosti a nebezpečenstvá sync_file_range() veľmi dobre opísané v toto materiál. Konkrétne sa zdá, že toto volanie používa RocksDB na riadenie toho, kedy jadro vyprázdni „špinavé“ dáta na disk. Zároveň sa však používa aj na zabezpečenie stabilného ukladania údajov fdatasync(). V kód RocksDB má k tejto téme niekoľko zaujímavých komentárov. Napríklad to vyzerá ako hovor sync_file_range() pri použití ZFS nevyprázdni dáta na disk. Skúsenosti mi hovoria, že zriedka používaný kód môže obsahovať chyby. Preto neodporúčam používať toto systémové volanie, pokiaľ to nie je absolútne nevyhnutné.

Systémové volania, ktoré pomáhajú zabezpečiť stálosť údajov

Dospel som k záveru, že existujú tri prístupy, ktoré možno použiť na vykonávanie trvalých I/O operácií. Všetky vyžadujú volanie funkcie fsync() pre adresár, v ktorom bol súbor vytvorený. Ide o tieto prístupy:

  1. Volanie funkcie fdatasync() alebo fsync() po funkcii write() (je lepšie použiť fdatasync()).
  2. Práca s deskriptorom súboru otvoreným s príznakom O_DSYNC alebo O_SYNC (lepšie - s vlajkou O_DSYNC).
  3. Použitie príkazov pwritev2() s vlajkou RWF_DSYNC alebo RWF_SYNC (najlepšie s vlajkou RWF_DSYNC).

Poznámky k výkonu

Nemeral som starostlivo výkon rôznych mechanizmov, ktoré som skúmal. Rozdiely, ktoré som si všimol v rýchlosti ich práce, sú veľmi malé. To znamená, že sa môžem mýliť a že v iných podmienkach môže tá istá vec vykazovať odlišné výsledky. Najprv budem hovoriť o tom, čo ovplyvňuje výkon viac, a potom o tom, čo ovplyvňuje výkon menej.

  1. Prepisovanie údajov súboru je rýchlejšie ako pridávanie údajov do súboru (nárast výkonu môže byť 2 – 100 %). Pripojenie údajov k súboru vyžaduje dodatočné zmeny v metadátach súboru, a to aj po systémovom volaní fallocate(), ale veľkosť tohto efektu sa môže líšiť. Pre najlepší výkon odporúčam zavolať fallocate() na predbežné pridelenie požadovaného priestoru. Potom musí byť tento priestor explicitne vyplnený nulami a zavolaný fsync(). To spôsobí, že príslušné bloky v súborovom systéme budú označené ako „pridelené“ namiesto „nepridelené“. To poskytuje malé (asi 2%) zlepšenie výkonu. Niektoré disky môžu mať pomalšiu operáciu prístupu k prvému bloku ako iné. To znamená, že vyplnenie priestoru nulami môže viesť k výraznému (asi 100 %) zlepšeniu výkonu. To sa môže stať najmä pri diskoch. AWS EBS (ide o neoficiálne údaje, nepodarilo sa mi ich potvrdiť). To isté platí pre skladovanie. Trvalý disk GCP (a to sú už oficiálne informácie, potvrdené testami). To isté urobili aj ďalší odborníci pozorovaniesúvisiace s rôznymi diskami.
  2. Čím menej systémových volaní, tým vyšší výkon (zisk môže byť približne 5 %). Vyzerá to ako výzva open() s vlajkou O_DSYNC alebo zavolajte pwritev2() s vlajkou RWF_SYNC rýchlejší hovor fdatasync(). Mám podozrenie, že ide o to, že pri tomto prístupe zohráva úlohu skutočnosť, že na vyriešenie tej istej úlohy je potrebné vykonať menej systémových volaní (jeden hovor namiesto dvoch). Rozdiel vo výkone je ale veľmi malý, takže ho môžete ľahko ignorovať a použiť v aplikácii niečo, čo nevedie ku komplikácii jej logiky.

Ak vás zaujíma téma udržateľného ukladania údajov, tu je niekoľko užitočných materiálov:

  • I/O prístupové metódy — prehľad základov vstupno-výstupných mechanizmov.
  • Zabezpečenie, aby sa dáta dostali na disk — príbeh o tom, čo sa deje s dátami na ceste z aplikácie na disk.
  • Kedy by ste mali fsynchronizovať obsahujúci adresár - odpoveď na otázku, kedy podať žiadosť fsync() pre adresáre. Stručne povedané, ukázalo sa, že to musíte urobiť pri vytváraní nového súboru a dôvodom tohto odporúčania je, že v Linuxe môže byť veľa odkazov na rovnaký súbor.
  • SQL Server na Linuxe: FUA Internals — tu je popis, ako je implementované trvalé ukladanie údajov v SQL Server na platforme Linux. Tu je niekoľko zaujímavých porovnaní medzi systémovými volaniami Windows a Linux. Som si takmer istý, že práve vďaka tomuto materiálu som sa dozvedel o FUA optimalizácii XFS.

Stratili ste niekedy dáta, o ktorých ste si mysleli, že sú bezpečne uložené na disku?

Odolné ukladanie dát a Linux File API

Odolné ukladanie dát a Linux File API

Zdroj: hab.com