Tartós adattárolás és Linux fájl API-k

Én, a felhő rendszerekben az adattárolás stabilitását kutatva, úgy döntöttem, kipróbálom magam, hogy megbizonyosodjak az alapvető dolgokról. én az NVMe specifikáció elolvasásával kezdődött hogy megértsük, milyen garanciákat biztosítunk az adatok fennmaradására (vagyis arra, hogy egy rendszerhiba után az adatok elérhetők lesznek) NMVe lemezeket. A következő fő következtetéseket vontam le: az adatírási parancs kiadásának pillanatától kezdve a tárolóeszközre való írásig sérültnek kell tekinteni az adatokat. A legtöbb programban azonban a rendszerhívások biztonságosan használhatók adatírásra.

Ebben a cikkben a Linux fájl API-k által biztosított fennmaradási mechanizmusokat fedezem fel. Úgy tűnik, itt mindennek egyszerűnek kell lennie: a program meghívja a parancsot write(), és a parancs végrehajtása után az adatok biztonságosan tárolódnak a lemezen. De write() csak az alkalmazásadatokat másolja a RAM-ban található kernel-gyorsítótárba. Ahhoz, hogy a rendszert arra kényszerítsük, hogy adatokat írjon lemezre, néhány további mechanizmust kell alkalmazni.

Tartós adattárolás és Linux fájl API-k

Általánosságban elmondható, hogy ez az anyag egy számomra érdekes témában tanultakkal kapcsolatos megjegyzések sorozata. Ha nagyon röviden beszélünk a legfontosabbról, akkor kiderül, hogy a fenntartható adattárolás megszervezéséhez a parancsot kell használni fdatasync() vagy nyissa meg a fájlokat zászlóval O_DSYNC. Ha többet szeretne megtudni arról, hogy mi történik az adatokkal a kódtól a lemezig terjedő úton, vessen egy pillantást ezt cikk.

A write() függvény használatának jellemzői

Rendszerhívás write() szabványban meghatározott IEEE POSIX mint kísérlet arra, hogy adatokat írjunk egy fájlleíróba. A munka sikeres elvégzése után write() az adatolvasási műveleteknek pontosan azokat a bájtokat kell visszaadniuk, amelyeket korábban megírtak, még akkor is, ha az adatokat más folyamatokból vagy szálakból érik el (itt a POSIX szabvány megfelelő szakasza). Itt, a szálak és a normál fájlműveletek kölcsönhatásáról szóló részben van egy megjegyzés, amely szerint ha két-két szál hívja meg ezeket a függvényeket, akkor minden hívásnak látnia kell az összes jelzett következményt, amelyhez a másik hívás végrehajtása vezet, vagy egyáltalán nem lát következményeket. Ebből arra a következtetésre juthatunk, hogy minden fájl I/O műveletnek zárolnia kell a feldolgozott erőforrást.

Ez azt jelenti, hogy a művelet write() atomos? Technikai szempontból igen. Az adatolvasási műveleteknek vagy az egészet, vagy semmit sem kell visszaadniuk abból, amivel írták write(). De a művelet write(), a szabványnak megfelelően, nem kell véget érnie, felírt mindent, amit kértek tőle. Az adatoknak csak egy részét szabad írni. Például két adatfolyamunk lehet, amelyek mindegyike 1024 bájtot fűz hozzá egy fájlhoz, amelyet ugyanaz a fájlleíró ír le. A szabvány szempontjából az eredmény akkor lesz elfogadható, ha mindegyik írási művelet csak egy bájtot tud hozzáfűzni a fájlhoz. Ezek a műveletek atomi maradnak, de befejezésük után a fájlba írt adatok összekeverednek. Itt nagyon érdekes vita erről a témáról a Stack Overflow-n.

fsync() és fdatasync() függvények

Az adatok lemezre ürítésének legegyszerűbb módja a függvény meghívása fsync(). Ez a funkció arra kéri az operációs rendszert, hogy helyezzen át minden módosított blokkot a gyorsítótárból a lemezre. Ez magában foglalja a fájl összes metaadatát (hozzáférési idő, fájlmódosítási idő stb.). Úgy gondolom, hogy ezekre a metaadatokra ritkán van szükség, ezért ha tudja, hogy nem fontosak Önnek, használhatja a funkciót fdatasync(). -Ban Segítség on fdatasync() azt írja, hogy ennek a funkciónak a működése során olyan mennyiségű metaadat kerül lemezre, amely "szükséges a következő adatolvasási műveletek helyes végrehajtásához". És pontosan ez az, ami a legtöbb alkalmazás számára fontos.

Az egyik probléma, amely itt felmerülhet, az, hogy ezek a mechanizmusok nem garantálják, hogy a fájl megtalálható egy esetleges hiba után. Különösen új fájl létrehozásakor kell meghívni fsync() az azt tartalmazó könyvtárhoz. Ellenkező esetben egy összeomlás után kiderülhet, hogy ez a fájl nem létezik. Ennek az az oka, hogy UNIX alatt a kemény hivatkozások használata miatt egy fájl több könyvtárban is létezhet. Ezért híváskor fsync() egy fájl nem tudja megtudni, hogy melyik könyvtár adatait is ki kell üríteni a lemezre (itt erről bővebben olvashat). Úgy tűnik, hogy az ext4 fájlrendszer képes erre automatikusan alkalmaz fsync() a megfelelő fájlokat tartalmazó könyvtárakba, de ez más fájlrendszereknél nem biztos, hogy így van.

Ez a mechanizmus különböző módon valósítható meg a különböző fájlrendszerekben. használtam blktrace megtudhatja, milyen lemezműveleteket használnak az ext4 és XFS fájlrendszerek. Mindketten a szokásos írási parancsokat adják ki a lemezre mind a fájlok tartalmához, mind a fájlrendszer naplójához, kiürítik a gyorsítótárat és kilépnek egy FUA (Force Unit Access, adatok írása közvetlenül a lemezre, a gyorsítótár megkerülése) végrehajtásával a naplóba. Valószínűleg csak azért teszik ezt, hogy megerősítsék a tranzakció tényét. A FUA-t nem támogató meghajtókon ez két gyorsítótár-öblítést okoz. Kísérleteim ezt mutatták fdatasync() egy kicsit gyorsabban fsync(). Hasznosság blktrace azt jelzi fdatasync() általában kevesebb adatot ír a lemezre (ext4 fsync() 20 KiB-t ír, és fdatasync() - 16 KiB). Arra is rájöttem, hogy az XFS valamivel gyorsabb, mint az ext4. És itt a segítséggel blktrace ezt sikerült megtudnia fdatasync() kevesebb adatot ürít a lemezre (4 KiB XFS-ben).

Kétértelmű helyzetek az fsync() használatakor

Három kétértelmű helyzet jut eszembe ezzel kapcsolatban fsync()amellyel a gyakorlatban találkoztam.

Az első ilyen eset 2008-ban történt. Abban az időben a Firefox 3 felülete „lefagyott”, ha nagyszámú fájlt írtak lemezre. A probléma az volt, hogy az interfész megvalósítása egy SQLite adatbázist használt az állapotára vonatkozó információk tárolására. A felületen bekövetkezett minden változás után a függvény meghívásra került fsync(), amely jó garanciákat adott a stabil adattárolásra. Az akkor használt ext3 fájlrendszerben a függvény fsync() a rendszerben lévő összes "piszkos" oldalt lemezre kell öblíteni, nem csak azokat, amelyek a megfelelő fájlhoz kapcsolódnak. Ez azt jelentette, hogy a Firefoxban egy gomb megnyomása megabájtnyi adatot írhat egy mágneslemezre, ami sok másodpercig tarthat. A probléma megoldása, amennyire értettem azt Az adatbázissal végzett munka áthelyezése aszinkron háttérfeladatokra volt. Ez azt jelenti, hogy a Firefox korábban a valóban szükségesnél szigorúbb tárolási perzisztencia követelményeket hajtott végre, és az ext3 fájlrendszer funkciói csak súlyosbították ezt a problémát.

A második probléma 2009-ben történt. Aztán egy rendszerösszeomlás után az új ext4 fájlrendszer felhasználói azt tapasztalták, hogy sok újonnan létrehozott fájl nulla hosszúságú, de ez nem történt meg a régebbi ext3 fájlrendszerrel. Az előző bekezdésben arról beszéltem, hogy az ext3 túl sok adatot rakott ki a lemezre, ami nagyon lelassította a dolgokat. fsync(). A helyzet javítása érdekében az ext4 csak azokat a "piszkos" oldalakat üríti ki, amelyek egy adott fájl szempontjából relevánsak. Más fájlok adatai pedig sokkal hosszabb ideig maradnak a memóriában, mint az ext3 esetében. Ez a teljesítmény javítása érdekében történt (alapértelmezés szerint az adatok ebben az állapotban maradnak 30 másodpercig, ezt a dirty_expire_centisecs; itt erről bővebb információt találhat). Ez azt jelenti, hogy egy összeomlás után nagy mennyiségű adat helyrehozhatatlanul elveszhet. A probléma megoldása a használat fsync() olyan alkalmazásokban, amelyeknek stabil adattárolást kell biztosítaniuk, és a lehető legjobban meg kell védeniük őket a meghibásodások következményeitől. Funkció fsync() sokkal hatékonyabban működik ext4-el, mint ext3-mal. Ennek a megközelítésnek az a hátránya, hogy használata a korábbiakhoz hasonlóan lelassít bizonyos műveleteket, például a programok telepítését. Lásd erről a részleteket itt и itt.

A harmadik probléma ezzel kapcsolatban fsync(), 2018-ban keletkezett. Aztán a PostgreSQL projekt keretein belül kiderült, hogy ha a függvény fsync() hibát észlel, a "piszkos" oldalakat "tisztának" jelöli. Ennek eredményeként a következő hívások fsync() ne csinálj semmit az ilyen oldalakkal. Emiatt a módosított oldalak a memóriában maradnak, és soha nem íródnak lemezre. Ez egy igazi katasztrófa, mivel az alkalmazás azt gondolja, hogy bizonyos adatok lemezre vannak írva, de valójában nem. Ilyen kudarcok fsync() ritkák, az ilyen helyzetekben történő alkalmazás szinte semmit sem tud a probléma leküzdésére. Manapság, amikor ez megtörténik, a PostgreSQL és más alkalmazások összeomlanak. IttA „Can Applications Recover from fsync Failures?” című cikkben ezt a problémát részletesen megvizsgáljuk. Jelenleg a legjobb megoldás erre a problémára a Direct I/O használata a jelzővel O_SYNC vagy zászlóval O_DSYNC. Ezzel a megközelítéssel a rendszer jelenteni fogja azokat a hibákat, amelyek adott adatírási műveletek végrehajtásakor fordulhatnak elő, de ez a megközelítés megköveteli, hogy az alkalmazás maga kezelje a puffereket. Olvass tovább róla itt и itt.

Fájlok megnyitása az O_SYNC és O_DSYNC jelzők használatával

Térjünk vissza a tartós adattárolást biztosító Linux-mechanizmusok tárgyalásához. Mégpedig a zászló használatáról beszélünk O_SYNC vagy zászlót O_DSYNC fájlok rendszerhívással történő megnyitásakor nyisd ki(). Ezzel a megközelítéssel minden adatírási műveletet úgy hajtanak végre, mintha minden egyes parancs után write() a rendszer parancsokat kap fsync() и fdatasync(). -Ban POSIX specifikációk ezt "Szinkronizált I/O fájl integritás befejezésének" és "Adatintegritás befejezésének" nevezik. Ennek a megközelítésnek a fő előnye, hogy az adatok integritásának biztosításához csak egy rendszerhívást kell végrehajtani, nem pedig kettőt (pl. write() и fdatasync()). Ennek a megközelítésnek a fő hátránya, hogy a megfelelő fájlleírót használó összes írási művelet szinkronizálva lesz, ami korlátozhatja az alkalmazáskód strukturálásának lehetőségét.

Közvetlen I/O használata az O_DIRECT jelzővel

Rendszerhívás open() támogatja a zászlót O_DIRECT, amelynek célja az operációs rendszer gyorsítótárának megkerülése, I / O műveletek végrehajtása, közvetlenül a lemezzel kommunikálva. Ez sok esetben azt jelenti, hogy a program által kiadott írási parancsok közvetlenül a lemezzel való munkavégzésre irányuló parancsokká lesznek lefordítva. De általában ez a mechanizmus nem helyettesíti a funkciókat fsync() vagy fdatasync(). A helyzet az, hogy maga a lemez képes késleltetés vagy gyorsítótár megfelelő parancsok az adatok írásához. És ami még rosszabb, néhány speciális esetben a zászló használatakor végrehajtott I / O műveletek O_DIRECT, adás hagyományos pufferelt műveletekbe. A probléma megoldásának legegyszerűbb módja a zászló használata a fájlok megnyitásához O_DSYNC, ami azt jelenti, hogy minden írási műveletet egy hívás követ fdatasync().

Kiderült, hogy az XFS fájlrendszer nemrég hozzáadott egy "gyors elérési utat" a számára O_DIRECT|O_DSYNC- adatrekordok. Ha a blokk felülírása a O_DIRECT|O_DSYNC, akkor az XFS a gyorsítótár kiürítése helyett végrehajtja a FUA írási parancsot, ha az eszköz támogatja. Ezt a segédprogram segítségével ellenőriztem blktrace Linux 5.4/Ubuntu 20.04 rendszeren. Ennek a megközelítésnek hatékonyabbnak kell lennie, mivel a minimális adatmennyiséget írja ki a lemezre, és egy műveletet használ, nem pedig kettőt (a gyorsítótár írása és kiürítése). Találtam egy linket tapasz 2018 kernel, amely megvalósítja ezt a mechanizmust. Vannak viták az optimalizálás más fájlrendszerekre való alkalmazásáról, de tudomásom szerint az XFS az egyetlen fájlrendszer, amely támogatja ezt.

sync_file_range() függvény

A Linuxnak van rendszerhívása sync_file_range(), amely lehetővé teszi, hogy a fájlnak csak egy részét ürítse ki a lemezre, a teljes fájlt nem. Ez a hívás aszinkron öblítést kezdeményez, és nem várja meg annak befejezését. De a hivatkozásban sync_file_range() ezt a parancsot "nagyon veszélyesnek" mondják. Használata nem javasolt. Jellemzők és veszélyek sync_file_range() nagyon jól leírva ezt anyag. Úgy tűnik, hogy ez a hívás a RocksDB-t használja annak szabályozására, hogy a kernel mikor öblítse ki a "piszkos" adatokat a lemezre. De ugyanakkor ott is használják a stabil adattárolás érdekében fdatasync(). -Ban kód A RocksDB-nek van néhány érdekes megjegyzése ebben a témában. Például úgy néz ki, mint a hívás sync_file_range() a ZFS használatakor nem húzza ki az adatokat a lemezre. A tapasztalat azt mutatja, hogy a ritkán használt kód hibákat tartalmazhat. Ezért azt tanácsolom, hogy ne használja ezt a rendszerhívást, hacsak nem feltétlenül szükséges.

Rendszerhívások az adatok fennmaradásának biztosítására

Arra a következtetésre jutottam, hogy három megközelítés használható a tartós I/O műveletek végrehajtására. Mindegyik függvényhívást igényel fsync() ahhoz a könyvtárhoz, ahol a fájl létrejött. Ezek a megközelítések:

  1. Funkcióhívás fdatasync() vagy fsync() funkció után write() (jobb használni fdatasync()).
  2. Zászlóval megnyitott fájlleíró használata O_DSYNC vagy O_SYNC (jobb - zászlóval O_DSYNC).
  3. Parancshasználat pwritev2() zászlóval RWF_DSYNC vagy RWF_SYNC (lehetőleg zászlóval RWF_DSYNC).

Teljesítmény megjegyzések

Nem mértem fel alaposan az általam vizsgált különféle mechanizmusok teljesítményét. A munkájuk sebességében tapasztalt különbségek nagyon kicsik. Ez azt jelenti, hogy tévedhetek, és más körülmények között ugyanaz a dolog eltérő eredményeket mutathat. Először arról fogok beszélni, hogy mi befolyásolja jobban a teljesítményt, majd arról, hogy mi befolyásolja kevésbé.

  1. A fájladatok felülírása gyorsabb, mint az adatok fájlhoz fűzése (a teljesítménynövekedés 2-100%). Az adatok fájlhoz csatolása további módosításokat igényel a fájl metaadataiban, még a rendszerhívás után is fallocate(), de ennek a hatásnak a mértéke változhat. Azt javaslom, hogy a legjobb teljesítmény érdekében hívjon fallocate() a szükséges hely előzetes kiosztásához. Ezután ezt a helyet kifejezetten nullákkal kell kitölteni, és meg kell hívni fsync(). Ez azt eredményezi, hogy a fájlrendszerben a megfelelő blokkok "allokált"-ként lesznek megjelölve "kiosztatlan" helyett. Ez kismértékű (körülbelül 2%-os) teljesítményjavulást eredményez. Ezenkívül egyes lemezek lassabb első blokk hozzáférési művelettel rendelkeznek, mint mások. Ez azt jelenti, hogy a hely nullákkal való kitöltése jelentős (kb. 100%-os) teljesítményjavuláshoz vezethet. Ez különösen a lemezeknél fordulhat elő. AWS EBS (ez nem hivatalos adat, nem tudtam megerősíteni). Ugyanez vonatkozik a tárolásra is. GCP állandó lemez (és ez már hivatalos információ, tesztekkel megerősítve). Más szakértők is ezt tették megfigyeléskülönböző lemezekhez kapcsolódik.
  2. Minél kevesebb rendszerhívás, annál nagyobb a teljesítmény (a nyereség körülbelül 5%). Úgy néz ki, mint egy hívás open() zászlóval O_DSYNC vagy hívjon pwritev2() zászlóval RWF_SYNC gyorsabb hívás fdatasync(). Gyanítom, hogy itt az a lényeg, hogy ennél a megközelítésnél az játszik szerepet, hogy ugyanazon feladat megoldásához kevesebb rendszerhívást kell végrehajtani (két helyett egy hívást). De a teljesítménykülönbség nagyon kicsi, így könnyen figyelmen kívül hagyhatja, és olyasvalamit használhat az alkalmazásban, amely nem vezet az alkalmazás logikájának bonyolításához.

Ha érdekel a fenntartható adattárolás témaköre, íme néhány hasznos anyag:

  • I/O hozzáférési módszerek — a bemeneti/kimeneti mechanizmusok alapjainak áttekintése.
  • Annak biztosítása, hogy az adatok eljussanak a lemezre - egy történet arról, hogy mi történik az adatokkal az alkalmazásból a lemezre vezető úton.
  • Mikor kell fszinkronizálni a tartalmazó könyvtárat? - a válasz arra a kérdésre, hogy mikor kell jelentkezni fsync() címtárak számára. Dióhéjban kiderül, hogy ezt új fájl létrehozásakor kell megtenni, és ennek az ajánlásnak az az oka, hogy Linuxban sok hivatkozás lehet ugyanarra a fájlra.
  • SQL Server Linuxon: FUA Internals - itt van egy leírás arról, hogyan valósult meg az állandó adattárolás az SQL Serverben Linux platformon. Itt van néhány érdekes összehasonlítás a Windows és a Linux rendszerhívások között. Szinte biztos vagyok benne, hogy ennek az anyagnak köszönhetően tudtam meg az XFS FUA optimalizálását.

Elveszített már olyan adatokat, amelyekről azt hitte, hogy biztonságosan vannak a lemezen tárolva?

Tartós adattárolás és Linux fájl API-k

Tartós adattárolás és Linux fájl API-k

Forrás: will.com