Patvarios duomenų saugyklos ir Linux failų API

Aš, tyrinėdamas duomenų saugojimo debesų sistemose stabilumą, nusprendžiau išbandyti save, įsitikinti, kad suprantu pagrindinius dalykus. aš pradėjo skaityti NVMe spec kad suprastume, kokios garantijos dėl duomenų išlikimo (ty garantijos, kad duomenys bus prieinami po sistemos gedimo), suteikia mums NMVe diskus. Padariau tokias pagrindines išvadas: reikia laikyti duomenis sugadintus nuo duomenų rašymo komandos davimo momento ir iki jų įrašymo į laikmeną. Tačiau daugumoje programų sistemos skambučiai gana saugiai naudojami duomenims įrašyti.

Šiame straipsnyje aš tyrinėju „Linux“ failų API teikiamus patvarumo mechanizmus. Atrodo, kad čia viskas turėtų būti paprasta: programa iškviečia komandą write(), o atlikus šią komandą, duomenys bus saugiai saugomi diske. Bet write() programos duomenis nukopijuoja tik į branduolio talpyklą, esančią RAM. Norint priversti sistemą įrašyti duomenis į diską, reikia naudoti kai kuriuos papildomus mechanizmus.

Patvarios duomenų saugyklos ir Linux failų API

Apskritai ši medžiaga yra pastabų rinkinys, susijęs su tuo, ką išmokau mane dominančia tema. Jei labai trumpai pakalbėtume apie svarbiausius, paaiškėtų, kad norint organizuoti tvarų duomenų saugojimą, reikia naudoti komandą fdatasync() arba atidarykite failus su vėliavėle O_DSYNC. Jei norite sužinoti daugiau apie tai, kas nutinka duomenims pakeliui iš kodo į diską, pažiūrėkite tai straipsnis.

Write() funkcijos naudojimo ypatybės

Sisteminis skambutis write() apibrėžta standarte IEEE POSIX kaip bandymas įrašyti duomenis į failo aprašą. Sėkmingai atlikus darbus write() duomenų skaitymo operacijos turi grąžinti tiksliai tuos baitus, kurie buvo įrašyti anksčiau, net jei duomenys pasiekiami iš kitų procesų ar gijų (čia atitinkamą POSIX standarto skyrių). Čia, skyriuje apie gijų sąveiką su įprastomis failų operacijomis, yra pastaba, nurodanti, kad jei dvi gijos iškviečia šias funkcijas, kiekvienas iškvietimas turi matyti visas nurodytas pasekmes, kurias sukelia kitas skambutis, arba ne. nemato jokių pasekmių. Tai leidžia daryti išvadą, kad visos failo įvesties / išvesties operacijos turi būti užblokuotos dirbamame išteklyje.

Ar tai reiškia, kad operacija write() yra atominis? Žvelgiant iš techninės pusės, taip. Duomenų skaitymo operacijos turi grąžinti viską, kas buvo parašyta, arba nieko write(). Bet operacija write(), pagal standartą, neturi baigtis, užsirašius viską, ko buvo paprašyta užrašyti. Leidžiama įrašyti tik dalį duomenų. Pavyzdžiui, galime turėti du srautus, kurių kiekvienas prideda po 1024 baitus prie failo, aprašyto tuo pačiu failo aprašu. Standarto požiūriu rezultatas bus priimtinas, kai kiekviena iš rašymo operacijų prie failo gali pridėti tik vieną baitą. Šios operacijos išliks atominės, tačiau jas atlikus į failą įrašyti duomenys bus sumaišyti. Čia labai įdomi diskusija šia tema „Stack Overflow“.

fsync() ir fdatasync() funkcijos

Lengviausias būdas išplauti duomenis į diską yra iškviesti funkciją fsync (). Ši funkcija prašo operacinės sistemos perkelti visus pakeistus blokus iš talpyklos į diską. Tai apima visus failo metaduomenis (prieigos laiką, failo modifikavimo laiką ir pan.). Manau, kad šie metaduomenys retai reikalingi, todėl jei žinote, kad jie jums nėra svarbūs, galite naudoti funkciją fdatasync(). Į padėti apie fdatasync() jame rašoma, kad šios funkcijos veikimo metu į diską išsaugomas toks metaduomenų kiekis, kuris „reikalingas norint teisingai atlikti sekančias duomenų nuskaitymo operacijas“. Ir būtent tai rūpi daugumai programų.

Viena problema, kuri gali kilti, yra ta, kad šie mechanizmai negarantuoja, kad failas gali būti rastas po galimo gedimo. Visų pirma, kai sukuriamas naujas failas, reikia skambinti fsync() katalogui, kuriame jis yra. Priešingu atveju po gedimo gali pasirodyti, kad šio failo nėra. Taip yra dėl to, kad naudojant UNIX, naudojant kietąsias nuorodas, failas gali egzistuoti keliuose kataloguose. Todėl skambinant fsync() failas negali žinoti, kurio katalogo duomenys taip pat turi būti išplauti į diską (čia galite perskaityti daugiau apie tai). Atrodo, kad ext4 failų sistema gali automatiškai taikyti fsync() į katalogus, kuriuose yra atitinkami failai, tačiau to gali nebūti kitose failų sistemose.

Šis mechanizmas skirtingose ​​failų sistemose gali būti įgyvendintas skirtingai. aš naudojau blktrace Norėdami sužinoti, kokios disko operacijos naudojamos ext4 ir XFS failų sistemose. Abu išduoda įprastas įrašymo komandas į diską ir failų turiniui, ir failų sistemos žurnalui, išplauna talpyklą ir išeina, atlikdami FUA (Force Unit Access, duomenų įrašymas tiesiai į diską, apeinant talpyklą) įrašymą į žurnalą. Tikriausiai jie tai daro norėdami patvirtinti sandorio faktą. Diskuose, kurie nepalaiko FUA, tai sukelia du talpyklos praplovimus. Mano eksperimentai tai parodė fdatasync() šiek tiek greičiau fsync(). Naudingumas blktrace tai rodo fdatasync() paprastai į diską įrašo mažiau duomenų (ext4 fsync() rašo 20 KiB, ir fdatasync() - 16 KiB). Be to, sužinojau, kad XFS yra šiek tiek greitesnis nei ext4. Ir čia su pagalba blktrace pavyko tai išsiaiškinti fdatasync() į diską išplaunama mažiau duomenų (4 KiB XFS).

Dviprasmiškos situacijos naudojant fsync()

Galiu galvoti apie tris dviprasmiškas situacijas fsync()su kuriais susidūriau praktikoje.

Pirmasis toks incidentas įvyko 2008 m. Tuo metu „Firefox 3“ sąsaja „užstojo“, jei į diską buvo įrašoma daug failų. Problema buvo ta, kad įdiegiant sąsają buvo naudojama SQLite duomenų bazė informacijai apie jos būseną saugoti. Po kiekvieno pakeitimo, kuris įvyko sąsajoje, funkcija buvo iškviesta fsync(), kuris suteikė geras stabilaus duomenų saugojimo garantijas. Tuo metu naudotoje ext3 failų sistemoje funkcija fsync() išplauti į diską visus „nešvarius“ sistemos puslapius, o ne tik tuos, kurie buvo susiję su atitinkamu failu. Tai reiškė, kad spustelėjus mygtuką „Firefox“ į magnetinį diską gali būti įrašyti megabaitai duomenų, o tai gali užtrukti daug sekundžių. Problemos sprendimas, kiek supratau jis medžiaga, buvo perkelti darbą su duomenų baze į asinchronines fonines užduotis. Tai reiškia, kad „Firefox“ anksčiau taikė griežtesnius saugyklos patvarumo reikalavimus, nei buvo iš tikrųjų būtina, o „ext3“ failų sistemos funkcijos šią problemą tik pablogino.

Antroji problema įvyko 2009 m. Tada, po sistemos gedimo, naujosios ext4 failų sistemos naudotojai pastebėjo, kad daugelis naujai sukurtų failų buvo nulinio ilgio, tačiau tai neįvyko su senesne ext3 failų sistema. Ankstesnėje pastraipoje kalbėjau apie tai, kaip ext3 išmetė per daug duomenų į diską, o tai labai sulėtino. fsync(). Siekdamas pagerinti situaciją, ext4 išplauna tik tuos „nešvarius“ puslapius, kurie yra susiję su konkrečia byla. O kitų failų duomenys išlieka atmintyje daug ilgiau nei su ext3. Tai buvo padaryta siekiant pagerinti našumą (pagal numatytuosius nustatymus duomenys išlieka tokios būsenos 30 sekundžių, galite tai sukonfigūruoti naudodami purvinas_galiojimo_centisecs; čia galite rasti daugiau informacijos apie tai). Tai reiškia, kad po gedimo gali būti negrįžtamai prarastas didelis duomenų kiekis. Šios problemos sprendimas yra naudoti fsync() programose, kurios turi užtikrinti stabilią duomenų saugyklą ir kiek įmanoma labiau apsaugoti jas nuo gedimų pasekmių. Funkcija fsync() veikia daug efektyviau su ext4 nei su ext3. Šio metodo trūkumas yra tas, kad naudojant jį, kaip ir anksčiau, sulėtėja kai kurios operacijos, pavyzdžiui, programų diegimas. Žr. išsamią informaciją apie tai čia и čia.

Trečioji problema, susijusi su fsync(), atsirado 2018 m. Tada, įgyvendinant PostgreSQL projektą, buvo išsiaiškinta, kad jei funkcija fsync() aptinkama klaida, ji pažymi „nešvarius“ puslapius kaip „švarius“. Dėl to šie skambučiai fsync() nieko nedaryti su tokiais puslapiais. Dėl šios priežasties modifikuoti puslapiai išsaugomi atmintyje ir niekada neįrašomi į diską. Tai tikra nelaimė, nes programa manys, kad kai kurie duomenys įrašyti į diską, bet iš tikrųjų taip nebus. Tokios nesėkmės fsync() yra retai, taikymas tokiose situacijose beveik nieko negali padėti išspręsti problemą. Šiomis dienomis, kai taip nutinka, PostgreSQL ir kitos programos sugenda. Čia, straipsnyje "Ar programos gali atsigauti po fsync gedimų?", ši problema išsamiai išnagrinėta. Šiuo metu geriausias šios problemos sprendimas yra naudoti tiesioginį įvestį / išvestį su vėliavėle O_SYNC arba su vėliava O_DSYNC. Taikant šį metodą, sistema praneš apie klaidas, kurios gali atsirasti atliekant konkrečias duomenų rašymo operacijas, tačiau šis metodas reikalauja, kad programa pati tvarkytų buferius. Skaitykite daugiau apie tai čia и čia.

Failų atidarymas naudojant O_SYNC ir O_DSYNC vėliavėles

Grįžkime prie diskusijos apie Linux mechanizmus, kurie užtikrina nuolatinį duomenų saugojimą. Būtent, mes kalbame apie vėliavos naudojimą O_SYNC arba vėliava O_DSYNC atidarant failus naudojant sistemos skambutį atviras(). Taikant šį metodą, kiekviena duomenų įrašymo operacija atliekama tarsi po kiekvienos komandos write() sistemai duodamos atitinkamai komandos fsync() и fdatasync(). Į POSIX specifikacijos tai vadinama „sinchronizuoto I/O failo vientisumo užbaigimu“ ir „duomenų vientisumo užbaigimu“. Pagrindinis šio metodo privalumas yra tas, kad norint užtikrinti duomenų vientisumą reikia atlikti tik vieną sistemos iškvietimą, o ne du (pvz. write() и fdatasync()). Pagrindinis šio metodo trūkumas yra tas, kad visos rašymo operacijos naudojant atitinkamą failo aprašą bus sinchronizuojamos, o tai gali apriboti galimybę struktūrizuoti programos kodą.

Tiesioginio I/O naudojimas su O_DIRECT vėliava

Sisteminis skambutis open() palaiko vėliavą O_DIRECT, kuris skirtas apeiti operacinės sistemos talpyklą, atlikti įvesties / išvesties operacijas, tiesiogiai sąveikaujant su disku. Tai daugeliu atvejų reiškia, kad programos išleistos rašymo komandos bus tiesiogiai išverstos į komandas, skirtas dirbti su disku. Tačiau apskritai šis mechanizmas nepakeičia funkcijų fsync() arba fdatasync(). Faktas yra tas, kad pats diskas gali delsa arba talpykla atitinkamas komandas duomenims rašyti. Ir, dar blogiau, kai kuriais ypatingais atvejais įvesties / išvesties operacijos atliekamos naudojant vėliavėlę O_DIRECT, transliacija į tradicines buferines operacijas. Lengviausias būdas išspręsti šią problemą yra naudoti vėliavėlę failams atidaryti O_DSYNC, o tai reikš, kad po kiekvienos rašymo operacijos bus iškvietimas fdatasync().

Paaiškėjo, kad XFS failų sistema neseniai pridėjo „greitąjį kelią“. O_DIRECT|O_DSYNC- duomenų įrašai. Jei blokas perrašomas naudojant O_DIRECT|O_DSYNC, tada XFS, užuot išvalęs talpyklą, vykdys FUA rašymo komandą, jei įrenginys ją palaiko. Aš tai patikrinau naudodamas įrankį blktrace Linux 5.4 / Ubuntu 20.04 sistemoje. Šis metodas turėtų būti veiksmingesnis, nes į diską įrašomas minimalus duomenų kiekis ir atliekama viena operacija, o ne dvi (rašymas ir talpyklos išvalymas). Radau nuorodą į pleistras 2018 branduolys, įgyvendinantis šį mechanizmą. Yra keletas diskusijų apie šio optimizavimo pritaikymą kitoms failų sistemoms, tačiau, kiek žinau, XFS kol kas yra vienintelė ją palaikanti failų sistema.

sync_file_range() funkcija

Linux turi sistemos iškvietimą sync_file_range(), kuri leidžia į diską nuplauti tik dalį failo, o ne visą failą. Šis skambutis inicijuoja asinchroninį praplovimą ir nelaukia, kol jis bus baigtas. Tačiau nuorodoje į sync_file_range() sakoma, kad ši komanda yra „labai pavojinga“. Nerekomenduojama jo naudoti. Savybės ir pavojai sync_file_range() labai gerai aprašyta tai medžiaga. Atrodo, kad šis iškvietimas naudoja „RocksDB“, kad kontroliuotų, kada branduolys išplauna „nešvarius“ duomenis į diską. Tačiau tuo pačiu metu, siekiant užtikrinti stabilų duomenų saugojimą, jis taip pat naudojamas fdatasync(). Į kodą RocksDB turi keletą įdomių komentarų šia tema. Pavyzdžiui, tai atrodo kaip skambutis sync_file_range() naudojant ZFS, duomenys į diską neįleidžiami. Patirtis rodo, kad retai naudojamame kode gali būti klaidų. Todėl patarčiau nenaudoti šio sistemos skambučio, nebent tai absoliučiai būtina.

Sistemos skambučiai, padedantys užtikrinti duomenų pastovumą

Padariau išvadą, kad yra trys būdai, kuriais galima atlikti nuolatines įvesties / išvesties operacijas. Jiems visiems reikalingas funkcijos iškvietimas fsync() katalogui, kuriame buvo sukurtas failas. Tai yra būdai:

  1. Funkcijos skambutis fdatasync() arba fsync() po funkcijos write() (geriau naudoti fdatasync()).
  2. Darbas su failo aprašu, atidarytu vėliavėle O_DSYNC arba O_SYNC (geriau - su vėliava O_DSYNC).
  3. Komandų naudojimas pwritev2() su vėliava RWF_DSYNC arba RWF_SYNC (geriausia su vėliavėle RWF_DSYNC).

Veiklos pastabos

Aš kruopščiai neįvertinau įvairių tirtų mechanizmų veikimo. Skirtumai, kuriuos pastebėjau jų darbo greičiu, yra labai nedideli. Tai reiškia, kad galiu klysti ir kad kitomis sąlygomis tas pats dalykas gali rodyti skirtingus rezultatus. Pirmiausia pakalbėsiu apie tai, kas veikia našumą labiau, o paskui – apie tai, kas mažiau veikia našumą.

  1. Failo duomenų perrašymas yra greitesnis nei duomenų pridėjimas prie failo (našumo padidėjimas gali būti 2–100%). Norint pridėti duomenis prie failo, reikia atlikti papildomų failo metaduomenų pakeitimų, net ir po sistemos iškvietimo fallocate(), tačiau šio poveikio mastas gali skirtis. Rekomenduoju, kad geriausiai veiktų, paskambinti fallocate() iš anksto paskirstyti reikiamą erdvę. Tada ši erdvė turi būti aiškiai užpildyta nuliais ir iškviesta fsync(). Dėl to atitinkami failų sistemos blokai bus pažymėti kaip „paskirstyti“, o ne „nepaskirstyti“. Tai suteikia nedidelį (apie 2%) našumo pagerėjimą. Be to, kai kurie diskai gali turėti lėtesnę prieigą prie pirmojo bloko nei kiti. Tai reiškia, kad užpildžius erdvę nuliais, galima žymiai (apie 100%) pagerinti našumą. Visų pirma tai gali atsitikti su diskais. AWS EBS (tai neoficialūs duomenys, jų patvirtinti negalėjau). Tas pats pasakytina ir apie saugojimą. GCP nuolatinis diskas (ir tai jau oficiali informacija, patvirtinta testais). Tą patį padarė ir kiti ekspertai stebėjimassusiję su skirtingais diskais.
  2. Kuo mažiau sistemos skambučių, tuo didesnis našumas (padidėjimas gali būti apie 5%). Tai atrodo kaip skambutis open() su vėliava O_DSYNC arba skambinti pwritev2() su vėliava RWF_SYNC greitesnis skambutis fdatasync(). Įtariu, kad čia esmė ta, kad taikant šį metodą tam tikrą vaidmenį vaidina tai, kad norint išspręsti tą pačią užduotį reikia atlikti mažiau sistemos iškvietimų (vienas skambutis vietoj dviejų). Tačiau našumo skirtumas yra labai mažas, todėl galite lengvai jo nepaisyti ir programoje naudoti ką nors, kas nesukelia jos logikos komplikacijų.

Jei jus domina tvaraus duomenų saugojimo tema, čia yra keletas naudingų medžiagų:

  • I/O prieigos metodai — įvesties/išvesties mechanizmų pagrindų apžvalga.
  • Užtikrinti, kad duomenys pasiektų diską - pasakojimas apie tai, kas nutinka duomenims pakeliui iš programos į diską.
  • Kada reikia fsinchronizuoti katalogą, kuriame yra – atsakymas į klausimą, kada kreiptis fsync() katalogams. Trumpai tariant, paaiškėja, kad tai reikia padaryti kuriant naują failą, o šios rekomendacijos priežastis yra ta, kad Linux sistemoje gali būti daug nuorodų į tą patį failą.
  • SQL serveris „Linux“: FUA vidiniai elementai - Čia yra aprašas, kaip nuolatinis duomenų saugojimas yra įdiegtas SQL Server Linux platformoje. Čia yra keletas įdomių Windows ir Linux sistemos skambučių palyginimų. Esu beveik tikras, kad būtent šios medžiagos dėka sužinojau apie XFS optimizavimą FUA.

Ar kada nors praradote duomenis, kurie, jūsų manymu, buvo saugiai saugomi diske?

Patvarios duomenų saugyklos ir Linux failų API

Patvarios duomenų saugyklos ir Linux failų API

Šaltinis: www.habr.com