Unë, duke hulumtuar qëndrueshmërinë e ruajtjes së të dhënave në sistemet e cloud, vendosa të verifikoj veten, të sigurohem që kuptoj gjërat bazë. Unë për t'u kuptuar me ato garancitë që ofrojnë disqet NVMe për ruajtjen e qëndrueshme të të dhënave (pra, garancitë që të dhënat do të jenë të disponueshme pas një dështimi të sistemit). Unë bëra këto përfundime kryesore: duhet të merret në konsideratë se të dhënat janë të dëmtuara që nga momenti kur është dhënë urdhri për të shkruar të dhënat dhe deri në momentin kur përfundon shkrimi i tyre në mediumin e informacionit. Megjithatë, në shumicën e programeve për shkrimin e të dhënave, thjesht përdoren thirrjet sistemike.
Në këtë material, unë hulumtoj mekanizmat e ruajtjes së qëndrueshme të të dhënave që ofrohen nga API-të e skedarëve në Linux. Duket se këtu gjithçka duhet të jetë e thjeshtë: programi thërret komandën write(), dhe pasi përfundon puna e kësaj komande, të dhënat do të jenë ruajtur në mënyrë të sigurt në disk. Por write() thjesht kopjon të dhënat e aplikacionit në cache-n e bërthamës, i cili ndodhet në memorien operative. Për të detyruar sistemin të ruajë të dhënat në disk, nevojitet përdorimi i disa mekanizmave shtesë.
Në përgjithësi, ky material përfaqëson një grup shënimesh rreth asaj që kam mësuar për temën që më intereson. Nëse flasim shumë shkurt mbi më të rëndësishmen, ajo është se për të organizuar ruajtjen e qëndrueshme të të dhënave duhet të përdoret komanda fdatasync() ose të hapen skedarë me flakë O_DSYNC. Nëse jeni të interesuar të dini në detaje se çfarë ndodh me të dhënat gjatë rrugës nga kodi i programit në disk, shikoni artikullin.
Karakteristikat e përdorimit të funksionit write()
Thirrja e sistemit write() janë përcaktuar në standardin si një përpjekje për të shkruar të dhëna në një të drejta skedari. Pas përfundimit të suksesshëm të veprimit write() operacionet e leximit të të dhënave duhet të kthejnë pikërisht ato byte që ishin shkruar më parë, duke e bërë këtë edhe në rast se të dhënat qasen nga procese ose rrjedha të tjera ( pjëza përkatëse e standardit POSIX). , në seksionin që trajton ndërveprimin e rrjedhave me operacionet e zakonshme të skedarëve ka një shënim ku thuhet se nëse secila nga dy rrjedhat thërret këto funksione, atëherë çdo thirrje duhet të shohë ose të gjitha pasojat e caktuara që ndodhin për shkak të ekzekutimit të thirrjes tjetër, ose të mos shohë asnjë pasojë. Kjo lejon të përfundohet se të gjitha operacionet e skedarëve të hyrjes/daljes duhet të mbajnë bllokimin e burimit me të cilin punojnë.
A do të thotë kjo se operacioni write() është atomic? Nga perspektiva teknike — po. Operacionet e leximit të të dhënave duhet të kthejnë ose gjithçka ose asgjë nga ajo që është shkruar me write(). Por operacioni write(), sipas standardit, nuk është domosdoshmërisht i detyruar të përfundojë duke shkruar gjithçka që iu ofrua për t'u shkruar. Ajo lejohet të shkruajë vetëm një pjesë të të dhënave. Për shembull, mund të kemi dy rrjedha, secila nga të cilat bashkëngjit 1024 byte në një skedar që përshkruhet nga e njëjta e drejte skedari. Nga pikëpamja e standardit, është e pranueshme një rezultat kur secila nga operacionet e shkrimit arrin të bashkëngjisë vetëm një byte në skedar. Këto operacione do të mbeten atomike, por pasi të përfundojnë, të dhënat që ato shkruan në skedar do të jenë të përziera. ka një diskutim shumë interesant mbi këtë temë në Stack Overflow.
Funksionet fsync() dhe fdatasync()
Mënyra më e thjeshtë për të shpërndarë të dhënat në disk është thirrja e funksionit . Ky funksion kërkon nga sistemi operativ të transferojë të gjithë blloqet e modifikuar nga cache në disk. Këtu përfshihen gjithashtu të gjitha metadata e skedarit (koha e aksesit, koha e modifikimit të skedarit dhe kështu me radhë). Unë besoj se nevoja për këto metadata ndodh rrallë, prandaj, nëse e dini se ato nuk janë të rëndësishme për ju, mund të përdorni funksionin fdatasync(). Në për fdatasync() thuhet se gjatë punës së këtij funksioni, ruhet në disk një volum metadata që "është e nevojshme për kryerjen e duhur të operacioneve të ardhshme të leximit të të dhënave". Dhe kjo është ajo që i shqetëson shumicën e aplikacioneve.
Një nga problemet që mund të lindin këtu është se këto mekanizma nuk garantojnë që skedari mund të zbulohet pas një dështimi të mundshëm. Në veçanti, kur krijohet një skedar i ri, është e nevojshme të thirret fsync() për direktorinë që e përmban atë. Ndryshe, pas dështimit mund të ndodhë që ky skedar të mos ekzistojë. Arsyeja për këtë është se në UNIX, për shkak të përdorimit të lidhjeve të forta, skedari mund të ekzistojë në disa direktori. Prandaj, kur thirret fsync() nuk ka mënyrë për të zbuluar se cilit direktorje i përkasin të dhënat gjithashtu duhet të shkarkohet në disk ( mund të lexoni më shumë rreth kësaj). Duket se sistema skedari ext4 është në gjendje të aplikoni fsync() të ndihmojë me direktorët që përmbajnë skedarët përkatës, por në rastin e sistemeve të tjera të skedareve, kjo mund të mos jetë e tillë.
Ky mekanizëm mund të jetë implementuar ndryshe në sisteme të ndryshme skedari. Kam përdorur për të zbuluar se cilat operacione disku përdoren në sistemet e skedarëve ext4 dhe XFS. Të dy japin komandat e zakonshme për shkrimin në disk për përmbajtjen e skedarëve dhe për jurnalin e sistemit të skedarëve, shkarkojnë cache-n dhe përfundojnë punën, duke kryer shkrim FUA (Accessi i Njësisë Forcuar, shkrimi i të dhënave direkt në disk, duke anashkaluar cache) në jurnal. Ndoshta, ata veprojnë në këtë mënyrë për të konfirmuar faktin e kryerjes së operacionit. Në diskët që nuk mbështesin FUA, kjo shkakton dy shkarkime të caches. Eksperimentet e mia treguan se fdatasync() pak më shpejt fsync(). Utilitari blktrace tregon se fdatasync() zakonisht shkruan më pak të dhëna në disk (në ext4 fsync() shkruan 20 KiB, ndërsa fdatasync() — 16 KiB). Për më tepër, kam zbuluar se XFS është pak më i shpejtë se ext4. Dhe këtu me ndihmën e blktrace kam arritur të zbuloj se fdatasync() shkarkon më pak të dhëna në disk (4 KiB në XFS).
Situatat e paqartë që ndodhin kur përdoret fsync()
Mund të përmend tri situata të paqarta, që lidhen me fsync(), me të cilat jam përballur në praktikë.
Rasti i parë ndodhi në vitin 2008. Atëherë, ndërfaqja e Firefox 3 “ngadalësohej” nëse po bëhej shkrim në disk të një numri të madh skedari. Problemi ishte se në implementimin e ndërfaqes për ruajtjen e informacionit të saj të gjendjes ishte përdorur një bazë të dhënash SQLite. Pas çdo ndryshimi që ndodhte në ndërfaqe, thirrej funksioni fsync(), i cili jepte garanci të mira për ruajtjen e qëndrueshme të të dhënave. Në sistemin e skedarëve ext3 në përdorim asokohe, funksioni fsync() shkarkonte në disk të gjitha “faqsat e ndotura” në sistem, dhe jo vetëm ato që kishin lidhje me skedarin përkatës. Kjo do të thoshte se një klikim mbi butonin në Firefox mund të iniciativësonte shkrimin e megabajt-eve të të dhënave në diskun magnetik, çka mund të zgjasë disa sekonda. Zgjidhja e problemit, sa kam kuptuar nga materiali, konsistonte në zhvendosjen e punës me bazën e të dhënave në detyra asinkrone në sfond. Kjo do të thoshte se më parë në Firefox ishin implementuar kërkesa më rigoroze për ruajtjen e qëndrueshme të të dhënave, sesa ishte realisht e nevojshme, dhe veçoritë e sistemit të skedarëve ext3 vetëm e kishin përkeqësuar këtë problem.
E dyta ndodhi në vitin 2009. Atëherë, pas një dështimi të sistemit, përdoruesit e sistemit të ri të skedarëve ext4 u përballën me faktin se shumë skedarë të sapokrijuar kishin gjatësi zero, ndërsa me sistemin e vjetër të skedarëve ext3 ndodhi ndryshe. Në paragrafin e kaluar thashë se ext3 shkarkonte shumë të dhëna në disk, gjë që ngadalësonte punën fsync(). Për të përmirësuar situatën, në ext4 shkarkohen në disk vetëm faqet “e ndotura” që lidhen me skedarin specifik. Të dhënat e skedarëve të tjerë mbeten në memorie për një periudhë shumë më të gjatë kohore se sa duke përdorur ext3. Kjo u bë për të përmirësuar performancën (sipërfaqësisht të dhënat qëndrojnë në këtë gjendje 30 sekonda, kjo mund të rregullohet përmes ; mund të gjenden materiale shtesë rreth kësaj). Kjo do të thoshte se një volum i madh të dhënash mund të humbasë përfundimisht pas një dështimi. Zgjidhja e këtij problemi është përdorimi i fsync() në aplikacionet që kanë nevojë të sigurojnë ruajtje të qëndrueshme të të dhënave dhe t'i mbrojnë maksimalisht ato nga pasojat e dështimeve. Funksioni fsync() punon në përdorimin e ext4 shumë më efektivisht se në përdorimin e ext3. Disavantazhi i këtij qasje është se aplikimi i saj, si më parë, ngadalëson ekzekutimin e disa operacioneve, si instalimi i programeve. Detajet rreth kësaj shihni dhe .
Problemi i tretë që lidhet me fsync(), ndodhi në vitin 2018. Atëherë, në kuadër të projektit PostgreSQL, u zbulua se nëse funksioni fsync() në rast të një gabimi, e shënon “faqet e ndotura” si “të pastra”. Si rezultat, thirrjet e mëpasshme fsync() nuk bëjnë asgjë me këto faqe. Për këtë arsye, faqet e modifikuara mbahen në memorie dhe kurrë nuk shkarkohen në disk. Kjo është një katastrofë e vërtetë, pasi aplikacioni do të mendojë se disa të dhëna janë shkarkuar në disk, ndërsa në të vërtetë kjo do të mos jetë ashtu. Këto dështime fsync() ndodhin rrallë, aplikacioni në këto situata gati nuk mund të bëjë asgjë për të luftuar problemin. Në ditët e sotme, kur ndodh kjo, PostgreSQL dhe aplikacione të tjera dalin menjëherë nga puna. , në materialin "A mund të rikuperohen aplikacionet nga dështimet e fsync?", kjo çështje studiohet në të gjitha detajet. Aktualisht, zgjidhja më e mirë për këtë problem është përdorimi i Direct I/O me flagun O_SYNC ose me flagun O_DSYNC. Me këtë qasje, sistemi do të njoftojë për gabimet që mund të ndodhin gjatë kryerjes së operacioneve specifike të shkrimit të të dhënave, por ky qasje kërkon që aplikacioni të administrojë vetë buffers. Detaje mbi këtë lexoni dhe .
Hapja e skedarëve duke përdorur flagjet O_SYNC dhe O_DSYNC
Të kthehemi në diskutimin e mekanizmave Linux që ofrojnë ruajtje të qëndrueshme të të dhënave. Saktësisht, bëhet fjalë për përdorimin e flagut O_SYNC ose flagut O_DSYNC kur hapni skedarë duke përdorur thirrjen sistemore . Me këtë qasje, çdo operacion shkrimi të dhënash kryhet sikur pas çdo komande write() sistemit i jepen, përkatësisht, komandat fsync() dhe fdatasync(). Në kjo quhet "Përfundimi i Integritetit të Skedarit të I/O të Sinkronizuar" dhe "Përfundimi i Integritetit të të Dhënave". Avantazhi kryesor i këtij qasjes është se për të siguruar integritetin e të dhënave, duhet të kryhet vetëm një thirrje sistemore, e jo dy (p.sh. - write() dhe fdatasync()). Disavantazhi kryesor i këtij qasjeje është se të gjitha operacionet e shkrimit që përdorin skedarin përkatës do të jenë të sinkronizuara, gjë që mund të kufizojë mundësitë për strukturuar kodin e aplikacionit.
Përdorimi i Direct I/O me flagun O_DIRECT
Thirrja e sistemit open() mbështet flagun O_DIRECT, i cili është menduar për të realizuar operacione input-output duke anashkaluar ndërfaqen e memories së sistemit, duke ndërvepruar drejtpërdrejt me diskun. Kjo, në shumë raste, do të thotë se komandat e shkrimit të lëshuara nga programi do të përkthehen drejtpërdrejt në komanda të orientuara ndaj diskut. Por, në përgjithësi, ky mekanizëm nuk është një zëvendësim për funksionet fsync() ose fdatasync(). Problemi është se disku vetë mund komandat përkatëse të shkrimit të të dhënave. Dhe, çfarë është edhe më keq, në disa raste të veçanta, operacionet e input-output që realizohen duke përdorur flagun O_DIRECT, në operacione tradicionale me buffered. Problemi lehtë mund të zgjidhet duke përdorur edhe flagun O_DSYNC, gjë që do të nënkuptonte se pas çdo operacioni shkrimi do të kishte një thirrje fdatasync().
Doli se në sistemin e skedarëve XFS, së fundmi është shtuar një "rrugë e shpejtë" për O_DIRECT|O_DSYNC-shkrimin e të dhënave. Nëse një bllok ripërsëritet duke përdorur O_DIRECT|O_DSYNC, atëherë XFS, në vend që të zbrazë memorjen, do të kryejë komandën FUA-shkrimi vetëm në rast se pajisja e mbështet. E verifikova këtë duke përdorur utilitarin blktrace në sistemin Linux 5.4/Ubuntu 20.04. Kjo qasje duhet të jetë më efikase, pasi gjatë saj shkruhen pak sa më shumë të dhëna në disk dhe përdoret një operacion, e jo dy (shkrimi dhe zbrazja e memorjes). Gjeta një lidhje në në kernelin e vitit 2018, ku është realizuar ky mekanizëm. Aty ka një diskutim në lidhje me aplikimin e kësaj optimizimi edhe në sisteme të tjera skedarësh, por deri ku di unë, XFS është për momentin sistemi i vetëm i skedarëve që e mbështet këtë.
Funksioni sync_file_range()
Në Linux ka një thirrje sistemore , e cila lejon zbrazjen në disk vetëm të një pjese të skedarit, e jo të gjithë skedarit. Kjo thirrje iniciaton një zbrazje asinkrone të të dhënave dhe nuk pret për përfundimin e saj. Por në shënimin e sync_file_range() thuhet se kjo komandë është "shumë e rrezikshme". Përdorimi i saj nuk rekomandohet. Veçoritë dhe rreziqet sync_file_range() janë përshkruar shumë mirë në material. Në veçanti, duket se kjo thirrje përdor RocksDB për të menaxhuar se kur bërthama zbraz "të dhënat e ndotura" në disk. Por gjithashtu aty, për të siguruar ruajtjen e qëndrueshme të të dhënave, përdoret edhe fdatasync(). Në RocksDB ka komente interesante në këtë temë. Për shembull, duket se thirrja sync_file_range() duke përdorur ZFS nuk çon në zbrazjen e të dhënave në disk. Eksperienca më tregon se kodi, i cili përdoret rrallë, mund të përmbajë gabime. Prandaj do të rekomandoja që të mos e përdorni këtë thirrje sistemore pa një nevojë të madhe.
Thirrjet sistemore që ndihmojnë në sigurimin e ruajtjes së qëndrueshme të të dhënave
Arrita në përfundimin se për të realizuar operacione input-output që sigurojnë ruajtjen e qëndrueshme të të dhënave, mund të përdoren tre qasje. Të gjitha ato kërkojnë thirrjen e funksionit fsync() për direktorine ku është krijuar skedari. Këto janë këto qasje:
- Thirrja e funksionit
fdatasync()osefsync()pas funksionitwrite()(më mirë të përdoretfdatasync()). - Puna me skedarin e hapur me flagun
O_DSYNCoseO_SYNC(më mirë — me flagunO_DSYNC). - Përdorimi i komandës
pwritev2()me flamurRWF_DSYNCoseRWF_SYNC(preferohet — me flagunRWF_DSYNC).
Shënime mbi performancën
Nuk e kam bërë një matje të kujdesshme të performancës së mekanizmave të ndryshëm që kam shqyrtuar. Diferencat e vërejtura nga unë në shpejtësinë e funksionimit të tyre janë të vogla. Kjo do të thotë se mund të kem gabime, dhe se nën kushte të tjera, të njëjtën gjë mund të tregojë rezultate të tjera. Së pari, do të flas për ato që kanë një ndikim më të madh në performancë dhe pastaj për ato që ndikojnë më pak.
- Rikthimi i të dhënave të skedarit është më i shpejtë se sa bashkimi i të dhënave në një skedar (fitimi në performancë mund të jetë 2-100%). Bashkimi i të dhënave në një skedar kërkon ndryshime shtesë në metadatat e skedarit, madje edhe pas thirrjes së sistemit.
fallocate(), por shkalla e këtij efekti mund të ndryshojë. Unë rekomandoj që për të siguruar performancën më të mirë, të thirretfallocate()për të rezervuar hapësirën e nevojshme. Më pas, kjo hapësirë duhet të mbushet qartë me zero dhe të thirretfsync(). Falë kësaj, blloqet përkatëse në sistemin e skedarëve do të shënohen si "të rezervuara", jo si "të pa rezervuara". Kjo ofron një përmirësim të vogël (rreth 2%) në performancë. Për më tepër, disa disqe mund të kenë operacionin e parë të akseses në bllok më të ngadalshëm se të tjerat. Kjo do të thotë se mbushja e hapësirës me zero mund të çojë në një përmirësim të konsiderueshëm (rreth 100%) në performancë. Kjo mund të ndodhë veçanërisht me disqet (kjo është informacion jozyrtar, nuk kam mundur ta konfirmoj). E njëjta gjë vlen edhe për ruajtjet (dhe kjo është tashmë informacion zyrtar, i konfirmuar nga testet). Spikatës të tjerë kanë bërë të njëjtat , që lidhen me disqe të ndryshme. - Sa më pak thirrje sistemike, aq më e lartë performanca (fitimi mund të arrijë rreth 5%). Duket se thirrja
open()me flamurO_DSYNCose thirrjapwritev2()me flamurRWF_SYNCështë më e shpejtë se thirrjafdatasync(). Dyshoj se kjo ndodh për shkak se me këtë qasje, roli luhet nga fakti se për të zgjidhur një detyrë të njëjtë, duhet të kryhen më pak thirrje sistemike (një thirrje në vend të dy). Por diferenca në performancë është shumë e vogël, kështu që ju mund ta injoroni dhe të përdorni atë që nuk do të çonte në komplikuarjen e logjikës së aplikacionit.
Nëse jeni të interesuar për temën e ruajtjes së qëndrueshme të të dhënave - ja disa materiale të dobishme:
- — një përmbledhje e mekanizmave të hyrjes/eksportit.
- — një tregim për atë që ndodh me të dhënat në rrugën e tyre nga aplikacioni në disk.
- — përgjigjja për pyetjen se kur duhet të aplikoni
fsync()për direktorët. Nëse ta përmbledhësh këtë në disa fjalë, do të del se duhet ta bëni këtë gjatë krijimit të një skedari të ri, dhe arsyeja për këtë rekomandim është se në Linux mund të ketë shumë lidhje në një skedar të njëjtë. - — kjo përmban një përshkrim të mënyrës se si realizohet ruajtja e qëndrueshme e të dhënave në SQL Server në platformën Linux. Këtu janë disa krahasime interesante midis thirrjeve sistemike të Windows dhe Linux. Jam pothuajse i sigurt se falë këtij materiali mësova për optimizimin FUA në XFS.
A keni humbur të dhëna që menduat se ishin ruajtur në mënyrë të sigurt në disk?
Burimi: habr.com
