Ruajtja e qëndrueshme e të dhënave dhe API-të e skedarëve Linux

Ndërsa hulumtoja qëndrueshmërinë e ruajtjes së të dhënave në sistemet cloud, vendosa të testoja veten për t'u siguruar që i kuptoja gjërat themelore. I filloi duke lexuar specifikimet NVMe për të kuptuar se çfarë garancish në lidhje me ruajtjen e qëndrueshme të të dhënave (d.m.th., garanci që të dhënat do të jenë të disponueshme pas një dështimi të sistemit) na japin disqet NMVe. Bëra këto përfundime kryesore: të dhënat duhet të konsiderohen të dëmtuara që nga momenti kur jepet komanda për të shkruar të dhënat deri në momentin kur ato shkruhen në mediumin e ruajtjes. Megjithatë, shumica e programeve përdorin me kënaqësi thirrjet e sistemit për të regjistruar të dhëna.

Në këtë postim, unë eksploroj mekanizmat e ruajtjes së vazhdueshme të ofruara nga API-të e skedarëve Linux. Duket se gjithçka duhet të jetë e thjeshtë këtu: programi thërret komandën write(), dhe pasi të përfundojë kjo komandë, të dhënat do të ruhen në mënyrë të sigurt në disk. Por write() kopjon vetëm të dhënat e aplikacionit në cache-in e kernelit të vendosur në RAM. Për ta detyruar sistemin të shkruajë të dhëna në disk, duhet të përdorni disa mekanizma shtesë.

Ruajtja e qëndrueshme e të dhënave dhe API-të e skedarëve Linux

Në përgjithësi, ky material është një koleksion shënimesh në lidhje me atë që kam mësuar për një temë me interes për mua. Nëse flasim shumë shkurt për gjënë më të rëndësishme, rezulton se për të organizuar ruajtjen e qëndrueshme të të dhënave, duhet të përdorni komandën fdatasync() ose hapni skedarët me flamurin O_DSYNC. Nëse jeni të interesuar të mësoni më shumë rreth asaj që ndodh me të dhënat në rrugën e tyre nga kodi në disk, hidhini një sy kjo artikull.

Karakteristikat e përdorimit të funksionit write().

Thirrja e sistemit write() të përcaktuara në standard IEEE POSIX si një përpjekje për të shkruar të dhëna në një përshkrues skedari. Pas përfundimit me sukses write() Operacionet e leximit të të dhënave duhet të kthejnë saktësisht bajtet që janë shkruar më parë, duke e bërë këtë edhe nëse të dhënat aksesohen nga procese ose thread të tjerë (këtu seksioni përkatës i standardit POSIX). Këtu, në seksionin se si thread-et ndërveprojnë me operacionet normale të skedarit, ka një shënim që thotë se nëse dy thread secila thërrasin këto funksione, atëherë çdo thirrje duhet të shohë ose të gjitha pasojat e përcaktuara të thirrjes tjetër, ose asnjë. pasojat. Kjo çon në përfundimin se të gjitha operacionet hyrëse/dalëse të skedarëve duhet të mbajnë një bllokim në burimin ku po operojnë.

A do të thotë kjo se operacioni write() a është atomike? Nga pikëpamja teknike, po. Operacionet e leximit të të dhënave duhet të kthejnë ose të gjitha ose asgjë nga ajo që është shkruar me të write(). Por operacioni write(), sipas standardit, nuk duhet domosdoshmërisht të përfundojë duke shkruar gjithçka që iu kërkua të shkruante. Ajo lejohet të shkruajë vetëm një pjesë të të dhënave. Për shembull, ne mund të kemi dy thread secila duke shtuar 1024 bajt në një skedar të përshkruar nga i njëjti përshkrues skedari. Nga pikëpamja e standardit, një rezultat i pranueshëm do të jetë kur çdo operacion shkrimi mund të shtojë vetëm një bajt në skedar. Këto operacione do të mbeten atomike, por pasi të përfundojnë, të dhënat që ata kanë shkruar në skedar do të ngatërrohen. Këtu diskutim shumë interesant për këtë temë në Stack Overflow.

Funksionet fsync() dhe fdatasync().

Mënyra më e lehtë për të hedhur të dhënat në disk është thirrja e funksionit fsync (). Ky funksion i kërkon sistemit operativ të transferojë të gjitha blloqet e modifikuara nga cache në disk. Kjo përfshin të gjitha meta të dhënat e skedarit (koha e hyrjes, koha e modifikimit të skedarit, e kështu me radhë). Unë besoj se këto meta të dhëna nevojiten rrallë, kështu që nëse e dini se nuk është e rëndësishme për ju, mund të përdorni funksionin fdatasync(). Në ndihmë mbi fdatasync() Thuhet se gjatë funksionimit të këtij funksioni, një sasi e tillë meta të dhënash ruhet në disk që është "e nevojshme për ekzekutimin e saktë të operacioneve të mëposhtme të leximit të të dhënave". Dhe kjo është pikërisht ajo që i intereson shumica e aplikacioneve.

Një problem që mund të lindë këtu është se këta mekanizma nuk garantojnë që skedari do të jetë i zbulueshëm pas një dështimi të mundshëm. Në veçanti, kur krijoni një skedar të ri, duhet të telefononi fsync() për direktorinë që e përmban. Përndryshe, pas një dështimi, mund të rezultojë se ky skedar nuk ekziston. Arsyeja për këtë është se në UNIX, për shkak të përdorimit të lidhjeve të forta, një skedar mund të ekzistojë në drejtori të shumta. Prandaj, kur telefononi fsync() nuk ka asnjë mënyrë që një skedar të dijë se cilat të dhëna direktorie duhet të hidhen gjithashtu në disk (këtu Mund të lexoni më shumë për këtë). Duket sikur sistemi i skedarëve ext4 është i aftë automatikisht përdorim fsync() në drejtoritë që përmbajnë skedarët përkatës, por ky mund të mos jetë rasti me sistemet e tjera të skedarëve.

Ky mekanizëm mund të zbatohet ndryshe në sisteme të ndryshme skedarësh. une e perdora blktrace për të mësuar se cilat operacione të diskut përdoren në sistemet e skedarëve ext4 dhe XFS. Të dy lëshojnë komanda të rregullta shkrimi në disk si për përmbajtjen e skedarit ashtu edhe për ditarin e sistemit të skedarëve, pastrojnë cache-in dhe dalin duke kryer një shkrim në ditar FUA (Force Unit Access, duke shkruar të dhëna direkt në disk, duke anashkaluar cache-in). Ata ndoshta e bëjnë këtë për të konfirmuar që transaksioni ka ndodhur. Në disqet që nuk mbështesin FUA, kjo shkakton dy rrjedhje të cache-it. Eksperimentet e mia e treguan këtë fdatasync() pak më shpejt fsync(). Shërbimet blktrace tregon se fdatasync() zakonisht shkruan më pak të dhëna në disk (në ext4 fsync() shkruan 20 KiB, dhe fdatasync() - 16 KiB). Gjithashtu, zbulova se XFS është pak më i shpejtë se ext4. Dhe këtu me ndihmën blktrace arriti ta zbulonte atë fdatasync() lan më pak të dhëna në disk (4 KiB në XFS).

Situata të paqarta që lindin kur përdorni fsync()

Unë mund të mendoj për tre situata të paqarta në lidhje me fsync()të cilat i kam hasur në praktikë.

Rasti i parë i tillë ka ndodhur në vitin 2008. Pastaj ndërfaqja Firefox 3 ngriu nëse një numër i madh skedarësh shkruheshin në disk. Problemi ishte se zbatimi i ndërfaqes përdorte një bazë të dhënash SQLite për të ruajtur informacionin rreth gjendjes së tij. Pas çdo ndryshimi që ndodhi në ndërfaqe, funksioni thirrej fsync(), i cili dha garanci të mira për ruajtjen e qëndrueshme të të dhënave. Në sistemin e skedarëve ext3 të përdorur më pas, funksioni fsync() hodhi të gjitha faqet "të pista" në sistem në disk, dhe jo vetëm ato që ishin të lidhura me skedarin përkatës. Kjo do të thoshte se klikimi i një butoni në Firefox mund të shkaktonte megabajt të dhënash për t'u shkruar në një disk magnetik, gjë që mund të zgjaste shumë sekonda. Zgjidhja e problemit, me sa kuptoj unë ajo materiali ishte transferimi i punës me bazën e të dhënave në detyrat e sfondit asinkron. Kjo do të thotë se Firefox-i më parë ka zbatuar kërkesa më të rrepta për ruajtje nga sa nevojitej realisht, dhe veçoritë e sistemit të skedarëve ext3 vetëm sa e përkeqësuan këtë problem.

Problemi i dytë ka ndodhur në vitin 2009. Pastaj, pas një përplasjeje të sistemit, përdoruesit e sistemit të ri të skedarëve ext4 u përballën me faktin se shumë skedarë të krijuar rishtazi kishin gjatësi zero, por kjo nuk ndodhi me sistemin më të vjetër të skedarëve ext3. Në paragrafin e mëparshëm, fola se si ext3 shpërndau shumë të dhëna në disk, gjë që i ngadalësoi shumë gjërat. fsync(). Për të përmirësuar situatën, në ext4, vetëm ato faqe të pista që janë të rëndësishme për një skedar të caktuar, shpërndahen në disk. Dhe të dhënat nga skedarët e tjerë mbeten në memorie për një kohë shumë më të gjatë sesa me ext3. Kjo është bërë për të përmirësuar performancën (si parazgjedhje, të dhënat qëndrojnë në këtë gjendje për 30 sekonda, ju mund ta konfiguroni këtë duke përdorur dirty_expire_centisecs; këtu Ju mund të gjeni materiale shtesë për këtë). Kjo do të thotë që një sasi e madhe e të dhënave mund të humbet në mënyrë të pakthyeshme pas një dështimi. Zgjidhja për këtë problem është përdorimi fsync() në aplikacionet që duhet të sigurojnë ruajtjen e qëndrueshme të të dhënave dhe t'i mbrojnë ato sa më shumë që të jetë e mundur nga pasojat e dështimeve. Funksioni fsync() funksionon shumë më me efikasitet kur përdorni ext4 sesa kur përdorni ext3. Disavantazhi i kësaj qasjeje është se përdorimi i saj, si më parë, ngadalëson ekzekutimin e disa operacioneve, siç është instalimi i programeve. Shihni detaje rreth kësaj këtu и këtu.

Problemi i tretë në lidhje me fsync(), e krijuar në vitin 2018. Më pas, në kuadër të projektit PostgreSQL, u konstatua se nëse funksioni fsync() ndeshet me një gabim, shënon faqet "të pista" si "të pastra". Si rezultat, thirrjet e mëposhtme fsync() Ata nuk bëjnë asgjë me faqe të tilla. Për shkak të kësaj, faqet e modifikuara ruhen në memorie dhe nuk shkruhen kurrë në disk. Kjo është një fatkeqësi e vërtetë, pasi aplikacioni do të mendojë se disa të dhëna janë shkruar në disk, por në fakt nuk do të jetë. Dështime të tilla fsync() janë të rralla, aplikimi në situata të tilla nuk mund të bëjë pothuajse asgjë për të luftuar problemin. Këto ditë, kur kjo ndodh, PostgreSQL dhe aplikacionet e tjera rrëzohen. Këtu, në materialin “Can Applications Recover from Fsync Failures?”, ky problem është eksploruar në detaje. Aktualisht, zgjidhja më e mirë për këtë problem është përdorimi i I/O direkt me flamurin O_SYNC ose me flamur O_DSYNC. Me këtë qasje, sistemi do të raportojë gabime që mund të ndodhin gjatë operacioneve specifike të shkrimit, por kjo qasje kërkon që aplikacioni të menaxhojë vetë buferët. Lexoni më shumë për këtë këtu и këtu.

Hapja e skedarëve duke përdorur flamujt O_SYNC dhe O_DSYNC

Le të kthehemi te diskutimi i mekanizmave Linux që ofrojnë ruajtje të qëndrueshme të të dhënave. Domethënë, po flasim për përdorimin e flamurit O_SYNC ose flamur O_DSYNC kur hapni skedarë duke përdorur thirrjen e sistemit hapur (). Me këtë qasje, çdo operacion i shkrimit të të dhënave kryhet sikur pas çdo komande write() sistemit i jepen komanda në përputhje me rrethanat fsync() и fdatasync(). Në Specifikimet e POSIX kjo quhet "Përfundimi i integritetit të skedarit të sinkronizuar I/O" dhe "Përfundimi i integritetit të të dhënave". Avantazhi kryesor i kësaj qasjeje është se për të siguruar integritetin e të dhënave, ju duhet vetëm të bëni një telefonatë sistemi, në vend të dy (për shembull - write() и fdatasync()). Disavantazhi kryesor i kësaj qasjeje është se të gjitha shkrimet duke përdorur përshkruesin përkatës të skedarit do të sinkronizohen, gjë që mund të kufizojë aftësinë për të strukturuar kodin e aplikacionit.

Përdorimi i I/O direkt me flamurin O_DIRECT

Thirrja e sistemit open() mbështet flamurin O_DIRECT, i cili është projektuar për të anashkaluar cache-in e sistemit operativ për të kryer operacione I/O duke ndërvepruar drejtpërdrejt me diskun. Kjo, në shumë raste, do të thotë që komandat e shkrimit të lëshuara nga programi do të përkthehen drejtpërdrejt në komanda që synojnë të punojnë me diskun. Por, në përgjithësi, ky mekanizëm nuk është një zëvendësim për funksionet fsync() ose fdatasync(). Fakti është se vetë disku mundet shtyj ose memorie komandat përkatëse të shkrimit të të dhënave. Dhe, për t'i bërë gjërat edhe më keq, në disa raste të veçanta operacionet I/O kryhen gjatë përdorimit të flamurit O_DIRECT, transmetim në operacionet tradicionale të buferuara. Mënyra më e lehtë për të zgjidhur këtë problem është përdorimi i flamurit për të hapur skedarë O_DSYNC, që do të thotë se çdo operacion shkrimi do të pasohet nga një thirrje fdatasync().

Doli se sistemi i skedarëve XFS kohët e fundit kishte shtuar një "rrugë të shpejtë" për O_DIRECT|O_DSYNC-regjistrimi i të dhënave. Nëse një bllok rishkruhet duke përdorur O_DIRECT|O_DSYNC, më pas XFS, në vend që të pastrojë cache-in, do të ekzekutojë komandën e shkrimit FUA nëse pajisja e mbështet atë. E verifikova këtë duke përdorur programin blktrace në një sistem Linux 5.4/Ubuntu 20.04. Kjo qasje duhet të jetë më efikase, pasi kur përdoret, një sasi minimale e të dhënave shkruhen në disk dhe përdoret një operacion, në vend të dy (shkrimi dhe shpëlarja e cache-it). Kam gjetur një lidhje për patch 2018 kernel, i cili zbaton këtë mekanizëm. Ka disa diskutime atje rreth aplikimit të këtij optimizimi në sisteme të tjera skedarësh, por me sa di unë, XFS është i vetmi sistem skedar që e mbështet këtë deri më tani.

funksioni sync_file_range().

Linux ka një thirrje sistemi SYNC_FILE_RANGE (), i cili ju lejon të shpërndani vetëm një pjesë të skedarit në disk, në vend të të gjithë skedarit. Kjo telefonatë fillon një shpëlarje asinkrone të të dhënave dhe nuk pret që të përfundojë. Por në certifikatë sync_file_range() skuadra thuhet se është "shumë e rrezikshme". Nuk rekomandohet përdorimi i tij. Karakteristikat dhe rreziqet sync_file_range() përshkruar shumë mirë në kjo material. Në mënyrë të veçantë, kjo thirrje duket se përdor RocksDB për të kontrolluar kur kerneli shpërndan të dhënat e pista në disk. Por në të njëjtën kohë, për të siguruar ruajtjen e qëndrueshme të të dhënave, përdoret gjithashtu fdatasync(). Në kodi RocksDB ka disa komente interesante për këtë temë. Për shembull, duket se thirrja sync_file_range() Kur përdorni ZFS, ai nuk i shpërndan të dhënat në disk. Përvoja më thotë se kodi që përdoret rrallë ka të ngjarë të përmbajë gabime. Prandaj, unë do të këshilloja të mos përdorni këtë telefonatë sistemi, përveç nëse është absolutisht e nevojshme.

Thirrjet e sistemit që ndihmojnë në sigurimin e qëndrueshmërisë së të dhënave

Unë kam arritur në përfundimin se ekzistojnë tre qasje që mund të përdoren për të kryer operacione I/O që sigurojnë qëndrueshmërinë e të dhënave. Ata të gjithë kërkojnë një thirrje funksioni fsync() për direktorinë në të cilën është krijuar skedari. Këto janë qasjet:

  1. Thirrja e funksionit fdatasync() ose fsync() pas funksionit write() (është më mirë të përdoret fdatasync()).
  2. Puna me një përshkrues skedari të hapur me një flamur O_DSYNC ose O_SYNC (më mirë - me një flamur O_DSYNC).
  3. Duke përdorur komandën pwritev2() me flamur RWF_DSYNC ose RWF_SYNC (mundësisht me flamur RWF_DSYNC).

Shënime të performancës

Nuk e kam matur me kujdes performancën e mekanizmave të ndryshëm që kam ekzaminuar. Dallimet që kam vënë re në shpejtësinë e punës së tyre janë shumë të vogla. Kjo do të thotë se mund të kem gabim dhe se në kushte të ndryshme e njëjta gjë mund të japë rezultate të ndryshme. Së pari, unë do të flas për atë që ndikon më shumë në performancën, dhe më pas çfarë ndikon në performancën më pak.

  1. Mbishkrimi i të dhënave të skedarit është më i shpejtë se shtimi i të dhënave në një skedar (përfitimi i performancës mund të jetë 2-100%). Shtimi i të dhënave në një skedar kërkon ndryshime shtesë në meta të dhënat e skedarit, edhe pas një telefonate sistemi fallocate(), por madhësia e këtij efekti mund të ndryshojë. Unë rekomandoj, për performancën më të mirë, të telefononi fallocate() për të paracaktuar hapësirën e kërkuar. Atëherë kjo hapësirë ​​duhet të plotësohet në mënyrë eksplicite me zero dhe të thirret fsync(). Kjo do të sigurojë që blloqet përkatëse në sistemin e skedarëve të shënohen si "të ndara" dhe jo "të pashpërndara". Kjo jep një përmirësim të vogël (rreth 2%) të performancës. Për më tepër, disa disqe mund të kenë një qasje të parë më të ngadaltë në një bllok se të tjerët. Kjo do të thotë se mbushja e hapësirës me zero mund të çojë në një përmirësim të ndjeshëm (rreth 100%) të performancës. Në veçanti, kjo mund të ndodhë me disqe AWS EBS (Këto janë të dhëna jozyrtare, nuk mund ta konfirmoja). E njëjta gjë vlen edhe për ruajtjen Disku i qëndrueshëm GCP (dhe ky është tashmë informacion zyrtar, i konfirmuar nga testet). Të njëjtën gjë kanë bërë edhe ekspertë të tjerë vrojtim, lidhur me disqe të ndryshëm.
  2. Sa më pak thirrje të sistemit, aq më e lartë është performanca (fitimi mund të jetë rreth 5%). Duket si një sfidë open() me flamur O_DSYNC ose telefononi pwritev2() me flamur RWF_SYNC më shpejt se një telefonatë fdatasync(). Unë dyshoj se çështja këtu është se kjo qasje luan një rol në faktin se duhet të kryhen më pak thirrje sistemore për të zgjidhur të njëjtin problem (një thirrje në vend të dy). Por ndryshimi në performancë është shumë i vogël, kështu që mund ta injoroni plotësisht dhe të përdorni diçka në aplikacion që nuk do ta komplikojë logjikën e tij.

Nëse jeni të interesuar në temën e ruajtjes së qëndrueshme të të dhënave, këtu janë disa materiale të dobishme:

  • Metodat e hyrjes në hyrje — pasqyrë e bazave të mekanizmave hyrës/dalës.
  • Sigurimi që të dhënat arrijnë në disk — një histori për atë që ndodh me të dhënat gjatë rrugës nga aplikacioni në disk.
  • Kur duhet të sinkronizoni direktorinë që përmban - përgjigja në pyetjen se kur të përdoret fsync() për drejtoritë. Për ta thënë me pak fjalë, rezulton se duhet ta bëni këtë kur krijoni një skedar të ri, dhe arsyeja për këtë rekomandim është se në Linux mund të ketë shumë referenca për të njëjtin skedar.
  • SQL Server në Linux: FUA Internals — këtu është një përshkrim se si zbatohet ruajtja e vazhdueshme e të dhënave në SQL Server në platformën Linux. Këtu ka disa krahasime interesante midis thirrjeve të sistemit Windows dhe Linux. Jam pothuajse i sigurt se ishte falë këtij materiali që mësova për optimizimin FUA të XFS.

A keni humbur të dhënat që mendonit se ishin ruajtur në mënyrë të sigurt në një disk?

Ruajtja e qëndrueshme e të dhënave dhe API-të e skedarëve Linux

Ruajtja e qëndrueshme e të dhënave dhe API-të e skedarëve Linux

Burimi: www.habr.com