Daŭraj Datumstokado kaj Linukso-Dosiero-APIoj

Esplorante la daŭripovon de datumstokado en nubaj sistemoj, mi decidis testi min por certigi, ke mi komprenas la bazajn aferojn. mi komencite legante la NVMe-specifon por kompreni kiajn garantiojn pri daŭrigebla datumstokado (tio estas garantioj, ke la datumoj estos disponeblaj post fiasko de la sistemo) donas al ni NMVe-diskojn. Mi faris la jenajn ĉefajn konkludojn: datumoj devas esti konsiderataj difektitaj de la momento, kiam la komando por skribi datumojn estas donita ĝis la momento, kiam ĝi estas skribita al la konserva medio. Tamen, plej multaj programoj sufiĉe feliĉe uzas sistemajn vokojn por registri datumojn.

En ĉi tiu afiŝo, mi esploras la persistajn stokadmekanismojn provizitajn de la Linukso-dosieroj API. Ŝajnas, ke ĉio devus esti simpla ĉi tie: la programo vokas la komandon write(), kaj post kiam ĉi tiu komando finiĝos, la datumoj estos sekure konservitaj al disko. Sed write() nur kopias aplikajn datumojn al la kernkaŝmemoro situanta en RAM. Por devigi la sistemon skribi datumojn al disko, vi devas uzi kelkajn pliajn mekanismojn.

Daŭraj Datumstokado kaj Linukso-Dosiero-APIoj

Ĝenerale, ĉi tiu materialo estas kolekto de notoj rilataj al tio, kion mi lernis pri temo interesa al mi. Se ni parolas tre mallonge pri la plej grava afero, rezultas, ke por organizi daŭrigeblan datumstokadon oni devas uzi la komandon fdatasync() aŭ malfermi dosierojn kun la flago O_DSYNC. Se vi interesiĝas lerni pli pri tio, kio okazas al datumoj survoje de kodo al disko, rigardu ĉi tio artikolo.

Karakterizaĵoj de uzado de la funkcio write().

Sistemvoko write() difinita en la normo IEEE POSIX kiel provo skribi datumojn al dosierpriskribilo. Post sukcesa kompletigo write() Datenlegaj operacioj devas resendi precize la bajtojn kiuj estis antaŭe skribitaj, farante tion eĉ se la datumoj estas aliritaj de aliaj procezoj aŭ fadenoj (jen koncerna sekcio de la POSIX-normo). estas, en la sekcio pri kiel fadenoj interagas kun normalaj dosieroperacioj, ekzistas noto kiu diras ke se du fadenoj ĉiu vokas ĉi tiujn funkciojn, tiam ĉiu voko devas vidi aŭ ĉiujn el la elektitaj sekvoj de la alia voko, aŭ neniun entute. konsekvencoj. Ĉi tio kondukas al la konkludo, ke ĉiuj dosieroj I/O-operacioj devas teni seruron sur la rimedo, sur kiu ili funkcias.

Ĉu ĉi tio signifas, ke la operacio write() ĉu ĝi estas atoma? El teknika vidpunkto, jes. Datenlegaj operacioj devas resendi aŭ ĉion aŭ nenion de tio, kion oni skribis write(). Sed la operacio write(), laŭ la normo, ne nepre devas fini skribante ĉion, kion oni petis noti. Ŝi rajtas skribi nur parton de la datumoj. Ekzemple, ni povus havi du fadenojn ĉiu aldonante 1024 bajtojn al dosiero priskribita de la sama dosierpriskribilo. El la vidpunkto de la normo, akceptebla rezulto estos kiam ĉiu skriboperacio povas aldoni nur unu bajton al la dosiero. Ĉi tiuj operacioj restos atomaj, sed post kiam ili finiĝos, la datumoj, kiujn ili skribis al la dosiero, estos miksitaj. tie tre interesa diskuto pri ĉi tiu temo pri Stack Overflow.

fsync() kaj fdatasync() funkcioj

La plej facila maniero flui datumojn al disko estas voki la funkcion fsync (). Ĉi tiu funkcio petas al la operaciumo translokigi ĉiujn modifitajn blokojn de la kaŝmemoro al disko. Ĉi tio inkluzivas ĉiujn metadatumojn de dosiero (tempo de aliro, tempo de modifado de dosieroj ktp). Mi kredas, ke ĉi tiuj metadatumoj malofte bezonas, do se vi scias, ke ĝi ne gravas por vi, vi povas uzi la funkcion fdatasync(). la helpo sur fdatasync() oni diras, ke dum la funkciado de ĉi tiu funkcio, tia kvanto da metadatenoj estas konservita al disko, kiu estas "necesa por la ĝusta ekzekuto de la sekvaj legado de datumoj." Kaj ĝuste ĉi tio zorgas pri la plej multaj aplikoj.

Unu problemo, kiu povas ekesti ĉi tie, estas, ke ĉi tiuj mekanismoj ne garantias, ke la dosiero estos malkovrebla post ebla fiasko. Precipe, kiam vi kreas novan dosieron, vi devas voki fsync() por la dosierujo kiu enhavas ĝin. Alie, post malsukceso, povas rezulti, ke ĉi tiu dosiero ne ekzistas. La kialo de tio estas, ke en UNIKSO, pro la uzo de malmolaj ligiloj, dosiero povas ekzisti en pluraj dosierujoj. Tial, kiam oni vokas fsync() ne ekzistas maniero por dosiero scii, kiuj dosierujo-datumoj ankaŭ devus esti fluitaj al disko (tie Vi povas legi pli pri tio). Ŝajnas, ke la dosiersistemo ext4 kapablas aŭtomate uzi fsync() al la dosierujoj enhavantaj la respondajn dosierojn, sed tio eble ne estas la kazo ĉe aliaj dosiersistemoj.

Ĉi tiu mekanismo povas esti efektivigita alimaniere sur malsamaj dosiersistemoj. mi uzis blktrace por lerni pri kiaj diskoperacioj estas uzataj en dosiersistemoj ext4 kaj XFS. Ambaŭ eldonas regulajn skribkomandojn al disko por kaj la dosierenhavo kaj la dosiersistemĵurnalo, forĵetas la kaŝmemoron, kaj eliras elfarante FUA (Force Unit Access, skribante datumojn rekte al disko, preterirante la kaŝmemoron) skribon al la ĵurnalo. Ili verŝajne faras tion por konfirmi, ke la transakcio okazis. Sur diskoj kiuj ne subtenas FUA, tio kaŭzas du kaŝmemorforfluojn. Miaj eksperimentoj montris tion fdatasync() iom pli rapide fsync(). Utilo blktrace indikas tion fdatasync() kutime skribas malpli da datumoj al disko (en ext4 fsync() skribas 20 KiB, kaj fdatasync() - 16 KiB). Ankaŭ mi eksciis, ke XFS estas iomete pli rapida ol ext4. Kaj ĉi tie kun la helpo blktrace sukcesis ekscii tion fdatasync() fluigas malpli da datumoj al disko (4 KiB en XFS).

Ambiguaj situacioj kiuj aperas dum uzado de fsync ()

Mi povas pensi pri tri ambiguaj situacioj koncerne fsync()kiujn mi renkontis en la praktiko.

La unua tia kazo okazis en 2008. Tiam la interfaco de Firefox 3 frostiĝis se granda nombro da dosieroj estis skribitaj al disko. La problemo estis, ke la efektivigo de la interfaco uzis SQLite-datumbazon por konservi informojn pri ĝia stato. Post ĉiu ŝanĝo, kiu okazis en la interfaco, la funkcio estis nomita fsync(), kiu donis bonajn garantiojn pri stabila datumstokado. En la dosiersistemo ext3 tiam uzata, la funkcio fsync() forĵetis ĉiujn "malpurajn" paĝojn en la sistemo al disko, kaj ne nur tiujn, kiuj estis rilataj al la responda dosiero. Ĉi tio signifis, ke klaki butonon en Fajrovulpo povus ekigi megabajtojn da datumoj por esti skribitaj al magneta disko, kio povus daŭri multajn sekundojn. La solvo de la problemo, laŭ mia kompreno ĝi materialo estis transdoni laboron kun la datumbazo al nesinkronaj fonaj taskoj. Ĉi tio signifas, ke Fajrovulpo antaŭe efektivigis pli striktajn stokadpostulojn ol vere bezonataj, kaj la funkcioj de la dosiersistemo ext3 nur pligravigis ĉi tiun problemon.

La dua problemo okazis en 2009. Tiam, post sistema kraŝo, uzantoj de la nova dosiersistemo ext4 alfrontis la fakton, ke multaj lastatempe kreitaj dosieroj havis nulan longon, sed tio ne okazis kun la pli malnova dosiersistemo ext3. En la antaŭa paragrafo, mi parolis pri kiel ext3 fluigis tro da datumoj al disko, kio malrapidigis aferojn multe. fsync(). Por plibonigi la situacion, en ext4 nur tiuj malpuraj paĝoj kiuj rilatas al aparta dosiero estas fluitaj al disko. Kaj datumoj de aliaj dosieroj restas en memoro por multe pli longa tempo ol kun ext3. Ĉi tio estis farita por plibonigi rendimenton (defaŭlte, la datumoj restas en ĉi tiu stato dum 30 sekundoj, vi povas agordi ĉi tion uzante dirty_expire_centiseks; tie Vi povas trovi pliajn materialojn pri tio). Ĉi tio signifas, ke granda kvanto da datumoj povas esti nerehaveble perdita post malsukceso. La solvo al ĉi tiu problemo estas uzi fsync() en aplikoj kiuj bezonas certigi stabilan datumstokadon kaj protekti ilin kiel eble plej multe de la sekvoj de misfunkciadoj. Funkcio fsync() funkcias multe pli efike kiam oni uzas ext4 ol kiam oni uzas ext3. La malavantaĝo de ĉi tiu aliro estas, ke ĝia uzo, kiel antaŭe, malrapidigas la ekzekuton de iuj operacioj, kiel instali programojn. Vidu detalojn pri tio tie и tie.

La tria problemo koncerne fsync(), estiĝis en 2018. Tiam, kadre de la projekto PostgreSQL, oni trovis, ke se la funkcio fsync() renkontas eraron, ĝi markas "malpurajn" paĝojn kiel "purajn". Kiel rezulto, la sekvaj vokoj fsync() Ili faras nenion kun tiaj paĝoj. Pro tio, modifitaj paĝoj estas konservitaj en memoro kaj neniam estas skribitaj al disko. Ĉi tio estas vera katastrofo, ĉar la aplikaĵo pensos, ke iuj datumoj estas skribitaj al la disko, sed fakte ne estos. Tiaj malsukcesoj fsync() estas maloftaj, la apliko en tiaj situacioj povas fari preskaŭ nenion por batali la problemon. Nuntempe, kiam tio okazas, PostgreSQL kaj aliaj aplikaĵoj kraŝas. estas, en la materialo "Ĉu Aplikoj povas Reakiri de fsync Fiaskoj?", Ĉi tiu problemo estas detale esplorita. Nuntempe la plej bona solvo al ĉi tiu problemo estas uzi Rektan I/O kun la flago O_SYNC aŭ kun flago O_DSYNC. Kun ĉi tiu aliro, la sistemo raportos erarojn kiuj povas okazi dum specifaj skribaj operacioj, sed ĉi tiu aliro postulas, ke la aplikaĵo administru la bufrojn mem. Legu pli pri ĉi tio tie и tie.

Malfermante dosierojn uzante la flagojn O_SYNC kaj O_DSYNC

Ni revenu al la diskuto pri Linukso-mekanismoj, kiuj provizas stabilan datumstokadon. Nome, ni parolas pri uzado de la flago O_SYNC aŭ flago O_DSYNC kiam oni malfermas dosierojn per sistema voko malfermi(). Kun ĉi tiu aliro, ĉiu datumskriba operacio estas farita kvazaŭ post ĉiu komando write() la sistemo ricevas komandojn laŭe fsync() и fdatasync(). la Specifoj de POSIX ĉi tio nomiĝas "Synchronized I/O File Integrity Completion" kaj "Data Integrity Completion". La ĉefa avantaĝo de ĉi tiu aliro estas, ke por certigi datuman integrecon, vi bezonas nur fari unu sistemvokon, anstataŭ du (ekzemple - write() и fdatasync()). La ĉefa malavantaĝo de ĉi tiu aliro estas, ke ĉiuj skribaĵoj uzantaj la respondan dosierpriskribilon estos sinkronigitaj, kio povas limigi la kapablon strukturi la aplikan kodon.

Uzante Rektan I/O kun la flago O_DIRECT

Sistemvoko open() subtenas flagon O_DIRECT, kiu estas dizajnita por preteriri la operaciuman kaŝmemoron por elfari I/O-operaciojn per interagado rekte kun la disko. Ĉi tio, en multaj kazoj, signifas, ke skribkomandoj eldonitaj de la programo estos rekte tradukitaj en komandojn celantajn labori kun la disko. Sed, ĝenerale, ĉi tiu mekanismo ne estas anstataŭaĵo por funkcioj fsync()fdatasync(). La fakto estas, ke la disko mem povas prokrasti aŭ kaŝmemori respondaj datumoj skribaj komandoj. Kaj, por plimalbonigi la aferojn, en iuj specialaj kazoj la I/O-operacioj faritaj dum uzado de la flago O_DIRECT, elsendo en tradiciajn bufrajn operaciojn. La plej facila maniero solvi ĉi tiun problemon estas uzi la flagon por malfermi dosierojn O_DSYNC, kio signifos ke ĉiu skriboperacio estos sekvita per voko fdatasync().

Montriĝis, ke la dosiersistemo XFS ĵus aldonis "rapidan vojon" por O_DIRECT|O_DSYNC-registrado de datumoj. Se bloko estas reverkita uzante O_DIRECT|O_DSYNC, tiam XFS, anstataŭ forĵeti la kaŝmemoron, efektivigos la FUA-skriban komandon se la aparato subtenas ĝin. Mi kontrolis ĉi tion uzante la ilon blktrace sur Linukso 5.4/Ubuntu 20.04 sistemo. Tiu aliro devus esti pli efika, ĉar kiam uzite, minimuma kvanto de datenoj estas skribita al la disko kaj unu operacio estas uzita, prefere ol du (skribado kaj fluado de la kaŝmemoro). Mi trovis ligilon al flikaĵo 2018-kerno, kiu efektivigas ĉi tiun mekanismon. Estas iom da diskuto tie pri aplikado de ĉi tiu optimumigo al aliaj dosiersistemoj, sed laŭ mia scio, XFS estas la nura dosiersistemo kiu subtenas tion ĝis nun.

sync_file_range() funkcio

Linukso havas sistemvokon sync_file_range (), kiu permesas vin purigi nur parton de la dosiero al disko, prefere ol la tuta dosiero. Ĉi tiu alvoko iniciatas nesinkronan datumfluadon kaj ne atendas ke ĝi finiĝos. Sed en la atestilo sync_file_range() la teamo laŭdire estas "tre danĝera". Ne rekomendas uzi ĝin. Trajtoj kaj danĝeroj sync_file_range() tre bone priskribita en ĉi tio materialo. Specife, ĉi tiu alvoko ŝajnas uzi RocksDB por kontroli kiam la kerno fluigas malpurajn datumojn al disko. Sed samtempe, por certigi stabilan konservadon de datumoj, ĝi ankaŭ estas uzata fdatasync(). la kodo RocksDB havas kelkajn interesajn komentojn pri ĉi tiu temo. Ekzemple, ŝajnas ke la voko sync_file_range() Kiam vi uzas ZFS, ĝi ne fluigas datumojn al disko. Sperto diras al mi, ke kodo malofte uzata verŝajne enhavas cimojn. Tial mi konsilus kontraŭ uzi ĉi tiun sistemvokon krom se nepre necese.

Sistemalvokoj, kiuj helpas certigi datumpersistenton

Mi venis al la konkludo, ke ekzistas tri aliroj, kiuj povas esti uzataj por fari I/O-operaciojn, kiuj certigas datumpersistenton. Ili ĉiuj postulas funkciovokon fsync() por la dosierujo en kiu la dosiero estis kreita. Ĉi tiuj estas la aliroj:

  1. Vokado de funkcio fdatasync()fsync() post funkcio write() (estas pli bone uzi fdatasync()).
  2. Laborante kun dosierpriskribilo malfermita kun flago O_DSYNCO_SYNC (pli bone - kun flago O_DSYNC).
  3. Uzante la komandon pwritev2() kun flago RWF_DSYNCRWF_SYNC (prefere kun flago RWF_DSYNC).

Agado-Notoj

Mi ne zorge mezuris la agadon de la diversaj mekanismoj, kiujn mi ekzamenis. La diferencoj, kiujn mi rimarkis en la rapideco de ilia laboro, estas tre malgrandaj. Ĉi tio signifas, ke mi eble eraras, kaj ke sub malsamaj kondiĉoj la sama afero povas produkti malsamajn rezultojn. Unue, mi parolos pri tio, kio pli influas rendimenton, kaj poste kio influas malpli rendimenton.

  1. Anstataŭigi dosierajn datumojn estas pli rapida ol aldoni datumojn al dosiero (la rendimento-utilo povas esti 2-100%). Aldoni datumojn al dosiero postulas kromajn ŝanĝojn al la metadatenoj de la dosiero, eĉ post sistemvoko fallocate(), sed la grandeco de ĉi tiu efiko povas varii. Mi rekomendas, por plej bona agado, telefoni fallocate() por antaŭ-asigni la bezonatan spacon. Tiam ĉi tiu spaco devas esti eksplicite plenigita per nuloj kaj vokita fsync(). Ĉi tio certigos, ke la respondaj blokoj en la dosiersistemo estas markitaj kiel "asignitaj" prefere ol "neasignitaj". Ĉi tio donas malgrandan (ĉirkaŭ 2%) agado-plibonigon. Aldone, iuj diskoj povas havi pli malrapidan unuan aliron al bloko ol aliaj. Ĉi tio signifas, ke plenigi la spacon per nuloj povas konduki al signifa (ĉirkaŭ 100%) plibonigo de rendimento. Precipe, ĉi tio povas okazi kun diskoj AWS EBS (ĉi tio estas neoficiala datumo, mi ne povis konfirmi ĝin). La sama validas por stokado GCP Persistenta Disko (kaj tio jam estas oficiala informo, konfirmita de provoj). Aliaj spertuloj faris same observado, rilata al diversaj diskoj.
  2. Ju malpli da sistemvokoj, des pli alta la rendimento (la gajno povas esti ĉirkaŭ 5%). Ŝajnas defio open() kun flago O_DSYNC aŭ voku pwritev2() kun flago RWF_SYNC pli rapide ol voko fdatasync(). Mi suspektas, ke la punkto ĉi tie estas, ke ĉi tiu aliro rolas en tio, ke malpli da sistemaj vokoj devas esti faritaj por solvi la saman problemon (unu voko anstataŭ du). Sed la diferenco en rendimento estas tre malgranda, do vi povas tute ignori ĝin kaj uzi ion en la aplikaĵo, kio ne malfaciligos ĝian logikon.

Se vi interesiĝas pri la temo de daŭrigebla datumstokado, jen kelkaj utilaj materialoj:

  • I/O-Alirmetodoj — superrigardo de la bazoj de enigo/eligmekanismoj.
  • Certigi ke datumoj atingas diskon — rakonto pri kio okazas al la datumoj survoje de la aplikaĵo al la disko.
  • Kiam vi devus fsync la enhavantan dosierujon - la respondo al la demando kiam uzi fsync() por adresaroj. Por resumi ĉi tion, rezultas, ke vi devas fari tion kiam vi kreas novan dosieron, kaj la kialo de ĉi tiu rekomendo estas, ke en Linukso povas esti multaj referencoj al la sama dosiero.
  • SQL-Servilo en Linukso: FUA Internals — jen priskribo pri kiel konstanta datumstokado estas efektivigita en SQL-Servilo sur la Linukso-platformo. Estas kelkaj interesaj komparoj inter Vindozo kaj Linukso sistemvokoj ĉi tie. Mi preskaŭ certas, ke danke al ĉi tiu materialo mi eksciis pri FUA-optimumigo de XFS.

Ĉu vi perdis datumojn, kiujn vi pensis, ke vi estas sekure konservitaj sur disko?

Daŭraj Datumstokado kaj Linukso-Dosiero-APIoj

Daŭraj Datumstokado kaj Linukso-Dosiero-APIoj

fonto: www.habr.com