Pri skúmaní odolnosti ukladania dát v cloudových systémoch som sa rozhodol preveriť svoje vedomosti a uistiť sa, že rozumiem základným veciam. Ja aby som porozumel zárukám týkajúcim sa odolného ukladania dát (teda zárukám, že dáta budú dostupné po zlyhaní systému), ktoré nám poskytujú disky NVMe. Urobil som nasledujúce základné zistenia: dáta sa musia považovať za poškodené od okamihu vydania príkazu na zápis dát až do momentu, keď sa dokončí ich zápis na médium. Avšak vo väčšine programov na zápis dát sa bez problémov používajú systémové volania.
V tomto materiáli skúmam mechanizmy odolného ukladania dát, ktoré poskytujú súborové API Linuxu. Zdá sa, že by to malo byť jednoduché: program volá príkaz write(), a po dokončení tejto operácie budú dáta spoľahlivo uložené na disku. Ale write() iba kopíruje dáta aplikácie do vyrovnávacej pamäte jadra, ktorá sa nachádza v operačnej pamäti. Aby sa systém prinútil zapisovať dáta na disk, je potrebné použiť niektoré ďalšie mechanizmy.
Celkovo tento materiál predstavuje súbor poznámok o tom, čo som sa naučil k téme, ktorá ma zaujíma. Ak by som mal veľmi stručne zhrnúť to najdôležitejšie, tak vychádza, že na organizáciu odolného ukladania dát je potrebné používať príkaz fdatasync() alebo otvoriť súbory s pr flagom O_DSYNC. Ak vás zaujíma podrobne, čo sa deje s dátami na ceste od programového kódu k disku, pozrite sa na článok.
Vlastnosti používania funkcie write()
Systémové volanie write() je definované v štandarde ako pokus o zápis dát do súborového deskriptora. Po úspešnom dokončení, write() operácie čítania dát by mali vracať presne tie bajty, ktoré boli predtým zapísané, a to aj v prípade, že sú k dátam pristupované z iných procesov alebo vlákien ( príslušná časť štandardu POSIX). , v sekcii týkajúcej sa interakcie tokov s bežnými súborovými operáciami sa nachádza poznámka, ktorá uvádza, že ak každý z dvoch tokov volá tieto funkcie, každý volanie musí vidieť buď všetky označené následky, ku ktorým vedie vykonávanie iného volania, alebo nevidieť vôbec žiadne následky. To umožňuje usudzovať, že všetky operácie vstupu/výstupu súborov musia udržiavať zámok na zdroji, s ktorým pracujú.
Znamená to, že operácia write() je atómová? Technicky áno. Operácie čítania dát by mali vrátiť buď všetko, alebo nič z toho, čo bolo zapísané pomocou write(). No operácia write(), podľa štandardu, nemusí nevyhnutne skončiť zápisom všetkého, čo jej bolo navrhnuté zapísať. Jej je dovolené zapísať len časť údajov. Napríklad môžeme mať dva toky, z ktorých každý pridáva 1024 bajtov do súboru opisovaného rovnakým súborovým deskriptorom. Z hľadiska štandardu bude prijateľným výsledkom, ak každá z operácií zápisu bude môcť pridať k súboru iba jeden bajt. Tieto operácie zostanú atómové, ale po ich dokončení budú údaje, ktoré zapísali do súboru, zmiešané. veľmi zaujímavá diskusia na túto tému na Stack Overflow.
Funkcie fsync() a fdatasync()
Najjednoduchší spôsob, ako preniesť dáta na disk, spočíva vo volaní funkcie . Táto funkcia žiada operačný systém, aby preniesol všetky modifikované bloky z cache na disk. Sem patrí aj všetka metadata súboru (čas prístupu, čas modifikácie súboru a tak ďalej). Domnievam sa, že potreba týchto métadát nastáva zriedka, takže ak viete, že pre vás nie sú dôležité, môžete používať funkciu fdatasync(). V o fdatasync() sa uvádza, že počas práce tejto funkcie sa vykonáva prenos na disk takého objemu métadát, ktorý "je potrebný na správne vykonanie nasledujúcich operácií čítania údajov". A to je presne to, čo trápi väčšinu aplikácií.
Jedným z problémov, ktoré sa tu môžu vyskytnúť, je, že tieto mechanizmy nezaručujú, že súbor bude možné po možnej poruche znovu nájsť. Najmä pri vytváraní nového súboru je potrebné zavolať fsync() pre adresár, ktorý ho obsahuje. Inak sa po zlyhaní môže stať, že tento súbor neexistuje. Dôvodom je, že v UNIXe, kvôli použitiu tvrdých odkazov, môže súbor existovať v niekoľkých adresároch. Preto pri vyvolaní fsync() pre súbor neexistuje spôsob, ako zistiť, ktoré údaje konkrétneho adresára je tiež potrebné zapísať na disk ( o tomto sa dá prečítať podrobnejšie). Vyzerá to, že súborový systém ext4 je schopný používať fsync() na adresáre, ktoré obsahujú príslušné súbory, ale v prípade iných súborových systémov to nemusí byť tak.
Tento mechanizmus môže byť rôzne implementovaný v rôznych súborových systémoch. Používal som na to, aby som zistil, aké diskové operácie sa používajú v súborových systémoch ext4 a XFS. Obe generujú bežné príkazy na zápis na disk a pre obsah súborov, aj pre žurnál súborového systému, uvoľňujú cache a končia práci vykonávajúc FUA-zápis (Force Unit Access, zápis údajov priamo na disk, obchádzajúc cache) do žurnálu. Pravdepodobne to robia práve tak, aby potvrdili fakt uskutočnenia operácie. Na diskoch, ktoré nepodporujú FUA, to vyžaduje dva zápisy cache. Moje experimenty ukázali, že fdatasync() trochu rýchlejšie fsync(). Nástroj blktrace naznačuje, že fdatasync() zvyčajne píše na disk menej údajov (v ext4 fsync() zapisuje 20 KiB, a fdatasync() — 16 KiB). Okrem toho som zistil, že XFS je trochu rýchlejší ako ext4. A tu pomocou blktrace sa mi podarilo zistiť, že fdatasync() zapisuje na disk menej údajov (4 KiB v XFS).
Nejednoznačné situácie, ktoré vznikajú pri používaní fsync()
Môžem si spomenúť na tri nejednoznačné situácie, ktoré sa týkajú fsync(), s ktorými som sa stretol v praxi.
Prvý taký prípad sa stal v roku 2008. Vtedy rozhranie Firefox 3 „zasekávalo“, ak sa vykonával zápis na disk veľkého množstva súborov. Problém spočíval v tom, že v implementácii rozhrania na uloženie informácií o jeho stave bola použitá databáza SQLite. Po každej zmene, ktorá nastala v rozhraní, sa volala funkcia fsync(), čo poskytovalo dobré záruky trvalého uchovávania údajov. Vtedy používaný súborový systém ext3 funkciu fsync() vymazávalo na disk všetky „špinavé“ stránky v systéme, nielen tie, ktoré mali súvislosť s príslušným súborom. To znamenalo, že kliknutie na tlačidlo vo Firefoxe mohlo spustiť zápis megabajtov údajov na magnetický disk, čo mohlo trvať dlhé sekundy. Riešenie problému, ako som pochopil z materiálu, spočívalo v presunutí práce s databázou do asynchrónnych pozadových úloh. To znamená, že skôr vo Firefoxe boli zavedené prísnejšie požiadavky na stabilitu ukladania údajov, než by bolo naozaj potrebné, a špecifiká súborového systému ext3 túto problém len zhoršili.
Druhá nezrovnalosť sa stala v roku 2009. Vtedy, po zlyhaní systému, používatelia novej súborovej sústavy ext4 narazili na problém, že mnohé nedávno vytvorené súbory majú nulovú dĺžku, zatiaľ čo pri staršej súborovej sústave ext3 sa to nestalo. V predchádzajúcom odseku som hovoril o tom, že ext3 zaznamenávala na disk príliš veľa údajov, čo výrazne spomaľovalo prácu fsync(). Aby sa situácia zlepšila, v ext4 sa na disk zaznamenávajú iba tie „špinavé“ stránky, ktoré sa týkajú konkrétneho súboru. A údaje iných súborov zostávajú v pamäti oveľa dlhšie, než pri použití ext3. Tieto zmeny boli vykonané na zlepšenie výkonu (údajne údaje zostávajú v tomto stave 30 sekúnd, nastavenia možné s pomocou ; nájdete ďalšie materiály o tom). To znamená, že veľké množstvo údajov môže byť nezvratne stratené po zlyhaní. Riešením tohto problému je používanie fsync() v aplikáciách, ktoré musia zabezpečiť stálosť ukladania údajov a maximálne ich ochrániť pred následkami zlyhaní. Funkcia fsync() pracuje pri použití ext4 oveľa efektívnejšie než pri použití ext3. Nevýhodou tohto prístupu je, že jeho použitie, ako aj predtým, spomaľuje vykonávanie niektorých operácií, ako je inštalácia programov. Podrobnosti o tom pozrite a .
Tretí problém, ktorý sa týka fsync(), sa objavil v roku 2018. Vtedy, v rámci projektu PostgreSQL, sa zistilo, že ak funkcia fsync() narazí na chybu, označuje „špinavé“ stránky ako „čisté“. V dôsledku toho nasledujúce volania fsync() s takýmito stránkami sa nič nerobí. Kvôli tomu sa upravené stránky uchovávajú v pamäti a nikdy sa nezapisujú na disk. To je naozaj katastrofa, pretože aplikácia si bude myslieť, že nejaké údaje sú zapísané na disku, ale v skutočnosti to tak nie je. Takéto zlyhania fsync() sa zriedkavo vyskytujú, aplikácia v takých situáciách takmer nič nemôže urobiť na obranu pred problémom. V súčasnosti, keď sa to stane, PostgreSQL a ostatné aplikácie havarijne ukončia činnosť. , v materiáli „Môžu aplikácie obnoviť zlyhania fsync?“, je tento problém preskúmaný vo všetkých podrobnostiach. V súčasnosti je najlepším riešením tohto problému použitie Direct I/O s príznakom O_SYNC alebo s príznakom O_DSYNC. Pri takomto prístupe systém oznámi chyby, ktoré sa môžu vyskytnúť pri vykonávaní konkrétnych operácií zápisu údajov, ale tento prístup vyžaduje, aby aplikácia spravovala vyrovnávacie pamäte sama. Podrobnosti o tomto si prečítajte a .
Otváranie súborov pomocou príznakov O_SYNC a O_DSYNC
Vráťme sa k diskusii o mechanizmoch Linuxu, ktoré zabezpečujú trvalé ukladanie dát. Konkrétne ide o použitie príznaku O_SYNC alebo príznaku O_DSYNC pri otváraní súborov pomocou systémového volania . Pri takomto prístupe sa každá operácia zápisu údajov vykonáva tak, akoby po každej miere write() systému sa dávajú, prípadne príkazy fsync() a fdatasync(). V to sa nazýva „Synchronized I/O File Integrity Completion“ a „Data Integrity Completion“. Hlavnou výhodou tohto prístupu je, že na zabezpečenie integrity dát je potrebné vykonať iba jeden systémový príkaz, nie dva (napríklad — write() a fdatasync()). Hlavnou nevýhodou tohto prístupu je, že všetky operácie zápisu, ktoré používajú príslušný súborový deskriptor, budú synchronizované, čo môže obmedziť možnosti štruktúrovania kódu aplikácie.
Použitie Direct I/O s príznakom O_DIRECT
Systémové volanie open() podporuje príznak O_DIRECT, ktorý je určený na to, aby sa, obchádzajúc vyrovnávaciu pamäť operačného systému, vykonávali operácie vstupu-výstupu priamo s diskom. To vo väčšine prípadov znamená, že príkazy zápisu, ktoré vydáva program, budú priamo prekladané do príkazov zameraných na prácu s diskom. Ale vo všeobecnosti tento mechanizmus nie je náhradou funkcií fsync() alebo fdatasync(). Faktom je, že samotný disk môže prislúchajúce príkazy na zápis dát. A čo je ešte horšie, v niektorých špeciálnych prípadoch operácie vstupu a výstupu vykonávané s použitím príznaku O_DIRECT, na tradičné buffered operácie. Najjednoduchší spôsob, ako túto situáciu vyriešiť, je použiť na otvorenie súborov aj príznak O_DSYNC, čo bude znamenať, že každá operácia zápisu bude nasledovaná volaním fdatasync().
Ukázalo sa, že v súborovom systéme XFS bol nedávno pridaný "rýchly spôsob" pre O_DIRECT|O_DSYNC-zápis dát. Ak sa prepisuje blok s použitím O_DIRECT|O_DSYNC, XFS, namiesto vyprázdnenia cache, vykoná príkaz FUA-zápisu, ak to zariadenie podporuje. Potvrdil som to pomocou nástroja blktrace v systéme Linux 5.4/Ubuntu 20.04. Tento prístup by mal byť efektívnejší, pretože jeho použitím sa na disk zapisuje minimálne množstvo údajov a zároveň sa vykonáva jedna operácia, a nie dve (zápis a vyprázdnenie cache). Našiel som odkaz na jadro z roku 2018, v ktorom je tento mechanizmus implementovaný. Tam je aj diskusia o aplikácii tejto optimalizácie aj v iných súborových systémoch, ale pokiaľ viem, XFS je zatiaľ jediný súborový systém, ktorý to podporuje.
Funkcia sync_file_range()
V Linuxe existuje systémový volanie , ktoré umožňuje vyprázdniť na disk iba časť súboru, a nie celý súbor. Toto volanie iniciuje asynchrónne vyprázdnenie dát a nečaká na jeho dokončenie. Ale v dokumentácii k sync_file_range() sa uvádza, že tento príkaz "je veľmi nebezpečný". Odporúča sa ním nepreraďovať. Špecifiká a nebezpečenstvá sync_file_range() sú veľmi dobre popísané v materiáli. Konkrétne, zjavne toto volanie používa RocksDB na riadenie toho, kedy jadro vyprázdňuje "špinavé" dáta na disk. Ale zároveň tam pre zabezpečenie trvalého ukladania údajov sa používa aj fdatasync(). V RocksDB má na túto tému zaujímavé komentáre. Napríklad, zdá sa, že volanie sync_file_range() pri použití ZFS nevedie k vyprázdneniu dát na disk. Skúsenosť mi hovorí, že kód, ktorý sa používa zriedka, môže obsahovať chyby. Preto by som odporučil nepoužívať toto systémové volanie bez akútnej potreby.
Systémové volania, ktoré pomáhajú zabezpečiť trvalé ukladanie údajov
Dospel som k záveru, že na vykonávanie vstupno/výstupných operácií zaisťujúcich trvalé uloženie dát je možné použiť tri prístupy. Všetky si vyžadujú zavolanie funkcie fsync() pre adresár, v ktorom bol súbor vytvorený. Tieto prístupy sú nasledovné:
- Zavolanie funkcie
fdatasync()alebofsync()po funkciiwrite()(najlepšie používaťfdatasync()). - Práca s popisovačom súboru, ktorý je otvorený s príznakom
O_DSYNCaleboO_SYNC(najlepšie — s príznakomO_DSYNC). - Použitie príkazu
pwritev2()s príznakomRWF_DSYNCaleboRWF_SYNC(preferovanejšie — s príznakomRWF_DSYNC).
Poznámky o výkonnosti
Nerozumel som presne meraniu výkonu rôznych zo mňa preskúmaných mechanizmov. Rozdiely v rýchlosti, ktoré som si všimol, sú veľmi malé. To znamená, že sa môžem mýliť a to, čo v iných podmienkach by mohlo priniesť iné výsledky. Najprv poviem o tom, čo viac ovplyvňuje výkon, a potom, o tom, čo ovplyvňuje výkon menej.
- Prepisovanie dát súboru je rýchlejšie ako pridávanie dát k súboru (zisk v výkonu môže byť 2-100%). Pridávanie dát k súboru si vyžaduje dodatočné zmeny v metadátach súboru, aj po systémovom volaní
fallocate(), ale veľkosť tohto efektu sa môže líšiť. Odporúčam, aby ste na zabezpečenie najlepšieho výkonu zavolalifallocate()na predprípravu potrebného priestoru. Potom je potrebné tento priestor explicitne vyplniť nulami a zavolaťfsync(). Vďaka tomu budú zodpovedajúce bloky vo filesystéme označené ako 'vyhradené', a nie ako 'nevyhradené'. To prináša malé (asi 2%) zlepšenie výkonu. Navyše, pri niektorých diskoch môže byť prvá operácia prístupu k bloku vykonaná pomalšie ako ostatné. To znamená, že vyplnenie priestoru nulami môže viesť k významnému (asi 100%) zlepšeniu výkonu. Osobitne sa to môže stať s diskami (toto sú neoficiálne údaje, nemohol som ich potvrdiť). To isté sa týka aj úložísk (a to je už oficiálna informácia, potvrdená testami). Iní odborníci urobili rovnaké , ktoré sa týkajú rôznych diskov. - Čím menej systémových volaní — tým vyšší výkon (zisk môže byť okolo 5%). Zdá sa, že volanie
open()s príznakomO_DSYNCalebo volaniepwritev2()s príznakomRWF_SYNCje rýchlejšie než volaniefdatasync()Podozrievam, že za tým stojí to, že pri tomto prístupe zohráva úlohu fakt, že na vyriešenie tej istej úlohy je potrebné vykonať menej systémových volaní (jedno volanie namiesto dvoch). Avšak rozdiel vo výkonnosti je veľmi malý, takže si môžete pokojne nevšímať a použiť v aplikácii to, čo nebude viesť k zložitosti 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:
- — prehľad základov mechanizmov vstupu/výstupu.
- — rozprávanie o tom, čo sa deje s údajmi na ceste od aplikácie k disku.
- — odpoveď na otázku, kedy je potrebné aplikovať
fsync()pre adresáre. Ak by som to mal stručne povedať, treba to robiť pri vytváraní nového súboru, a dôvodom tejto odporúčania je, že v Linuxe môže existovať veľa odkazov na ten istý súbor. - — tu je opísané, ako je udržateľné ukladanie údajov implementované v SQL Serveri na platforme Linux. Obsahuje niektoré zaujímavé porovnania medzi systémovými volaniami Windows a Linuxu. Som takmer presvedčený, že práve vďaka tomuto materiálu som sa dozvedel o optimalizácii FUA na XFS.
Stratili ste niekedy údaje, ktoré ste považovali za bezpečne uložené na disku?
Zdroj: habr.com
