Duorsume Data Storage en Linux File API's

By it ûndersykjen fan de duorsumens fan gegevensopslach yn wolksystemen, besleat ik mysels te testen om te soargjen dat ik de basis dingen begriep. ik begûn mei it lêzen fan de NVMe-spesifikaasje om te begripen hokker garânsjes oangeande duorsume gegevens opslach (dat wol sizze, garandearret dat de gegevens sille wêze beskikber nei in systeem flater) jou ús NMVe skiven. Ik makke de folgjende haadkonklúzjes: gegevens moatte wurde beskôge as skansearre fanôf it momint dat it kommando om gegevens te skriuwen wurdt jûn oant it momint dat it skreaun is nei it opslachmedium. De measte programma's brûke lykwols frij lokkich systeemoproppen om gegevens op te nimmen.

Yn dit post ûndersykje ik de oanhâldende opslachmeganismen levere troch de Linux-bestân-API's. It liket derop dat alles hjir ienfâldich wêze moat: it programma neamt it kommando write(), en nei't dit kommando foltôge is, wurde de gegevens feilich opslein op skiif. Mar write() kopiearret allinnich applikaasje gegevens nei de kernel cache leit yn RAM. Om it systeem te twingen om gegevens op skiif te skriuwen, moatte jo wat ekstra meganismen brûke.

Duorsume Data Storage en Linux File API's

Oer it algemien is dit materiaal in samling notysjes oangeande wat ik haw leard oer in ûnderwerp fan belang foar my. As wy it heul koart prate oer it wichtichste, docht bliken dat jo it kommando brûke moatte om duorsume gegevensopslach te organisearjen fdatasync() of iepenje triemmen mei de flagge O_DSYNC. As jo ​​​​ynteressearre binne om mear te learen oer wat bart mei gegevens op 'e wei fan koade nei skiif, sjoch dan ris nei dit lidwurd.

Skaaimerken fan it brûken fan de write() funksje

Systeem oprop write() definiearre yn de standert IEEE POSIX as in besykjen om te skriuwen gegevens nei in triem descriptor. Nei suksesfolle foltôging write() Gegevenslêsoperaasjes moatte presys de bytes weromjaan dy't earder skreaun binne, dit dwaan sels as de gegevens tagong krije fan oare prosessen of diskusjes (sjoch relevante seksje fan 'e POSIX-standert). it is, yn 'e seksje oer hoe't diskusjes ynteraksje mei normale triemoperaasjes, is d'r in notysje dy't seit dat as twa diskusjes elk dizze funksjes neame, dan moat elke oprop alle oanwiisde gefolgen fan 'e oare oprop sjen, of hielendal gjinien. gefolgen. Dit liedt ta de konklúzje dat alle triem I / O operaasjes moatte hold in slot op de boarne se operearje op.

Betsjut dit dat de operaasje write() is it atoom? Ut in technysk eachpunt, ja. Gegevenslêsoperaasjes moatte alles of neat weromjaan fan wat mei skreaun is write(). Mar de operaasje write(), neffens de standert, hoecht net perfoarst ôf te sluten mei it opskriuwen fan alles wat frege waard om op te skriuwen. Se mei mar in part fan de gegevens skriuwe. Wy kinne bygelyks twa triedden hawwe dy't elk 1024 bytes tafoegje oan in bestân beskreaun troch deselde triembeskriuwer. Ut it eachpunt fan 'e standert sil in akseptabel resultaat wêze as elke skriuwoperaasje mar ien byte kin tafoegje oan it bestân. Dizze operaasjes sille atomysk bliuwe, mar nei't se foltôge binne, wurde de gegevens dy't se skreaun hawwe nei it bestân mingd. hjir heul ynteressante diskusje oer dit ûnderwerp op Stack Overflow.

fsync() en fdatasync() funksjes

De maklikste manier om gegevens op skiif te spoelen is de funksje op te roppen fsync(). Dizze funksje freget it bestjoeringssysteem om alle wizige blokken oer te bringen fan 'e cache nei skiif. Dit omfettet alle metadata fan bestân (tagongstiid, tiid foar wiziging fan bestân, ensfh.). Ik leau dat dizze metadata selden nedich binne, dus as jo witte dat it net wichtich is foar jo, kinne jo de funksje brûke fdatasync(). de help on fdatasync() it wurdt sein dat by de wurking fan dizze funksje sa'n hoemannichte metadata wurdt opslein op skiif dy't "nedich is foar de juste útfiering fan de folgjende gegevenslêsoperaasjes." En dit is krekt wat de measte applikaasjes soarchje oer.

Ien probleem dat hjir ûntstean kin is dat dizze meganismen net garandearje dat it bestân te finen is nei in mooglike mislearring. Benammen by it meitsjen fan in nij bestân moatte jo skilje fsync() foar de map dy't it befettet. Oars kin it nei in mislearring bliken dat dizze triem net bestiet. De reden hjirfoar is dat yn UNIX, troch it brûken fan hurde keppelings, in bestân yn meardere mappen bestean kin. Dêrom, by it roppen fsync() d'r is gjin manier foar in bestân om te witten hokker mapgegevens ek nei skiif moatte wurde flush (hjir Jo kinne hjir mear oer lêze). It liket derop dat it ext4-bestânsysteem yn steat is automatysk oanfreegje fsync() nei de mappen dy't de oerienkommende triemmen befetsje, mar dit kin miskien net it gefal wêze mei oare bestânssystemen.

Dit meganisme kin oars wurde ymplementearre op ferskate bestânssystemen. ik brûkte blktrace om te learen oer hokker skiifoperaasjes wurde brûkt yn ext4- en XFS-bestânsystemen. Beide jouwe reguliere skriuwkommando's út nei skiif foar sawol de triemynhâld as it bestânsysteemsjoernaal, spoelje de cache, en gean út troch in FUA (Force Unit Access, skriuwe gegevens direkt nei skiif, omgean de cache) skriuwe nei it sjoernaal. Se dogge dit wierskynlik om te befêstigjen dat de transaksje hat plakfûn. Op driuwfearren dy't FUA net stypje, feroarsake dit twa cache-flushes. Myn eksperiminten lieten dat sjen fdatasync() in bytsje flugger fsync(). Utility blktrace jout dat oan fdatasync() skriuwt normaal minder gegevens op skiif (yn ext4 fsync() skriuwt 20 KiB, en fdatasync() - 16 KiB). Ek fûn ik út dat XFS wat flugger is dan ext4. En hjir mei help blktrace slagge om dat út te finen fdatasync() spoelt minder gegevens nei skiif (4 KiB yn XFS).

Dûbelsinnige situaasjes dy't ûntsteane by it brûken fan fsync()

Ik kin tinke oan trije dûbelsinnige situaasjes oangeande fsync()dy't ik yn 'e praktyk tsjinkaam.

De earste sa'n gefal barde yn 2008. Dan beferzen de Firefox 3-ynterface as in grut oantal bestannen op skiif skreaun waard. It probleem wie dat de ymplemintaasje fan 'e ynterface in SQLite-database brûkte om ynformaasje oer har steat te bewarjen. Nei elke feroaring dy't barde yn 'e ynterface, waard de funksje neamd fsync(), dy't goede garânsjes joech foar stabile gegevensopslach. Yn it dan brûkte ext3-bestânsysteem, de funksje fsync() dumpte alle "smoarge" siden yn it systeem nei skiif, en net allinnich dyjingen dy't wiene besibbe oan de oerienkommende triem. Dit betsjutte dat troch te klikken op in knop yn Firefox kin megabytes oan gegevens wurde skreaun nei in magnetyske skiif, wat in protte sekonden koe duorje. De oplossing foar it probleem, sa fier as ik begryp út it materiaal wie om wurk mei de databank oer te dragen nei asynchrone eftergrûntaken. Dit betsjut dat Firefox earder strangere opslacheasken ymplementearre dan echt nedich wie, en de funksjes fan it ext3-bestânsysteem fergrutte dit probleem allinich.

It twadde probleem barde yn 2009. Dan, nei in systeemcrash, waarden brûkers fan it nije ext4-bestânsysteem konfrontearre mei it feit dat in protte nij oanmakke bestannen gjin lingte hiene, mar dit barde net mei it âldere ext3-bestânsysteem. Yn 'e foarige paragraaf haw ik it oer hoe't ext3 tefolle gegevens nei skiif spoelde, wat dingen in protte fertrage. fsync(). Om de situaasje te ferbetterjen, wurde yn ext4 allinich de smoarge siden dy't relevant binne foar in bepaald bestân nei de skiif spoeld. En gegevens fan oare bestannen bliuwe folle langer yn it ûnthâld dan mei ext3. Dit is dien om prestaasjes te ferbetterjen (standert bliuwe de gegevens 30 sekonden yn dizze steat, jo kinne dit konfigurearje mei dirty_expire_centisecs; hjir Jo kinne ekstra materialen fine oer dit). Dit betsjut dat in grutte hoemannichte gegevens ûnherstelber ferlern gean kinne nei in mislearring. De oplossing foar dit probleem is te brûken fsync() yn applikaasjes dy't stabile gegevensopslach soargje moatte en har safolle mooglik beskermje tsjin 'e gefolgen fan mislearrings. Funksje fsync() wurket folle effisjinter by it brûken fan ext4 dan by it brûken fan ext3. It neidiel fan dizze oanpak is dat it gebrûk, lykas earder, de útfiering fan guon operaasjes, lykas it ynstallearjen fan programma's, fertraget. Sjoch details oer dit hjir и hjir.

It tredde probleem oangeande fsync(), ûntstien yn 2018. Dan, yn it ramt fan it PostgreSQL-projekt, waard fûn dat as de funksje fsync() in flater tsjinkomt, it markearret "smoarge" siden as "skjin". As gefolch, de folgjende oproppen fsync() Se dogge neat mei sokke siden. Hjirtroch wurde wizige siden opslein yn it ûnthâld en wurde nea op skiif skreaun. Dit is in echte ramp, om't de applikaasje sil tinke dat guon gegevens op 'e skiif skreaun binne, mar yn feite sil it net wêze. Sokke mislearrings fsync() binne seldsum, de applikaasje yn sokke situaasjes kin dwaan hast neat te bestriden it probleem. Dizze dagen, as dit bart, crashe PostgreSQL en oare applikaasjes. it is, yn it materiaal "Kin applikaasjes herstelle fan fsync-fouten?", wurdt dit probleem yn detail ûndersocht. Op it stuit is de bêste oplossing foar dit probleem om Direct I / O te brûken mei de flagge O_SYNC of mei in flagge O_DSYNC. Mei dizze oanpak sil it systeem flaters rapportearje dy't foarkomme kinne by spesifike skriuwoperaasjes, mar dizze oanpak fereasket dat de applikaasje de buffers sels beheart. Lês mear oer dit hjir и hjir.

Triemen iepenje mei de flaggen O_SYNC en O_DSYNC

Litte wy weromgean nei de diskusje fan Linux-meganismen dy't stabile gegevensopslach leverje. Wy hawwe it nammentlik oer it brûken fan de flagge O_SYNC of flagge O_DSYNC by it iepenjen fan bestannen mei systeemoprop iepen(). Mei dizze oanpak wurdt elke gegevensskriuwoperaasje útfierd as nei elk kommando write() it systeem wurdt jûn opdrachten neffens fsync() и fdatasync(). de POSIX spesifikaasjes dit hjit "Syngronisearre I/O-bestânyntegriteitsfoltôging" en "Dataintegriteitfoltôging". It wichtichste foardiel fan dizze oanpak is dat jo om de yntegriteit fan gegevens te garandearjen mar ien systeemoprop hoege te meitsjen, ynstee fan twa (bygelyks - write() и fdatasync()). It wichtichste neidiel fan dizze oanpak is dat alle skriuwt mei de oerienkommende triembeskriuwing syngronisearre wurde, wat de mooglikheid beheine kin om de applikaasjekoade te strukturearjen.

Mei help fan Direct I / O mei de O_DIRECT flagge

Systeem oprop open() stipet flagge O_DIRECT, dat is ûntworpen om de cache fan it bestjoeringssysteem te omgean om I/O-operaasjes út te fieren troch direkt mei de skiif te ynteraksje. Dit betsjut yn in protte gefallen dat skriuwkommando's útjûn troch it programma direkt wurde oerset yn kommando's dy't rjochte binne op it wurkjen mei de skiif. Mar, yn it algemien, dit meganisme is gjin ferfanging fan funksjes fsync() of fdatasync(). It feit is dat de skiif sels kin defer of cache korrespondearjende gegevens skriuwen kommando's. En, om saken slimmer te meitsjen, yn guon spesjale gefallen de I / O operaasjes útfierd by it brûken fan de flagge O_DIRECT, útstjoere yn tradisjonele buffered operaasjes. De maklikste manier om dit probleem op te lossen is de flagge te brûken om bestannen te iepenjen O_DSYNC, wat sil betsjutte dat elke skriuwoperaasje wurdt folge troch in oprop fdatasync().

It die bliken dat it XFS-bestânsysteem koartlyn in "snel paad" tafoege hat foar O_DIRECT|O_DSYNC- gegevens opname. As in blok wurdt herskreaun mei help O_DIRECT|O_DSYNC, dan sil XFS, ynstee fan it cache te spoelen, it FUA-skriuwkommando útfiere as it apparaat it stipet. Ik ferifiearre dit mei help fan it hulpprogramma blktrace op in Linux 5.4/Ubuntu 20.04-systeem. Dizze oanpak soe effisjinter wêze moatte, om't as brûkt wurdt, wurdt in minimale hoemannichte gegevens op 'e skiif skreaun en ien operaasje wurdt brûkt, ynstee fan twa (skriuwen en spoelen fan it cache). Ik fûn in keppeling nei patch 2018 kernel, dy't dit meganisme ymplementearret. D'r is wat diskusje oer it tapassen fan dizze optimalisaasje op oare bestânssystemen, mar foar safier't ik wit, is XFS it ienige bestânsysteem dat dit oant no ta stipet.

sync_file_range() funksje

Linux hat in systeemoprop sync_file_range(), wêrmei jo mar in part fan it bestân nei skiif spoelje kinne, ynstee fan it hiele bestân. Dizze oprop inisjearret in asynchrone gegevensspoeling en wachtet net op it foltôgjen. Mar yn it sertifikaat sync_file_range() it team wurdt sein "hiel gefaarlik". It is net oan te rieden om it te brûken. Funksjes en gefaren sync_file_range() hiel goed beskreaun yn dit materiaal. Spesifyk liket dizze oprop RocksDB te brûken om te kontrolearjen as de kernel smoarge gegevens nei skiif spoelt. Mar tagelyk, om stabile gegevens opslach te garandearjen, wurdt it ek brûkt fdatasync(). de koade RocksDB hat wat nijsgjirrige opmerkings oer dit ûnderwerp. Bygelyks, it docht bliken dat de oprop sync_file_range() By it brûken fan ZFS spoelt it gjin gegevens op skiif. Underfining fertelt my dat koade dy't selden brûkt wurdt wierskynlik bugs befetsje. Dêrom soe ik advisearje tsjin it brûken fan dit systeem oprop útsein as perfoarst nedich.

Systeemoproppen dy't helpe om gegevenspersistinsje te garandearjen

Ik bin ta de konklúzje kommen dat d'r trije oanpak binne dy't kinne wurde brûkt om I / O-operaasjes út te fieren dy't gegevenspersistinsje garandearje. Se hawwe allegear in funksje oprop nedich fsync() foar de map wêryn it bestân makke is. Dit binne de oanpak:

  1. Oprop in funksje fdatasync() of fsync() na funksje write() (it is better om te brûken fdatasync()).
  2. Wurkje mei in triembeskriuwer iepene mei in flagge O_DSYNC of O_SYNC (better - mei in flagge O_DSYNC).
  3. Kommando gebrûk pwritev2() mei flagge RWF_DSYNC of RWF_SYNC (leafst mei in flagge RWF_DSYNC).

Performance Notes

Ik haw de prestaasjes fan 'e ferskate meganismen dy't ik haw ûndersocht net soarchfâldich mjitten. De ferskillen dy't ik opmurken yn 'e snelheid fan har wurk binne heul lyts. Dit betsjut dat ik ferkeard wêze kin, en dat itselde ding ûnder ferskillende omstannichheden ferskate resultaten kin produsearje. Earst sil ik prate oer wat de prestaasjes mear beynfloedet, en dan wat de prestaasjes minder beynfloedet.

  1. It oerskriuwen fan triemgegevens is rapper dan it tafoegjen fan gegevens oan in bestân (it prestaasjesfoardiel kin 2-100% wêze). It taheakjen fan gegevens oan in bestân fereasket ekstra wizigingen oan de metadata fan it bestân, sels nei in systeemoprop fallocate(), mar de omfang fan dit effekt kin ferskille. Ik advisearje, foar bêste prestaasje, te skiljen fallocate() om de fereaske romte foarôf te tawizen. Dan moat dizze romte eksplisyt ynfolle wurde mei nullen en neamd wurde fsync(). Dit sil derfoar soargje dat de oerienkommende blokken yn it bestânsysteem wurde markearre as "allokearre" ynstee fan "net allocearre". Dit jout in lytse (sawat 2%) prestaasjesferbettering. Derneist kinne guon skiven in stadiger earste tagong hawwe ta in blok as oaren. Dit betsjut dat it foljen fan de romte mei nullen kin liede ta in signifikante (sawat 100%) ferbettering fan prestaasjes. Benammen dit kin barre mei skiven AWS EBS (dit is net-offisjele gegevens, ik koe it net befestigje). Itselde jildt foar opslach GCP Persistente Skiif (en dit is al offisjele ynformaasje, befêstige troch tests). Oare saakkundigen hawwe itselde dien observaasje, ferbân mei ferskate skiven.
  2. De minder systeemoproppen, hoe heger de prestaasjes (de winst kin sawat 5%). It liket in útdaging open() mei flagge O_DSYNC of belje pwritev2() mei flagge RWF_SYNC flugger as in oprop fdatasync(). Ik tink dat it punt hjir is dat dizze oanpak in rol spilet yn it feit dat minder systeemoproppen útfierd wurde moatte om itselde probleem op te lossen (ien oprop ynstee fan twa). Mar it ferskil yn prestaasjes is heul lyts, dus jo kinne it folslein negearje en iets brûke yn 'e applikaasje dat syn logika net komplisearret.

As jo ​​​​ynteressearre binne yn it ûnderwerp fan duorsume gegevensopslach, binne hjir wat nuttige materialen:

  • I / O tagong metoaden - oersjoch fan 'e basis fan ynfier- / útfiermeganismen.
  • It garandearjen fan gegevens berikt skiif - in ferhaal oer wat der bart mei de gegevens op 'e wei fan' e applikaasje nei de skiif.
  • Wannear moatte jo de befette map fsyngronisearje - it antwurd op de fraach wannear te brûken fsync() foar mappen. Om dit yn in nutedop te setten, docht bliken dat jo dit dwaan moatte by it meitsjen fan in nij bestân, en de reden foar dizze oanbefelling is dat yn Linux d'r in protte ferwizings kinne wêze nei deselde bestân.
  • SQL Server op Linux: FUA Internals - hjir is in beskriuwing fan hoe persistente gegevensopslach wurdt ymplementearre yn SQL Server op it Linux-platfoarm. D'r binne wat nijsgjirrige fergelikingen tusken Windows- en Linux-systeemoproppen hjir. Ik bin der hast wis fan dat it wie te tankjen oan dit materiaal dat ik learde oer FUA-optimalisaasje fan XFS.

Hawwe jo gegevens ferlern dy't jo tocht wiene feilich opslein op in skiif?

Duorsume Data Storage en Linux File API's

Duorsume Data Storage en Linux File API's

Boarne: www.habr.com