Olen uurinud andmete salvestamise usaldusväärsust pilvesüsteemides ja olen otsustanud end proovile panna, et aru saada, kas ma mõistan põhilisi aspekte. Ma , et mõista, milliseid tagatisi püsiva andmete salvestamise kohta (st tagatisi, et andmed on saadaval pärast süsteemi riket) pakuvad NMVe kettad. Jõudsin järgmistele peamistele järeldustele: andmeid tuleb pidada rikutuks hetkel, kui on antud andmete kirjutamise käsk, ja kuni hetkeni, mil nende kirjutamine andmekandjale lõpetatakse. Siiski, enamikus andmete kirjutamise programmides kasutatakse väga rahulikult süsteemi väljakutseid.
Käesolevas materjalis uurin püsivate andmete salvestamise mehhanisme, mida pakuvad Linuxi failide API-d. Tundub, et siin peaks kõik olema lihtne: programm kutsub välja käsu write(), ja pärast selle käsu täitmist peaksid andmed olema usaldusväärselt salvestatud kettale. Kuid write() see lihtsalt kopeerib rakenduse andmed tuuma vahemällu, mis asub aktiivses mälus. Et sundida süsteemi andmeid kettale kirjutama, tuleb kasutada mõningaid täiendavaid mehhanisme.
Üldiselt on see materjal kogumik märkmeid, mis käsitlevad seda, mida ma huvitaval teemal õppisin. Kui väga lühidalt rääkida kõige olulisemast, siis selgub, et püsiva andmete salvestamise korraldamiseks tuleb kasutada käsku fdatasync() või avada faile flagi O_DSYNCabil. Kui teid huvitab üksikasjalik ülevaade sellest, mis juhtub andmetega teel programmikoodist kettale, vaadake artiklit.
write() funktsiooni kasutamise eripärad
Süsteemi väljakutse write() on määratletud standardis kui katse andmete kirjutamiseks failide deskiptorisse. Pärast operatsiooni edukat lõpetamist write() peavad andmete lugemise toimingud tagastama täpselt need baitid, mis olid eelnevalt kirjutatud, isegi juhul, kui andmetele pääsevad ligi teised protsessid või lõimed ( vastav POSIXi standardi osa). , jaotises, mis käsitleb voogude suhtlemist tavaliste failitegevustega, on märge, kus öeldakse, et kui iga voog kutsub neid funktsioone välja, peab iga väljakutse nägema kas kõiki määratud tagajärgi, mida põhjustab muu väljakutse täitmine, või ei näe mitte ühtegi tagajärge. See võimaldab järeldada, et kõik sisendi/väljundi failitegevused peavad hoidma ressursi lukus, millega nad töötavad.
Kas see tähendab, et operatsioon write() on aatomaarne? Tehniliselt — jah. Andmete lugemise operatsioonid peaksid tagastama kas kõik või mitte midagi sellest, mis on kirjutatud funktsiooni write(). Kuid operatsioon write(), vastavalt standardile, ei pea tingimata lõpule viima, kirjutades kõik, mis tal oli ette nähtud kirjutada. Tal on lubatud kirjutada ainult osa andmeid. Näiteks võib meil olla kaks voogu, igaüks, kes lisab 1024 baiti faili, millel on sama failihaldur. Standardi kohaselt on vastuvõetav tulemus, kui iga kirjutamisoperatsioon suudab faili lisada vaid ühe baiti. Need operatsioonid jäävad aatomaarseks, kuid nende lõpuleviimisel on neile kirjutatud andmed failis segamini. Sellel teemal on väga huvitav arutelu Stack Overflow's.
Funktsioonid fsync() ja fdatasync()
Lihtsaim viis andmete kettale kirjutamiseks on funktsiooni väljakutse . See funktsioon taotleb operatsioonisüsteemilt kõigi muudetud plokkide edastamist vahemälust kettale. Siia kuuluvad ka kõik faili metaandmed (jõudmisaja, faili muutmise aeg jne). Ma usun, et nende metaandmete vajadus tekib harva, seega kui teate, et need ei ole teile olulised, võite kasutada funktsiooni fdatasync(). Failis 6TB fdatasync() ütleb, et selle funktsiooni töö käigus salvestatakse kettale nii palju metaandmeid, kui on "vajalik järgmiste andmete lugemise operatsioonide korrektseks teostamiseks". Ja see on täpselt see, mis enamiku rakenduste jaoks muret tekitab.
Üks probleem, mis siit võib tekkida, on see, et need mehhanismid ei garanteeri, et faili saab pärast võimalikku riket leida. Eriti siis, kui luuakse uus fail, tuleb väljakutse teha fsync() katalooge, mis seda sisaldab. Vastasel juhul võib pärast tõrget juhtuda, et seda faili ei eksisteeri. Selle põhjuseks on asjaolu, et UNIXis, tugevate linkide kasutamise tõttu, võib fail eksisteerida mitmes kataloogis. Seetõttu kutsutakse esile fsync() faili jaoks pole võimalust teada, milliste kataloogide andmeid tuleb samuti kettale kirjutada ( sellest saab lugeda lähemalt). Tundub, et failisüsteem ext4 on võimeline rakendama fsync() kataloogidesse, mis sisaldavad vastavaid faile, kuid teiste failisüsteemide puhul ei pruugi see nii olla.
Seda mehhanismi võib erinevates failisüsteemides eri viisil rakendada. Ma kasutasin selgitamaks, milliseid kettategevusi kasutatakse failisüsteemides ext4 ja XFS. Mõlemad annavad tavalised kettale kirjutamise käsud nii failide sisu kui ka failisüsteemi žurnali jaoks, kirjutavad vahemälud tühjaks ja lõpetavad töötamise, teostades FUA-kirjutamise (Force Unit Access, andmete kirjutamine otse kettale, vahe mälu vahele küsimata) žurnali. Tõenäoliselt käituvad nad nii, et kinnitada operatsiooni teostamise fakti. Kettadel, mis ei toeta FUA-d, tekitab see kaks vahemälu tühjendust. Minu katsed näitasid, et fdatasync() veidi kiiremini fsync(). Utiliit blktrace'i jätab mulje, et fdatasync() kirjutab tavaliselt kettale vähem andmeid (ext4 fsync() kirjutab 20 KiB, samas kui fdatasync() — 16 KiB). Lisaks avastasin, et XFS on veidi kiirem kui ext4. Ja siin sain blktrace'i teada, et fdatasync() kirjutab kettale vähem andmeid (4 KiB XFS-is).
Ebamugavad olukorrad, mis tekivad fsync() kasutamisel
Mulle tuleb meelde kolm ebamugavat olukorda, mis on seotud fsync(), millega ma olen praktikas kokku puutunud.
Esimene selline juhtum toimus 2008. aastal. Sel ajal ‘jäi’ Firefox 3 kasutajaliides seisma, kui kirjutati kettale palju faile. Probleem oli selles, et liidese oleku teabe salvestamiseks kasutati SQLite andmebaasi. Pärast iga muudatust, mis toimus liideses, kutsuti välja funktsioon fsync(), mis pakkus head garantiid andmete püsiva hoidmise osas. Tol ajal kasutatud failisüsteemis ext3 funktsioon fsync() salvestas süsteemis kõik „räpased“ lehed, mitte ainult need, mis olid seotud vastava failiga. See tähendas, et hiireklõps Firefoxis võis algatada megabaidise andmete salvestamise magnetketta peale, mis võis võtta palju sekundeid. Probleemi lahendus, nii nagu ma aru sain, oli materjalist, et andmebaasihalduse viimine asünkroonsetesse taustatöödesse oli vajalik. See tähendab, et varem olid Firefoxis rakendatud rangemad andmete salvestamise vastupidavuse nõuded, kui see tegelikult vajalik oli, ja ext3 failisüsteemi omadused halvendavad seda probleemi veelgi.
Teine vastuolu tekkis 2009. aastal. Siis, pärast süsteemi nurjumist, leidsid uue ext4 failisüsteemi kasutajad, et paljud hiljuti loodud failid on nullpikkusega, kuid vanema ext3 failisüsteemi puhul ei toimunud sellist asja. Eelmises lõigus rääkisin, et ext3 salvestas kettale liiga palju andmeid, mis aeglustas oluliselt fsync()tööd. Situatsiooni parandamiseks salvestab ext4 kettale ainult need „räpased“ lehed, mis on seotud konkreetse failiga. Teiste failide andmed jäävad mälu sisse palju pikemaks ajaks kui ext3 kasutamisel. See tehti jõudluse parandamiseks (vaikimisi on andmed sellises olekus 30 sekundit, seda saab seadistada ; leiate täiendavat teavet selle kohta). See tähendab, et suurel andmemahtude puhul võivad andmed olla pärast süsteemiviga pöördumatult kadunud. Selle probleemi lahendus seisneb selle kasutamises fsync() rakendustes, mis peavad tagama usaldusväärse andmesalvestuse ja maksimaalselt kaitsta neid tõrgete tagajärgede eest. Funktsioon fsync() toimib ext4 kasutamisel palju tõhusamalt kui ext3 kasutamisel. Selle lähenemise miinus on see, et selle rakendamine aeglustab, nagu ennegi, teatud toimingute täitmist, näiteks programmide installimist. Üksikasjad selle kohta leiate ja .
Kolmas probleem, mis fsync(), kerkis esile 2018. aastal. Sel ajal, PostgreSQL projekti raames, selgitati välja, et kui funktsioon fsync() kohtab viga, märgib see „räpased“ lehed kui „puhtad“. Seetõttu järgmised väljakutsed fsync() Selliseid lehti ei tehta midagi. Seetõttu säilivad muudetud lehed mälu piires ja neid ei salvestata kunagi kettale. See on tõeline katastroof, kuna rakendus usub, et teatud andmed on kettale salvestatud, kuigi tegelikult see ei ole nii. Sellised vead fsync() esinevad harva, rakendus sellistes olukordades peaaegu ei saa probleemi lahendamiseks midagi ette võtta. Tänapäeval, kui see juhtub, katkestavad PostgreSQL ja teised rakendused oma töö. , artiklis „Kas rakendused saavad fsynci tõrgetest taastuda?”, uuritakse seda probleemi üksikasjalikult. Praegu on parim lahendus selle probleemi jaoks kasutada Direct I/O koos lipuga O_SYNC või lipuga O_DSYNC. Sellise lähenemise korral teatab süsteem vigu, mis võivad tekkida andmete kirjutamise konkreetsete operatsioonide täitmisel, kuid see lähenemine nõuab, et rakendus haldaks puhverdamist ise. Lisateavet selle kohta lugege ja .
Failide avamine O_SYNC ja O_DSYNC lippudega
Naaseme arutelu juurde Linuxi mehhanismide kohta, mis tagavad andmete püsiva salvestamise. Nimelt räägime me lipu kasutamisest O_SYNC või lipu O_DSYNC failide avamisel süsteemi süsteemikõnes . Sellise lähenemise korral toimub iga andmete kirjutamise toiming nii, nagu oleks pärast iga käsku write() süsteemile antud vastavalt käsud fsync() ja fdatasync(). Failis nii öeldakse "Sünkroniseeritud I/O Faili Terviklikkuse Täitmine" ja "Andmete Terviklikkuse Täitmine". Selle lähenemise peamine eelis on see, et andmete terviklikkuse tagamiseks peab toimuma ainult üks süsteemi kõne, mitte kaks (näiteks — write() ja fdatasync()). Selle lähenemise peamine puudus on see, et kõik kirjutamisoperatsioonid, mis kasutavad vastavat failikirjet, sünkroniseeritakse, mis võib piirata rakenduse koodi struktureerimise võimalusi.
Direct I/O kasutamine O_DIRECT lipuga
Süsteemi väljakutse open() toetab lippu O_DIRECT, mis on mõeldud selleks, et mööda minna operatsioonisüsteemi vahemälust ning teostada sisendi-väljundi toiminguid, suheldes otse ketastega. See tähendab paljudes olukordades, et programmi poolt antud kirjutamiskäsked tõlgitakse otse ketastele suunatud käskudeks. Kuid üldiselt ei asenda see mehhanism funktsioone fsync() või fdatasync(). Asi on selles, et ise kettas võib vastavaid andmete salvestamise käsklusi. Ja mis veel hullem, mõningatel erijuhtudel sisendi-väljundi toimingud, mida viiakse läbi lipu kasutamisel O_DIRECT, traditsioonilistesse puhvrifunktsioonidesse. Selle probleemi lihtsaim lahendus on kasutada failide avamisel ka lippu O_DSYNC, mis tähendab, et iga salvestamistoiminguga kaasneb kutse fdatasync().
Leiti, et XFS failisüsteemisse on hiljuti lisatud "kiire tee" O_DIRECT|O_DSYNC-andmete salvestamiseks. Kui bloke peatatakse, kasutades O_DIRECT|O_DSYNC, siis XFS, selle asemel et vahemälu tühjendada, viib läbi FUA-salvestamise käsu, kui seade seda toetab. Ma olen seda veendunud, kasutades tööriista blktrace'i Linuxis 5.4 / Ubuntu 20.04. Selline lähenemine peaks olema efektiivsem, kuna selle kasutamisel salvestatakse kettale minimaalne andmemahu hulk ja rakendatakse ühte operatsiooni, mitte kahte (salvestamine ja vahemälu tühjendamine). Leidsin viite 2018. aasta tuumale, milles see mehhanism on rakendatud. Seal on arutelu selle optimeerimise rakendamise kohta ka teistes failisüsteemides, kuid niipalju kui ma tean, on XFS praegu ainus failisüsteem, mis seda toetab.
funktsioon sync_file_range()
Linuxis on süsteemikutsumus , mis võimaldab salvestada kettale vaid osa failist, mitte kogu faili. See kutsung algatab asünkroonse andmete tühjendamise ja ei oota selle lõpetamist. Kuid juhendis sync_file_range() öeldakse, et see käsk on "väga ohtlik". Selle kasutamist ei soovitata. Omadused ja ohud sync_file_range() on väga hästi kirjeldatud materjalis. Eelkõige näib, et see kutse kasutab RocksDB-d selleks, et hallata, millal tuum tühjendab "mustad" andmed kettale. Kuid samal ajal kasutatakse seal andmete püsimise tagamiseks ka fdatasync(). Failis RocksDB-s on selle teema kohta huvitavad kommentaarid. Näiteks tundub, et kutse sync_file_range() kasutades ZFS ei viibi andmete tühjendamiseni kettale. Kogemus vihjab, et harva kasutatav kood võib sisaldada vigu. Seetõttu soovitaksin seda süsteemikutsumust mitte kasutada ilma äärmise vajaduseta.
Süsteemikutsumised, mis aitavad tagada andmete püsivuse
Olen jõudnud järeldusele, et andmete püsivaks salvestamiseks mõeldud sisendi/väljundi toiminguteks on olemas kolm lähenemist. Kõik need nõuavad funktsiooni väljakutset fsync() kausta, kus fail on loodud. Need lähenemised on:
- Funktsiooni väljakutse
fdatasync()võifsync()pärast funktsiooniwrite()(on parem kasutadafdatasync()). - Faili descriptoriga töötamine, mis on avatud lipuga
O_DSYNCvõiO_SYNC(on parem — lipugaO_DSYNC). - Käsu kasutamine
pwritev2()lipugaRWF_DSYNCvõiRWF_SYNC(on eelistatavam — lipugaRWF_DSYNC).
Jõudluse märkmed
Ma ei ole põhjalikult mõõtnud erinevate lähenemiste jõudlust, mida olen uurinud. Märgatud kiirusvahed on üsna väikesed. See tähendab, et võin eksida ja teistes tingimustes võivad samad asjaolud anda teistsuguseid tulemusi. Esiteks räägin sellest, mis mõjutab jõudlust enam, ja seejärel sellest, mis mõjutab jõudlust vähem.
- Faili andmete ülekatte täitmine on kiirem kui andmete failiga liitmine (jõudluse kasu võib olla 2-100%). Andmete failiga liitmine nõuab täiendavate muudatuste tegemist faili metaandmetes, isegi pärast süsteemikutsumist
fallocate(), kuid selle efekti ulatus võib varieeruda. Soovitan, et parima jõudluse tagamiseks kutsudafallocate()vajaliku ruumi eelnevalt reserveerimiseks. Seejärel tuleb see ruum selgelt nullidega täita ja kutsudafsync(). Nii märgitakse vastavad plokid failisüsteemis kui 'reserveeritud', mitte kui 'reserveerimata'. See toob kaasa väikese (umbes 2%) jõudluse paranemise. Lisaks võivad mõned kettad esmakordsel plokiga juurdepääsutegemisel töötada aeglasemalt kui teised. See tähendab, et ruumi nullidega täitmine võib viia märkimisväärse (umbes 100%) jõudluse paranemiseni. Eriti võib see juhtuda ketastega (need andmed on mitteametlikud, ma ei suutnud neid kinnitada). Sama kehtib ka salvestuste kohta (see on juba ametlik teave, mis on kinnitatud katsetega). Teised spetsialistid on teinud samasuguseid , mis on seotud erinevate ketastega. - Mida vähem on süsteemikutsungeid, seda kõrgem on jõudlus (kasu võib olla umbes 5%). Tundub, et
open()lipugaO_DSYNCvõi kutsepwritev2()lipugaRWF_SYNCkiiremini kutsefdatasync(). Kahtlen, et asi on selles, et sellise lähenemise korral on oluline, et sama ülesande lahendamiseks on vaja teha vähem süsteemikõnesid (üks kõne kahe asemel). Kuid jõudluse erinevus on väga väike, seetõttu võite selle tähelepanuta jätta ja kasutada rakenduses seda, mis ei too kaasa selle loogika keerukust.
Kui teid huvitab andmete püsivuse teema – siin on mõned kasulikud materjalid:
- – ülevaade sisendi/väljaande mehhanismide põhimõtetest.
- – jutt sellest, mis juhtub andmetega teel rakendusest kettale.
- – vastus küsimusele, millal tuleks rakendada
fsync()kaustadele. Kui rääkida sellest lühidalt, siis tuleb seda teha uue faili loomisel, ning soovituse põhjus seisneb selles, et Linuxis võib olla palju viiteid ühe ja sama faili kohta. - – siin on toodud kirjeldus selle kohta, kuidas andmete püsivust rakendatakse SQL Serveris Linuxi platvormil. Siin on mõned huvitavad võrdlused Windowsi ja Linuxi süsteemikõnede vahel. Olen peaaegu kindel, et just tänu sellele materjalile sain teada FUA optimeerimisest XFS-il.
Kas olete kunagi kaotanud andmeid, mida pidasite usaldusväärselt kettale salvestatuks?
Allikas: habr.com
