Usaldusväärne andmete ja failide salvestamine Linuxis

Uurides andmete säilitamise usaldusväärsust pilvesüsteemides, otsustasin end proovile panna ja veenduda, et mõistan põhiasju. Ma alustasin NVMe spetsifikatsiooni lugemisega et mõista, milliseid garantiisid pakuvad NMVe kettad seoses andmete usaldusväärse säilitamisega (st garanteerides, et andmed on süsteemi rikke korral kergesti kättesaadavad). Minu põhijäreldused olid järgmised: andmeid tuleb pidada kahjustatuks hetkest, kui on antud andmete kirjutamise käsk, kuni nende täieliku kirjutamiseni salvestusseadmesse. Kuid enamikus andmete kirjutamise programmides kasutatakse süsteemi kutseid täiesti rahulikult.

Selles materjalis uurin Linuxi failide API-de kaudu pakutavaid andmete usaldusväärse säilitamise mehhanisme. Tundub, et siin peaks kõik olema lihtne: programm kutsub välja käsu write(), ja kui selle käsu töö on lõpetatud, on andmed usaldusväärselt salvestatud kettale. Kuid write() ainult kopeerib rakenduse andmed tuuma vahemällu, mis asub juhuslikus mälus. Kettale andmete kirjutamise sundimiseks on vaja kasutada mõningaid täiendavaid mehhanisme.

Usaldusväärne andmete ja failide salvestamine Linuxis

Kokkuvõttes on see materjal märkmete kogum, mis käsitleb, mida olen õppinud mind huvitavast teemast. Kui rääkida kõige olulisemast väga lühidalt, siis tuleb andmete usaldusväärse salvestamise organiseerimiseks kasutada käsku fdatasync() või avada faile lipuga O_DSYNC. Kui olete huvitatud, et teada saada, mis täpselt juhtub andmetega programmikoodist kuni ketaseni, vaadake seda artiklit.

Funktsiooni write() kasutamise omadused

Süsteemikõne write() on määratletud standardis IEEE POSIX kui katse andmete kirjutamiseks failikirjeldajasse. Pärast edukat täitmist write() peavad andmete lugemise operatsioonid tagastama täpselt need baitid, mis olid enne kirjutatud, isegi juhul, kui andmetele pääsevad juurde teised protsessid või lõimud (siin on vastav jaotis POSIX standardist). Siit, jaotises, mis käsitleb voogude suhete loomist tavaliste failitoimetamistega, on märge, mille kohaselt kui iga voog kutsub neid funktsioone, peab iga kutse nägema kas kõiki määratud tagajärgi, mis tulenevad teise kutse täitmisest, või mitte nägema üldse mingeid tagajärgi. See toetab järeldust, et kõik sisendi/väljundi failitoimingud peavad hoidma ressurssi, millega nad töötavad, lukustatud.

Kas see tähendab, et operatsioon write() on atomaarne? Tehnilisest vaatepunktist - jah. Andmete lugemise operatsioonid peavad tagastama kas kõik või mitte midagi, mis on kirjutatud kasutades write(). Kuid operatsioon write(), vastavalt standardile ei pea see tingimata lõpetama, kirjutades kogu, mis on talle ette nähtud. Tal on lubatud salvestada ainult osa andmetest. Näiteks võib meil olla kaks voogu, igaüks neist lisab 1024 baidi faili, mis on kirjeldatud sama failihandleriga. Standardi seisukohalt on vastuvõetav tulemus, kui iga salvestusoperatsioon suudab faili lisada vaid ühe baidi. Need operatsioonid jäävad aatomaarseteks, kuid pärast nende lõpetamist on failis salvestatud andmed omavahel segamini. Siin on selle teema kohta on väga huvitav arutelu Stack Overflow's.

Funktsioonid fsync() ja fdatasync()

Lihtsaim viis andmete diskile kirjutamiseks on funktsiooni väljakutse fsync(). See funktsioon palub operatsioonisüsteemil viia kõik muudetud plokid vahemälust diskile. See hõlmab ka kõiki failide metaandmeid (juurdepääsu aeg, faili muutmise aeg jne). Arvan, et nende metaandmete vajadus tekib harva, seega kui teate, et need ei ole teie jaoks olulised, võite kasutada funktsiooni fdatasync(). Dokumendihalduses abi kohta fdatasync() On väidetud, et selle funktsiooni töötamise käigus salvestatakse kettale sellises mahus metaandmeid, mis on vajalik järgmiste andmelugemise toimingute õigeaegseks täitmiseks. See on just see, mis enamikku rakendusi huvitab.

Üks probleem, mis siin võib tekkida, on see, et need mehhanismid ei garanteeri, et faili saab pärast võimalikke tõrkeid leida. Eriti siis, kui luuakse uus fail, tuleb kutsuda fsync() kausta, mis seda sisaldab. Vastasel juhul võib pärast tõrget selguda, et see fail ei eksisteeri. Selle põhjuseks on see, et UNIXis, jäigade linkide kasutamise tõttu, võib fail eksisteerida mitmes kaustas. Seetõttu kutsudes fsync() faili puhul ei ole võimalik teada saada, millise kausta andmeid tuleb samuti kettale salvestada (siin selle kohta saab lugeda rohkem). Tundub, et failisüsteem ext4 on suuteline automaatselt rakendama fsync() kaustadele, mis sisaldavad vastavaid faile, kuid teiste failisüsteemide puhul ei pruugi see nii olla.

See mehhanismi rakendamine võib erinevates failisüsteemides erineda. Kasutasin blktrace , et teada saada, milliseid kettaoperatsioone kasutatakse failisüsteemides ext4 ja XFS. Mõlemad väljastavad tavapärased ketta kirjutamise käsud, nii failisisu osas kui ka failisüsteemi logis, tühjendavad vahemälu ja lõpetavad töö, tehes FUA-kirjutuse (Force Unit Access, andmete kirjutamine otse kettale, mööda vahemälu) logisse. Tõenäoliselt käituvad nad nii operatsiooni kinnitamise tagamiseks. Ketastel, mis ei toeta FUA-d, põhjustab see kahe vahemälu tühjendamise. Minu katsed on näidanud, et fdatasync() veidi kiiremini fsync(). Tööriist blktrace näitab, et fdatasync() tavaliselt kirjutab 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 abil blktrace sain teada, et fdatasync() tühjendab kettale vähem andmeid (4 KiB XFS).

Kahtlased olukorrad, mis tekivad fsync() kasutamisel

Mäletan kolme kahtlast olukorda, millega olen praktikas silmitsi seisnud. fsync(), millega ma praktikas kokku puutusin.

Esimene selline juhtum leidis aset 2008. aastal. Sel ajal 'riputas' Firefox 3 liides, kui salvestati suure arvu failide andmeid kettale. Probleem oli seotud sellega, et liidese olekuandmete talletamiseks kasutati SQLite andmebaasi. Iga kord, kui liideses toimus muutus, kutsuti välja funktsioon fsync(), mis pakkus head garantiid andmete stabiilse salvestamise kohta. Toona kasutusel olnud ext3 failisüsteemis funktsioon fsync() salvestas kettale kõik 'mädanenud' lehed süsteemis, mitte ainult need, mis olid seotud vastava failiga. See tähendas, et Firefoxi nuppudele klõpsamine võis alustada megabaitide andmete salvestamist magnetkettale, mis võis kesta mitu sekundit. Probleemi lahendus, nii nagu ma aru sain sellest materjalist, seisnes andmebaasiga töötamise üleviimises asünkroonsetesse taustategevustesse. See tähendab, et varem rakendati Firefoxis rangemaid nõudeid andmete stabiilse salvestamise osas, kui see reaalselt vajalik oli, ja ext3 failisüsteemi eripära süvendas seda probleemi.

Teine probleem tekkis 2009. aastal. Pärast süsteemi riket sattusid uue ext4 failisüsteemi kasutajad olukorda, kus paljud hiljuti loodud failid olid nullpikkusega, samas kui vanema ext3 failisüsteemi puhul seda ei juhtunud. Eelnevas lõigus rääkisin, et ext3 salvestas kettale liiga palju andmeid, mis aeglustas tööd märkimisväärselt. fsync()Selle olukorra parandamiseks salvestatakse ext4 failile kettale vaid need 'mustad' leheküljed, mis on seotud konkreetse failiga. Ülejäänud failide andmed jäävad mällu palju pikemaks ajaks kui ext3 kasutamisel. See tehti jõudluse parandamise nimel (vaikimisi jäävad andmed sellesse olekusse 30 sekundiks, seda saab seadistada koos dirty_expire_centisecs; siin leiate täiendavaid materjale selle kohta). See tähendab, et suur hulk andmeid võib pärast rikete tekkimist pöördumatult kaduma minna. Selle probleemi lahenduseks on fsync() rakendustes, mis peavad tagama andmete stabiilse säilitamise ja maksimaalselt nende kaitsmise rikete tagajärgede eest. Funktsioon fsync() ext4 töötab märksa tõhusamalt kui ext3. Selle lähenemise miinus on aga see, et selle kasutamine aeglustab teatud toimingute, näiteks programmide installimise, teostamist. Lisainfot leiate siit siit ja siit.

Kolmas probleem, mis puudutab fsync(), tekkis 2018. aastal. Siis selgus PostgreSQL projekti raames, et kui funktsioon fsync() kohtub veaga, märgib ta 'mustad' lehed kui 'puhtad'. Selle tulemusena ei tehta järgmiste kõnede fsync() selliste lehtedega midagi. Selle tõttu jäävad muudetud lehed mällu ja neid ei kirjutata kunagi kettale. See on tõeline katastroof, kuna rakendus arvab, et andmed on kettale kirjutatud, kuid tegelikult see nii ei ole. Sellised tõrked fsync() on haruldased, rakendus ei suuda sellistes olukordades peaaegu midagi teha probleemi lahendamiseks. Tänapäeval, kui see juhtub, PostgreSQL ja teised rakendused lõpetavad äkki töö. Siit, artiklis 'Kas rakendused saavad fsync tõrgetest taastuda?', uuritakse seda probleemi kõigis detailides. Praegu on parim lahendus selle probleemi lahendamiseks Direct I/O kasutamine koos lipuga O_SYNC või lipuga O_DSYNC. Selle lähenemise korral teatab süsteem vigadest, mis võivad tekkida konkreetsete andmete kirjutamise toimingute käigus, kuid see nõuab, et rakendus haldaks puhverdusi ise. Lisateavet leiate siit siit ja siit.

Failide avamine O_SYNC ja O_DSYNC lipukestega

Naaseme arutelule Linuxi mehhanismide üle, mis tagavad andmete järjepideva salvestamise. Eelkõige räägime me lipu kasutamisest O_SYNC või lipust O_DSYNC failide avamisel süsteemikõne kaudu open(). Selle lähenemisega viiakse iga andme kirjutamise toiming läbi nagu pärast iga käsku antaks süsteemile vastavad käsud write() POSIX spetsifikatsioonide fsync() ja fdatasync(). Dokumendihalduses seda nimetatakse „Sünkroonitud I/O Faili Terviklikkuse Täitmine“ ja „Andmete Terviklikkuse Täitmine“. Selle lähenemise peamine eelis on see, et andmete terviklikkuse tagamiseks tuleb teha vaid üks süsteemikõne, mitte kaks (näiteks — ). Selle lähenemise peamine puudus on see, et kõik kirjutamisoperatsioonid, mis kasutavad vastavat failide kirjeldajat, synchroniseeritakse, mis võib piirata rakenduse koodi struktureerimise võimalusi. write() ja fdatasync()). Selle lähenemise peamine puudus on see, et kõik kirjutamise operatsioonid, mis kasutavad vastavat faili deskriptorit, saavad olema sünkroniseeritud, mis võib piirata rakenduse koodistruktuuri loomise võimalusi.

Direct I/O kasutamine O_DIRECT lipuga

Süsteemikõne open() toetab lippu O_DIRECT, mis on mõeldud selleks, et vältida operatsioonisüsteemi vahemälu, teostades sisendi-väljundi toiminguid otse kettaga. See tähendab, et paljusid juhtumeid, kirjutamiskäsklusi, mis on antud programmile, edastatakse otse kettale suunatud käsklusteks. Kuid üldiselt ei asenda see mehhanism funktsioone fsync() või fdatasync(). Asi on selles, et ise ketas võib edasi lükata või vahemäluda vastavaid andmete kirjutamise käske. Ja mis veelgi hullem, mõningatel erijuhtudel, sisendi-väljundi toimingud, mis teostatakse lippu kasutades O_DIRECT, tõlgitakse traditsioonilisteks puhverdatud toiminguteks. Selle probleemi lahendamine on lihtsaim viis avada failid ka lipuga O_DSYNC, mis tähendab, et iga kirjutamistoiminguga kaasneb kutse fdatasync().

Leiti, et failisüsteemis XFS on hiljuti lisatud "kiire tee" O_DIRECT|O_DSYNC-andmete kirjutamiseks. Kui kirjutatakse üle plokk, kasutades O_DIRECT|O_DSYNC, siis XFS, asemel vahemälu tühjendamise, täidab FUA-kirje käsku, kui seade seda toetab. Olen selle kinnitanud tööriista kasutamisega blktrace Linuxi süsteemis 5.4/Ubuntu 20.04. See lähenemine peaks olema efektiivsem, kuna selle kasutamisel kirjutatakse kettale minimaalne kogus andmeid ja kasutatakse ühte toimingut, mitte kahte (kirjutamine ja vahemälu tühjendamine). Leidsin viite plekk 2018. aasta tuuma, kus see mehhanism on rakendatud. Seal on arutelu, mis käsitleb selle optimeerimise rakendamist ka teistes failisüsteemides, kuid niipalju kui mina tean, on XFS praegu ainus failisüsteem, mis seda toetab.

sync_file_range()

Linuxis on süsteemi kõne sync_file_range(), mis võimaldab kirjutada kettale ainult osa failist, mitte kogu faili. See kõne algatab asünkroonse andmete kirjutamise ja ei oota selle lõpetamist. Kuid juhendis sync_file_range() öeldakse, et käsk on "väga ohtlik". Selle kasutamine ei ole soovitatav. Eripära ja ohtlikud aspektid sync_file_range() on väga hästi kirjeldatud selles materjalis. Eriti paistab silma, et see kutse kasutab RocksDB-d selleks, et hallata, millal südamik "räpane" teave kettale kirjutab. Kuid selleks, et andmed oleksid stabiilselt salvestatud, kasutatakse ka fdatasync(). Dokumendihalduses koodis RocksDB-s on selle teema kohta huvitavad kommentaarid. Näiteks tundub, et kutse sync_file_range() ZFS-i kasutamisel ei põhjusta andmete kirjutamist kettale. Minu kogemus ütleb, et harva kasutatav kood võib sisaldada vigu. Seetõttu soovitaksin seda süsteemikutsumist vältida, kui see ei ole hädavajalik.

Süsteemikutsed, mis aitavad tagada andmete stabiilse salvestamise

Jõudsin järeldusele, et andmete stabiilse salvestamise tagamiseks saab kasutada kolme lähenemisviisi. Kõik need nõuavad funktsiooni väljakutset fsync() kaustas, kus fail on loodud. Siin on need lähenemisviisid:

  1. Funktsiooni väljakutse fdatasync() või fsync() pärast funktsiooni write() (soovitatav on kasutada fdatasync()).
  2. Failideskirpti kasutamine, mis on avatud lipuga O_DSYNC või O_SYNC (parem — lipuga O_DSYNC).
  3. Käskluse kasutamine pwritev2() lipu RWF_DSYNC või RWF_SYNC (eelistatult — lipuga RWF_DSYNC).

Toimivuse märkmed

Ma ei ole teadlikult mõõtnud erinevate uuritud mehhanismide jõudlust. Minu tähele pandud kiiruseread on üsna väiksed. See tähendab, et ma võin eksida ja et samade tingimuste korral võivad tulemused olla erinevad. Esiteks räägin sellest, mis mõjutab jõudlust kõige enam, ja seejärel, mis mõjutab jõudlust vähem.

  1. Faili andmete üle kirjutamine on kiirem kui andmete lisamine failile (jõudluse kasum võib ulatuda 2–100%). Failile andmete lisamine nõuab lisamuudatusi faili metaandmetes, isegi pärast süsteemikõnet, fallocate(), kuid selle efekti ulatus võib varieeruda. Soovitan parima jõudluse tagamiseks kutsuda fallocate() välja vajaliku ruumi eelnevalt eraldamiseks. Siis tuleb see ruum selgesõnaliselt nullidega täita ja kutsuda fsync(). Tänu sellele märgistatakse vastavad plokid failisüsteemis kui "valitud", mitte kui "mittevalitud". See toob kaasa väikese (umbes 2%) jõudluse paranemise. Lisaks sellele võivad mõned kettad esimesel ploki ligipääsu operatsioonil olla aeglasemad kui teised. See tähendab, et ruumi täitmine nullidega võib tuua kaasa märkimisväärse (umbes 100%) jõudluse paranemise. Eelkõige võib see juhtuda ketastega AWS EBS (need on mitteformaalne andmed, neid ei ole võimalik kinnitada). Sama kehtib ka salvestuste kohta. GCP Persistent Disk (see on juba ametlik teave, kinnitatud katsetega). Teised spetsialistid on teinud sarnaseid täheldusi, mis on seotud erinevate ketastega.
  2. Mida vähem on süsteemikõnesid, seda kõrgem on jõudlus (kasv võib olla umbes 5%). Tundub, et kõne open() lipu O_DSYNC või kõne pwritev2() lipu RWF_SYNC on kiiremini kui kõne fdatasync()Kahtlen, et asi on selles, et sellise lähenemise puhul mängib rolli, et ühe ja sama ülesande täitmiseks tuleb teha vähem süsteemi kõnesid (üks kõne kahe asemel). Kuid jõudluse erinevus on väga väike, seega võite selle täiesti tähelepanuta jätta ja kasutada rakenduses seda, mis ei too kaasa selle loogika keerukuse suurenemist.

Kui teid huvitab andmete usaldusväärne salvestamine, siis siin on mõned kasulikud materjalid:

  • I/O sisenemismeetodid — ülevaade sisenemise ja väljumise mehhanismidest.
  • Andmete jõudmine kettale — jutt sellest, mis juhtub andmetega rakendusest ketta suunas.
  • Millal peaksite fsync'i rakendama sisaldavale kaustale — vastus küsimusele, millal tuleb seda rakendada fsync() kaustade jaoks. Kui seda kahe sõnaga rääkida, selgub, et seda tuleks teha uue faili loomisel, ja põhjuseks on see, et Linuxis võib olla palju linke ühe ja sama faili jaoks.
  • SQL Server Linuxil: FUA sisemised seadmed — siin on kirjeldus, kuidas andmete püsiv hoidmine on realiseeritud SQL Serveris Linuxi platvormil. Siin on tehtud mõned huvitavad võrdlused Windowsi ja Linuxi süsteemikutsungite vahel. Olen peaaegu kindel, et just selle materjali kaudu õppisin FUA optimeerimisest XFS-s.

Kas olete kunagi kaotanud andmeid, mida pidasite kindlalt kettale salvestatuks?

Usaldusväärne andmete ja failide salvestamine Linuxis

Usaldusväärne andmete ja failide salvestamine Linuxis

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster