Miközben az adatok felhőbeli tárolásának megbízhatóságát kutattam, úgy döntöttem, hogy tesztelem magam, és megbizonyosodom arról, hogy értem az alapvető dolgokat. Én hogy megérthessem, milyen garanciák vonatkoznak az adatok megbízható tárolására (vagyis a garanciák arra, hogy az adatok elérhetők lesznek a rendszerhibák után), amelyeket az NVMe meghajtók biztosítanak. A következő főbb következtetéseket vontam le: az adatok sérültnek tekinthetők attól a pillanattól kezdve, amikor a rendszer körüli parancsot kiadják, egészen addig, amíg a tárolóra való írás befejeződik. Azonban a legtöbb adatíró programban a rendszerhívások teljesen normálisan használhatók.
Ebben az anyagban azokat a megbízható tárolási mechanizmusokat kutatom, amelyeket a Linux fájl API-k biztosítanak. Úgy tűnik, hogy itt minden egyszerű kellene legyen: a program kiad egy parancsot, write(), és amint a parancs befejeződik, az adatok megbízhatóan tárolva lesznek a lemezen. De write() csak az alkalmazás adatait másolja a memóriában található kernel gyorsítótárába. Ahhoz, hogy a rendszert arra kényszerítsük, hogy az adatokat a lemezre írja, néhány további mechanizmust kell használni.
Összességében ez az anyag egy olyan jegyzetek gyűjteménye, amely a témával kapcsolatos tudásomra épül. Ha nagyon röviden szeretném összefoglalni a legfontosabbat, akkor azt mondhatom, hogy az adatok megbízható tárolásának megszervezéséhez a következő parancsot kell használni: fdatasync() vagy fájlokat O_DSYNCflaggal kell megnyitni. Ha részletesebben szeretne megtudni arról, hogy mi történik az adatokkal a programkódból a lemezre való átadásig, nézze meg a a cikkre.
A write() függvény használatának sajátosságai
A rendszerhívás write() a következő szabványban van definiálva: mint az adatok próbálkozása a fájl leíróba való írására. A művelet sikeres befejezése után write() az olvasási műveleteknek pontosan azokat a byte-okat kell visszaadniuk, amelyeket korábban írtak, még akkor is, ha más folyamatok vagy szálak hozzáférnek az adatokhoz ( a POSIX szabvány megfelelő része). , a szálak és a hagyományos fájlkezelési műveletek interakciójára vonatkozó részben van egy megjegyzés, amely hangsúlyozza, hogy ha mindkét szál ezeket a funkciókat hívja meg, akkor minden egyes hívásnak vagy az összes jelzett következményt kell látnia, amelyet egy másik hívás végrehajtása okoz, vagy egyáltalán semmilyen következményt. Ez lehetővé teszi arra a következtetésre jutást, hogy minden bemeneti/kimeneti fájlműveletnek meg kell tartania a blokkolás erőforrást, amellyel dolgozik.
Jelenti ez azt, hogy a művelet write() atomikus? Technikai szempontból — igen. Az adatolvasási műveleteknek vagy mindent, vagy semmit kell visszaadniuk a write()segítségével írt adatokból. De a művelet write(), a szabványnak megfelelően, nem köteles úgy befejeződni, hogy mindent, amit írtak neki, ténylegesen rögzít. Engedélyezett számára, hogy csak egy részét a datának írja be. Például lehet két szálunk, amelyek mindegyike 1024 bájtot csatol a fájlhoz, amely egy és ugyanazon fájlleíróval van leírva. A szabvány szempontjából a megengedett eredmény az, amikor mindegyik írási művelet csak egy bájtot képes csatolni a fájlhoz. Ezek a műveletek atomikusak maradnak, de miután befejeződnek, az általuk a fájlba írt adatok keveredve lesznek. nagyon érdekes vita folyik e témában a Stack Overflow-on.
Az fsync() és fdatasync() függvények
A legkönnyebb módja az adatok lemezre írásának funkció hívásának, . Ez a funkció arra kéri az operációs rendszert, hogy a módosított blokkokat a gyorsítótárból átvigye a lemezre. Ide tartozik az összes fájlmetainformáció (hozzáférési idő, fájl módosítási ideje stb.). Úgy vélem, hogy ezekre a metainformációkra ritkán van szükség, ezért ha tudod, hogy számodra nem fontosak, használhatod a fdatasync(). A hogy fdatasync() azt mondja, hogy ennek a funkciónak a működése során a lemezre írásra kerül a metainformációk olyan mennyisége, amely "szükséges a következő adatolvasási műveletek megfelelő végrehajtásához". És ez pontosan az, ami a legtöbb alkalmazást érdekli.
Az egyik probléma, amely itt felmerülhet, hogy ezek a mechanizmusok nem garantálják, hogy a fájl felfedezhető lesz egy esetleges hiba után. Különösen, amikor új fájlt hoznak létre, szükséges a fsync() a könyvtár számára, amely tartalmazza azt. Különben előfordulhat, hogy a hiba után ez a fájl nem létezik. Ennek oka az, hogy UNIX környezetben a kemény linkek alkalmazása miatt a fájl több könyvtárban is létezhet. Ezért a híváskor fsync() nincs mód a fájl számára, hogy megtudja, hogy melyik könyvtár adatait is kell a lemezre írni ( erről bővebben olvashat). Úgy tűnik, hogy az ext4 fájlrendszer képes alkalmazni fsync() a megfelelő fájlokat tartalmazó könyvtárakra, de más fájlrendszerek esetében ez nem biztos, hogy így van.
Ez a mechanizmus különböző módokon valósulhat meg a különböző fájlrendszerekben. Használatba vettem a annak megértésére, hogy milyen lemezműveleteket használnak az ext4 és XFS fájlrendszerek. Mindkettő normál lemezírási parancsokat ad ki a fájlok tartalmára és a fájlrendszer naplójára is, kiüríti a gyorsítótárat és befejezi a működést, végrehajtva az FUA (Force Unit Access, az adatok közvetlen írása a lemezre a gyorsítótár megkerülésével) írást a naplóba. Valószínűleg így járnak el annak érdekében, hogy megerősítsék a művelet végrehajtásának tényét. Azokon a lemezeken, amelyek nem támogatják a FUA-t, ez két gyorsítótárazást okoz. Kísérleteim azt mutatták, hogy fdatasync() kissé gyorsabb fsync(). Segédprogram blktrace-t arra utal, hogy fdatasync() általában kevesebb adatot ír a lemezre (az ext4-ben fsync() 20 KiB-ot ír, míg fdatasync() 16 KiB-ot). Ezenkívül kiderítettem, hogy az XFS egy kicsit gyorsabb, mint az ext4. Ezzel kapcsolatban blktrace-t tudtam meg, hogy fdatasync() kevesebb adatot ír a lemezre (4 KiB az XFS-ben).
Kétes helyzetek, amelyek az fsync() használatával merülnek fel
három kétes helyzetet tudok felidézni, amelyek kapcsolódnak fsync(), amelyekkel a gyakorlatban találkoztam.
Az első eset 2008-ban történt. Akkor a Firefox 3 felülete 'beragadt', amikor nagy mennyiségű fájl írása zajlott a lemezre. A probléma az volt, hogy a felület állapotának tárolására SQLite adatbázist használtak. Minden egyes változás után, ami a felületen történt, a fsync()függvény hívása történt, amely jó garanciát nyújtott az adatok tartós tárolására. Az akkor használt ext3 fájlrendszerben a függvény fsync() minden "piszkos" oldalt a rendszeren a lemezre írták, nem csak azokat, amelyek a megfelelő fájlhoz tartoztak. Ez azt jelentette, hogy a Firefoxban egy gombra kattintva megabytes adat íródhatott a mágneses lemezre, ami sok másodpercet vehetett igénybe. A probléma megoldása, ahogyan azt megértettem, abban rejlett, hogy az adatbázis-kezelést aszinkron háttérfeladatokká kellett áthelyezni. Ez azt jelenti, hogy a Firefoxban korábban szigorúbb követelmények voltak a adatmegőrzés megbízhatóságával kapcsolatban, mint amennyire valójában szükség volt, és az ext3 fájlrendszer jellemzői csak súlyosbították ezt a problémát.
A második ellentmondás 2009-ben történt. Akkor, a rendszer leállása után, az új ext4 fájlrendszer felhasználói szembesültek azzal, hogy sok, nemrégiben létrehozott fájl hossza nulla, míg a régi ext3 fájlrendszernél ilyesmi nem fordult elő. Az előző bekezdésben azt mondtam, hogy az ext3 túl sok adatot írt a lemezre, ami jelentősen lelassította a működést fsync(). Ahhoz, hogy javuljon a helyzet, az ext4 csak azokat a "piszkos" oldalakat írja a lemezre, amelyek a konkrét fájlhoz tartoznak. Más fájlok adatai sokkal hosszabb ideig maradnak a memóriában, mint az ext3 használata esetén. Ezt a teljesítményjavítás érdekében tették (alapértelmezés szerint az adatok 30 másodpercig maradnak ilyen állapotban, ezt a ; segítségével állíthatjuk be). Ez azt jelenti, hogy nagy mennyiségű adat véglegesen elveszhet a rendszer leállása után. A probléma megoldása az, hogy fsync() alkalmazásokban, amelyeknek biztosítaniuk kell az adatok megbízható tárolását, és maximumra kell csökkenteniük azok veszélyeit a leállások következményeivel szemben. A fsync() fokozottan hatékonyabban működik ext4 használata esetén, mint ext3 esetén. Ennek a megközelítésnek a hátránya, hogy a használata, mint korábban, lassít bizonyos műveletek végrehajtását, például a programok telepítését. A részletekért lásd és .
A harmadik probléma, amely fsync(), 2018-ban merült fel. Akkor a PostgreSQL projekt keretében kiderült, hogy ha a fsync() funkció hibába ütközik, a "piszkos" oldalakat „tiszta”-ként jelöli meg. Ennek eredményeként a következő hívások fsync() Nem csinálnak semmit az ilyen oldalakkal. Ennek következtében a módosított oldalak a memóriában maradnak, és soha nem kerülnek írásra a lemezre. Ez valódi katasztrófa, mivel az alkalmazás azt fogja hinni, hogy bizonyos adatok íródtak a lemezre, pedig valójában nem. Az ilyen hibák fsync() ritkán fordulnak elő, ilyenkor az alkalmazás szinte semmit nem tud tenni a probléma kezeléséért. Manapság, amikor ez megtörténik, a PostgreSQL és más alkalmazások váratlanul leállnak. , a "Can Applications Recover from fsync Failures?" című anyagban ezt a problémát részletesen vizsgálják. Jelenleg a legjobb megoldás erre a problémára a Direct I/O használata az O_SYNC vagy az O_DSYNCflaggel. Ezen megközelítéssel a rendszer jelenteni fogja az adatírási műveletek során fellépő hibákat, azonban ez a megoldás azt igényli, hogy az alkalmazás önállóan kezelje a pufferelést. A részletekért olvasson el és .
Fájlok megnyitása O_SYNC és O_DSYNC zászlókkal
Térjünk vissza a Linux-mechanizmusok megvitatásához, amelyek biztosítják az adatok megbízható tárolását. Pontosabban, a O_SYNC vagy a O_DSYNC használatáról van szó fájlok megnyitásakor a rendszerhívás során. . Ezen megközelítéssel minden adatírási művelet úgy valósul meg, mintha minden parancs után write() a rendszer megkapná a megfelelő parancsokat fsync() és fdatasync(). A ezt "Synchronized I/O File Integrity Completion" és "Data Integrity Completion" néven ismerik. Ennek a megközelítésnek a legnagyobb előnye, hogy az adatok integritásának biztosításához csupán egy rendszerhívást kell végrehajtani, nem kettőt (például - write() és fdatasync()). E megközelítés legfőbb hátránya, hogy minden írási művelet, amely a megfelelő fájlleíróval használatos, szinkronizálva lesz, ami korlátozhatja az alkalmazás kódjának struktúrázására vonatkozó lehetőségeket.
A Direct I/O O_DIRECT zászlóval való használata
A rendszerhívás open() támogatja a O_DIRECT, amelyet arra terveztek, hogy megkerülve az operációs rendszer gyorsítótárát, közvetlenül a lemezzel kommunikálva hajtsa végre a be- és kimeneti műveleteket. Ez gyakran azt jelenti, hogy a program által kibocsátott írási parancsok közvetlenül kerülnek átkonvertálásra a lemez műveleteire vonatkozó parancsokká. Az általános esetben azonban ez a mechanizmus nem helyettesíti a fsync() vagy fdatasync(). A probléma az, hogy magá a lemez megfelelő adatrögzítési parancsok. És ami még rosszabb, hogy bizonyos speciális esetekben, a bemeneti/kimeneti műveletek, amelyek a flag használatával történnek, O_DIRECT, hagyományos puffertöltési műveletekbe. E probléma legkönnyebben azzal oldható meg, ha fájlok megnyitásához további flaget is használunk, O_DSYNC, ami azt jelenti, hogy minden rögzítési művelet után egy hívás következik. fdatasync().
Kiderült, hogy az XFS fájlrendszerhez nemrégiben egy "gyors út" került hozzáadásra, O_DIRECT|O_DSYNC-adatok rögzítéséhez. Ha egy blokkot felülírnak a O_DIRECT|O_DSYNC, akkor az XFS a cache törlése helyett FUA-rögzítési parancsot hajt végre, amennyiben az eszköz ezt támogatja. Ezt a Linux 5.4 / Ubuntu 20.04 rendszer alatt az blktrace-t segítségével tapasztaltam. Ez a megközelítés hatékonyabbnak kell lennie, mivel a minimális adatmennyiséget írja a lemezre, és egyetlen műveletet végez, nem pedig kettőt (rögzítést és a cache törlését). Találtam egy hivatkozást egy 2018-as kernelre, amelyben ezt a mechanizmust implementálták. Ott van egy beszélgetés a más fájlrendszerekben történő alkalmazásáról, de amennyire tudom, az XFS egyelőre az egyetlen fájlrendszer, amely ezt támogatja.
sync_file_range() funkció
A Linuxban van egy rendszerhívás , amely lehetővé teszi, hogy csak a fájl egy részét írja a lemezre, nem pedig az egész fájlt. Ez a hívás aszinkron adathalászást indít el, és nem várja meg a befejezését. De a sync_file_range() segítségével azt mondják, hogy ezen parancs "nagyon veszélyes". nem ajánlottásra. A részletek és veszélyek sync_file_range() nagyon jól le vannak írva a anyagon. Különösen úgy tűnik, hogy ez a hívás a RocksDB-t használja az adatok "piszkos" lemezre történő törlésének irányítására. De a megbízható tárolás biztosítása érdekében itt is, fdatasync(). A A RocksDB-vel kapcsolatban vannak érdekes megjegyzések erről a témáról. Például úgy tűnik, hogy a hívás sync_file_range() ZFS használata esetén nem vezet adatfehasználáshoz a lemezen. A tapasztalat azt mondja, hogy a ritkán használt kód valószínűleg hibákat tartalmaz. Ezért azt tanácsolnám, hogy ne használja ezt a rendszerhívást kivételes szükségletek nélkül.
A rendszerhívások, amelyek segítenek a megbízható adatmegőrzés biztosításában
Arra jutottam, hogy az adatok megbízható tárolását biztosító bevitel/kivitel műveletek végrehajtására három módszert lehet alkalmazni. Mindegyikhez szükséges a fájl létrehozásának könyvtárára vonatkozó függvény meghívása. fsync() Ezek a megközelítések:
- Funkcióhívás
fdatasync()vagyfsync()a függvény utánwrite()(jobb használnifdatasync()). - A fájlleíróval való munka, amelyet a
O_DSYNCvagyO_SYNC(jobb — aO_DSYNC). - parancs használata
pwritev2()zászlóvalRWF_DSYNCvagyRWF_SYNC(előnyösebb — aRWF_DSYNC).
Teljesítmény megjegyzések
Nem végeztem alapos teljesítményméréseket a különböző általam megvizsgált mechanizmusokról. Az általam észlelt sebességbeli eltérések meglehetősen kicsiek. Ez azt jelenti, hogy tévedhetek, és hogy más körülmények között ugyanaz más eredményeket hozhat. Először arról fogok beszélni, hogy mi befolyásolja legjobban a teljesítményt, majd arról, hogy mi befolyásolja kevésbé.
- A fájl adatainak felülírása gyorsabb, mint az adatok fájlhoz való hozzáfűzése (a teljesítmény nyeresége 2-100% között mozoghat). Az adatok fájlhoz való hozzáfűzése 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. A legjobb teljesítmény biztosítása érdekében javaslom, hogy hívd megfallocate()a szükséges hely előzetes lefoglalására. Ezt a helyet kifejezetten nullákkal kell feltölteni, majd meghívnifsync(). Ezzel a megfelelő blokkok a fájlrendszerben 'lefoglalt' státuszt kapnak, és nem 'nem lefoglalt' státuszt. Ez egy kis (~2%) teljesítménynövekedést eredményez. Ezen kívül egyes tárolók esetében az első blokkhoz való hozzáférés lassabb lehet, mint másoké. Ez azt jelenti, hogy a hely nullákkal való feltöltése jelentős (~100%) teljesítménynövekedést okozhat. Különösen ez előfordulhat az (ez nem hivatalos adat, amit nem tudtam megerősíteni). Ugyanez vonatkozik a (ez már hivatalos információ, amelyet tesztelések igazoltak). Más szakértők hasonló , amelyek különböző tárolókra vonatkoznak. - Minél kevesebb a rendszerhívás, annál magasabb a teljesítmény (a nyereség körülbelül 5% lehet). Úgy tűnik, hogy a
open()zászlóvalO_DSYNCvagy apwritev2()zászlóvalRWF_SYNChívás gyorsabb, mint a hívás.fdatasync()Gyanítom, hogy a dolog ott rejlik, hogy e megközelítés esetén a szerepet az játssza, hogy ugyanannak a feladatnak a megoldásához kevesebb rendszerhívást kell végrehajtani (egy hívás a kettő helyett). De a teljesítménykülönbség nagyon kicsi, ezért nyugodtan figyelmen kívül hagyhatja, és használhatja az alkalmazásban azt, ami nem bonyolítja a logikáját.
Ha érdekli az adatvédelem és a fenntartható tárolás témája, itt van néhány hasznos anyag:
- — az I/O mechanizmusainak alapvető áttekintése.
- — azt meséli el, hogy mi történik az adatokkal az alkalmazástól a lemezig.
- — válasz arra, hogy mikor szükséges alkalmazni
fsync()a könyvtárak esetében. Ha két szóban kellene erről beszélni, akkor azt mondanám, hogy ezt új fájl létrehozásakor kell csinálni, és az ajánlás okát az adja, hogy Linux alatt sok hivatkozás lehet ugyanarra a fájlra. - — itt ismertetik, hogy az adatok fenntartható tárolása hogyan valósul meg az SQL Server Linux platformján. Itt néhány érdekes összehasonlítás található a Windows és Linux rendszerhívásai között. Szinte biztos vagyok benne, hogy éppen ennek a anyagnak köszönhetem, hogy megtudtam a FUA optimalizálását az XFS-nél.
Veszítettek már el olyan adatokat, amelyeket biztosan megbízhatóan tároltak a lemezen?
Forrás: habr.com
