Vastupidav andmesalvestus ja Linuxi faili API-d

Pilvesüsteemides andmete salvestamise jätkusuutlikkust uurides otsustasin end proovile panna, et põhiasjadest aru saada. I alustasin NVMe spetsifikatsiooni lugemisest selleks, et mõista, millised garantiid jätkusuutliku andmesalvestuse osas (st tagatised, et andmed on pärast süsteemitõrget saadaval) annavad meile NMVe kettad. Tegin järgmised peamised järeldused: andmeid tuleb lugeda kahjustatud andmete kirjutamise käsu andmisest kuni nende kirjutamise hetkeni andmekandjale. Kuid enamik programme kasutab andmete salvestamiseks üsna hea meelega süsteemikõnesid.

Selles postituses uurin Linuxi faili API-de pakutavaid püsivaid salvestusmehhanisme. Tundub, et siin peaks kõik olema lihtne: programm kutsub käsu välja write()ja pärast selle käsu täitmist salvestatakse andmed turvaliselt kettale. Aga write() kopeerib rakenduse andmed ainult RAM-is asuvasse kerneli vahemällu. Selleks, et sundida süsteemi andmeid kettale kirjutama, peate kasutama mõningaid lisamehhanisme.

Vastupidav andmesalvestus ja Linuxi faili API-d

Üldiselt on see materjal märkmete kogumik, mis on seotud sellega, mida olen õppinud mind huvitaval teemal. Kui rääkida väga lühidalt kõige olulisemast, siis selgub, et säästva andmesalvestuse korraldamiseks tuleb kasutada käsku fdatasync() või avage failid lipuga O_DSYNC. Kui soovite lisateavet selle kohta, mis juhtub andmetega teel koodist kettale, vaadake seda see artiklit.

Funktsiooni write() kasutamise omadused

Süsteemikõne write() standardis määratletud IEEE POSIX katsena kirjutada andmeid failideskriptorisse. Pärast edukat lõpetamist write() Andmete lugemise toimingud peavad tagastama täpselt varem kirjutatud baidid, tehes seda isegi siis, kui andmetele pääseb juurde teistest protsessidest või lõimedest (siin POSIX standardi asjakohane jaotis). see on, jaotises, kuidas lõimed tavapäraste failitoimingutega suhtlevad, on märkus, mis ütleb, et kui kaks lõime kutsuvad neid funktsioone, peab iga kõne nägema kas kõiki teise kõne määratud tagajärgi või mitte ühtegi. ei tagajärjed. See viib järeldusele, et kõik faili sisend-/väljundtoimingud peavad olema lukustatud ressursil, millega nad töötavad.

Kas see tähendab, et operatsioon write() kas see on aatomiline? Tehnilisest vaatenurgast jah. Andmete lugemise toimingud peavad tagastama kas kõik või mitte midagi, millega kirjutati write(). Aga operatsioon write(), vastavalt standardile, ei pea tingimata lõppema sellega, et kirjutatakse üles kõik, mida tal paluti üles kirjutada. Tal on lubatud kirjutada ainult osa andmetest. Näiteks võib meil olla kaks lõime, millest igaüks lisab 1024 baiti sama failikirjeldusega kirjeldatud faili. Standardi seisukohast on vastuvõetav tulemus, kui iga kirjutusoperatsioon saab faili lisada ainult ühe baidi. Need toimingud jäävad tuumaks, kuid pärast nende lõpetamist lähevad faili kirjutatud andmed segamini. siin on väga huvitav arutelu sellel teemal Stack Overflow's.

fsync() ja fdatasync() funktsioonid

Lihtsaim viis andmete kettale loputamiseks on funktsiooni kutsumine fsync(). See funktsioon palub operatsioonisüsteemil kanda kõik muudetud plokid vahemälust kettale. See hõlmab kõiki faili metaandmeid (juurdepääsuaeg, faili muutmise aeg ja nii edasi). Usun, et neid metaandmeid läheb harva vaja, nii et kui teate, et need pole teile olulised, võite kasutada funktsiooni fdatasync(). Sisse abi edasi fdatasync() öeldakse, et selle funktsiooni töötamise ajal salvestatakse kettale selline hulk metaandmeid, mis on "vajalikud järgmiste andmete lugemise toimingute korrektseks täitmiseks". Ja see on just see, millest enamik rakendusi hoolib.

Üks probleem, mis siin võib tekkida, on see, et need mehhanismid ei garanteeri faili avastamist pärast võimalikku tõrget. Eelkõige tuleb uue faili loomisel helistada fsync() seda sisaldava kataloogi jaoks. Vastasel juhul võib pärast ebaõnnestumist selguda, et seda faili pole olemas. Selle põhjuseks on asjaolu, et UNIXis võib fail kõvade linkide kasutamise tõttu eksisteerida mitmes kataloogis. Seetõttu helistades fsync() fail ei saa kuidagi teada, millise kataloogi andmed tuleks samuti kettale loputada ( siin Selle kohta saate rohkem lugeda). Näib, et ext4 failisüsteem on selleks võimeline automaatselt kohaldada fsync() vastavaid faile sisaldavatesse kataloogidesse, kuid teiste failisüsteemide puhul ei pruugi see nii olla.

Seda mehhanismi võib erinevates failisüsteemides erinevalt rakendada. ma kasutasin blktrace et saada teada, milliseid kettaoperatsioone ext4- ja XFS-failisüsteemides kasutatakse. Mõlemad väljastavad regulaarsed kettale kirjutamiskäsud nii faili sisu kui ka failisüsteemi päeviku jaoks, tühjendavad vahemälu ja väljuvad FUA (Force Unit Access, andmete kirjutamine otse kettale, vahemälust möödahiili) abil päevikusse. Tõenäoliselt teevad nad seda tehingu toimumise kinnitamiseks. Draividel, mis ei toeta FUA-d, põhjustab see kaks vahemälu loputamist. Minu katsed näitasid seda fdatasync() natuke kiiremini fsync(). Kasulikkus blktrace viitab sellele fdatasync() kirjutab tavaliselt kettale vähem andmeid (ext4 fsync() kirjutab 20 KiB, ja fdatasync() - 16 KiB). Samuti sain teada, et XFS on veidi kiirem kui ext4. Ja siin abiga blktrace õnnestus see teada saada fdatasync() loputab kettale vähem andmeid (4 KiB XFS-is).

Mitmetähenduslikud olukorrad, mis tekivad fsync() kasutamisel

Ma suudan sellega seoses välja mõelda kolm mitmetähenduslikku olukorda fsync()millega praktikas kokku puutusin.

Esimene selline juhtum leidis aset 2008. aastal. Seejärel hangus Firefox 3 liides, kui kettale kirjutati suur hulk faile. Probleem seisnes selles, et liidese juurutamine kasutas SQLite'i andmebaasi, et salvestada teavet selle oleku kohta. Pärast iga liideses toimunud muudatust kutsuti funktsioon välja fsync(), mis andis head garantiid stabiilsele andmesalvestusele. Seejärel kasutatud ext3 failisüsteemis funktsioon fsync() pakkis kettale kõik süsteemis olevad "määrdunud" lehed, mitte ainult need, mis olid seotud vastava failiga. See tähendas, et Firefoxis nupul klõpsamine võib käivitada magnetkettale kirjutamise megabaitidega andmeid, mis võib võtta mitu sekundit. Probleemi lahendus, niipalju kui ma aru saan Käesoleva materjaliks oli andmekoguga töö ülekandmine asünkroonsetele taustaülesannetele. See tähendab, et Firefox rakendas varem rangemaid salvestusnõudeid, kui tegelikult vaja oli, ja failisüsteemi ext3 funktsioonid ainult süvendasid seda probleemi.

Teine probleem tekkis 2009. aastal. Seejärel, pärast süsteemi krahhi, seisid uue ext4 failisüsteemi kasutajad silmitsi tõsiasjaga, et paljudel äsja loodud failidel oli null pikkus, kuid vanema ext3 failisüsteemiga seda ei juhtunud. Eelmises lõigus rääkisin, kuidas ext3 loputas kettale liiga palju andmeid, mis aeglustas asja oluliselt. fsync(). Olukorra parandamiseks loputatakse ext4-s kettale ainult need määrdunud lehed, mis on konkreetse faili jaoks olulised. Ja andmed muudest failidest jäävad mällu palju pikemaks ajaks kui ext3 puhul. Seda tehti jõudluse parandamiseks (vaikimisi jäävad andmed sellesse olekusse 30 sekundiks, saate seda konfigureerida kasutades dirty_expire_centisecs; siin Selle kohta leiate lisamaterjale). See tähendab, et pärast riket võib pöördumatult kaduda suur hulk andmeid. Selle probleemi lahendus on kasutada fsync() rakendustes, mis peavad tagama stabiilse andmesalvestuse ja kaitsma neid nii palju kui võimalik rikete tagajärgede eest. Funktsioon fsync() töötab ext4 kasutamisel palju tõhusamalt kui ext3 kasutamisel. Selle lähenemisviisi puuduseks on see, et selle kasutamine, nagu varem, aeglustab teatud toimingute, näiteks programmide installimise, täitmist. Vaadake selle kohta üksikasju siin и siin.

Kolmas probleem seoses fsync(), sai alguse 2018. aastal. Seejärel leiti PostgreSQL projekti raames, et kui funktsioon fsync() ilmneb tõrge, märgib see "määrdunud" leheküljed kui "puhtad". Selle tulemusena järgmised kõned fsync() Nad ei tee selliste lehtedega midagi. Seetõttu salvestatakse muudetud lehed mällu ja neid ei kirjutata kunagi kettale. See on tõeline katastroof, kuna rakendus arvab, et osa andmeid kirjutatakse kettale, kuid tegelikult see nii ei ole. Sellised ebaõnnestumised fsync() on haruldased, ei saa sellistes olukordades rakendamine probleemiga võitlemiseks peaaegu midagi teha. Nendel päevadel, kui see juhtub, jooksevad PostgreSQL ja muud rakendused kokku. see on, materjalis "Kas rakendused saavad fsynci tõrgetest taastuda?", on seda probleemi üksikasjalikult uuritud. Praegu on selle probleemi parim lahendus kasutada lipuga otsesisendit/väljundit O_SYNC või lipuga O_DSYNC. Selle lähenemisviisi korral teatab süsteem vigadest, mis võivad ilmneda konkreetsete kirjutamistoimingute ajal, kuid see lähenemisviis nõuab, et rakendus haldaks puhvreid ise. Loe selle kohta lähemalt siin и siin.

Failide avamine lippude O_SYNC ja O_DSYNC abil

Tuleme tagasi arutelu juurde Linuxi mehhanismide üle, mis pakuvad stabiilset andmesalvestust. Nimelt räägime lipu kasutamisest O_SYNC või lipp O_DSYNC failide avamisel süsteemikõne abil avatud (). Selle lähenemisviisi korral tehakse iga andmete kirjutamise toiming justkui iga käsu järel write() süsteemile antakse vastavalt käsud fsync() и fdatasync(). Sisse POSIXi spetsifikatsioonid seda nimetatakse "Sünkroniseeritud I/O-faili terviklikkuse lõpuleviimiseks" ja "Andmete terviklikkuse lõpetamiseks". Selle lähenemisviisi peamine eelis on see, et andmete terviklikkuse tagamiseks peate tegema ainult ühe süsteemikõne, mitte kahe (näiteks - write() и fdatasync()). Selle lähenemisviisi peamiseks puuduseks on see, et kõik vastavat failideskriptorit kasutavad kirjutamised sünkroonitakse, mis võib piirata rakenduse koodi struktureerimise võimalust.

Otsese I/O kasutamine lipuga O_DIRECT

Süsteemikõne open() toetab lippu O_DIRECT, mis on mõeldud operatsioonisüsteemi vahemälust möödahiilimiseks, et sooritada I/O toiminguid, suheldes otse kettaga. See tähendab paljudel juhtudel, et programmi antud kirjutamiskäsud tõlgitakse otse kettaga töötamiseks mõeldud käskudeks. Kuid üldiselt ei asenda see mehhanism funktsioone fsync() või fdatasync(). Fakt on see, et ketas ise saab edasilükkamine või vahemälu vastavad andmete kirjutamise käsud. Ja mis veelgi hullemaks teeb, mõnel erijuhtudel lipu kasutamisel sooritatud I/O toimingud O_DIRECT, saade traditsioonilisteks puhveroperatsioonideks. Lihtsaim viis selle probleemi lahendamiseks on kasutada failide avamiseks lippu O_DSYNC, mis tähendab, et igale kirjutamistoimingule järgneb kõne fdatasync().

Selgus, et XFS-failisüsteem oli hiljuti lisanud "kiirtee". O_DIRECT|O_DSYNC- andmete salvestamine. Kui plokk kirjutatakse ümber kasutades O_DIRECT|O_DSYNC, siis XFS täidab vahemälu tühjendamise asemel FUA kirjutamiskäsu, kui seade seda toetab. Kontrollisin seda utiliidi abil blktrace Linux 5.4/Ubuntu 20.04 süsteemis. See lähenemisviis peaks olema tõhusam, kuna selle kasutamisel kirjutatakse kettale minimaalne kogus andmeid ja kasutatakse ühte toimingut, mitte kahte (vahemälu kirjutamine ja tühjendamine). Leidsin lingi plaaster 2018 kernel, mis seda mehhanismi rakendab. Seal on arutelu selle optimeerimise üle teistele failisüsteemidele, kuid minu teada on XFS seni ainus failisüsteem, mis seda toetab.

funktsioon sync_file_range().

Linuxil on süsteemikutse sync_file_range(), mis võimaldab teil kettale loputada ainult osa failist, mitte kogu faili. See kõne algatab asünkroonse andmete loputamise ega oota selle lõpuleviimist. Aga tunnistusel sync_file_range() meeskond on väidetavalt "väga ohtlik". Seda ei soovitata kasutada. Omadused ja ohud sync_file_range() väga hästi kirjeldatud see materjalist. Täpsemalt näib, et see kõne kasutab RocksDB-d, et juhtida, millal kernel määrdunud andmed kettale loputab. Kuid samal ajal kasutatakse seda ka stabiilse andmesalvestuse tagamiseks fdatasync(). Sisse kood RocksDB-l on sellel teemal huvitavaid kommentaare. Näiteks näib, et kõne sync_file_range() ZFS-i kasutamisel ei loputa andmeid kettale. Kogemused näitavad, et harva kasutatav kood sisaldab tõenäoliselt vigu. Seetõttu ei soovita ma seda süsteemikutset kasutada, kui see pole tingimata vajalik.

Süsteemikõned, mis aitavad tagada andmete püsivuse

Olen jõudnud järeldusele, et andmete püsivust tagavate I/O toimingute tegemiseks saab kasutada kolme lähenemist. Kõik need nõuavad funktsioonikutset fsync() kataloogi jaoks, kuhu fail loodi. Need on lähenemisviisid:

  1. Funktsioonikutse fdatasync() või fsync() pärast funktsiooni write() (parem on kasutada fdatasync()).
  2. Lipuga avatud failideskriptoriga töötamine O_DSYNC või O_SYNC (parem - lipuga O_DSYNC).
  3. Kasutades käsku pwritev2() lipuga RWF_DSYNC või RWF_SYNC (soovitavalt lipuga RWF_DSYNC).

Tulemuslikkuse märkmed

Ma ei ole hoolikalt mõõtnud erinevate uuritud mehhanismide toimivust. Erinevused, mida nende töö kiiruses märkasin, on väga väikesed. See tähendab, et ma võin eksida ja erinevatel tingimustel võib sama asi anda erinevaid tulemusi. Esiteks räägin sellest, mis mõjutab jõudlust rohkem ja seejärel, mis mõjutab jõudlust vähem.

  1. Failiandmete ülekirjutamine on kiirem kui andmete failile lisamine (jõudluskasu võib olla 2–100%). Andmete lisamine faili nõuab täiendavaid muudatusi faili metaandmetes isegi pärast süsteemikutset fallocate(), kuid selle mõju ulatus võib varieeruda. Soovitan parima tulemuse saavutamiseks helistada fallocate() vajaliku ruumi eeljaotamiseks. Siis tuleb see ruum selgesõnaliselt nullidega täita ja kutsuda fsync(). See tagab, et failisüsteemi vastavad plokid on märgistatud kui "eraldatud", mitte "eraldamata". See annab väikese (umbes 2%) jõudluse paranemise. Lisaks võib mõne ketta esimene juurdepääs plokile olla aeglasem kui teistel. See tähendab, et ruumi nullidega täitmine võib jõudlust oluliselt (umbes 100%) parandada. Eelkõige võib see juhtuda ketaste puhul AWS EBS (tegemist on mitteametlike andmetega, ma ei saanud seda kinnitada). Sama kehtib ka ladustamise kohta GCP püsiketas (ja see on juba ametlik teave, mida kinnitavad testid). Sama on teinud ka teised eksperdid vaatlus, mis on seotud erinevate ketastega.
  2. Mida vähem süsteemikõnesid, seda suurem on jõudlus (võim võib olla umbes 5%). Paistab väljakutsena open() lipuga O_DSYNC või helista pwritev2() lipuga RWF_SYNC kiirem kui kõne fdatasync(). Ma kahtlustan, et siin on mõte selles, et see lähenemine mängib rolli selles, et sama probleemi lahendamiseks tuleb teha vähem süsteemikutseid (kahe asemel üks kõne). Kuid jõudluse erinevus on väga väike, nii et saate seda täielikult ignoreerida ja kasutada rakenduses midagi, mis ei muuda selle loogikat keeruliseks.

Kui olete huvitatud säästva andmesalvestuse teemast, on siin mõned kasulikud materjalid:

  • I/O juurdepääsumeetodid — ülevaade sisend/väljundmehhanismide põhitõdedest.
  • Andmete kettale jõudmise tagamine — lugu sellest, mis juhtub andmetega teel rakendusest kettale.
  • Millal peaksite sisaldava kataloogi fsync - vastus küsimusele, millal kasutada fsync() kataloogide jaoks. Lühidalt öeldes selgub, et peate seda tegema uue faili loomisel ja selle soovituse põhjuseks on see, et Linuxis võib samale failile olla palju viiteid.
  • SQL Server Linuxis: FUA sisemised — siin on kirjeldus selle kohta, kuidas püsivat andmesalvestust SQL Serveris Linuxi platvormil rakendatakse. Siin on mõned huvitavad võrdlused Windowsi ja Linuxi süsteemikutsete vahel. Olen peaaegu kindel, et just tänu sellele materjalile õppisin XFS-i FUA optimeerimise kohta.

Kas olete kaotanud andmed, mis teie arvates olid kettale turvaliselt salvestatud?

Vastupidav andmesalvestus ja Linuxi faili API-d

Vastupidav andmesalvestus ja Linuxi faili API-d

Allikas: www.habr.com