Duursame databerging en Linux-lêer-API's

Terwyl ek die volhoubaarheid van databerging in wolkstelsels ondersoek het, het ek besluit om myself te toets om seker te maak dat ek die basiese dinge verstaan. ek begin deur die NVMe-spesifikasie te lees om te verstaan ​​watter waarborge met betrekking tot volhoubare databerging (dit wil sê waarborge dat die data beskikbaar sal wees na 'n stelselfout) vir ons NMVe-skywe gee. Ek het die volgende hoofafleidings gemaak: data moet as beskadig beskou word vanaf die oomblik dat die opdrag om data te skryf gegee word tot die oomblik dat dit na die stoormedium geskryf word. Die meeste programme gebruik egter heel gelukkig stelseloproepe om data op te neem.

In hierdie pos ondersoek ek die aanhoudende bergingsmeganismes wat deur die Linux-lêer-API's verskaf word. Dit blyk dat alles hier eenvoudig moet wees: die program roep die opdrag write(), en nadat hierdie opdrag voltooi is, sal die data veilig op skyf gestoor word. Maar write() kopieer slegs toepassingsdata na die kernkas wat in RAM geleë is. Om die stelsel te dwing om data na skyf te skryf, moet jy 'n paar bykomende meganismes gebruik.

Duursame databerging en Linux-lêer-API's

Oor die algemeen is hierdie materiaal 'n versameling notas wat verband hou met wat ek geleer het oor 'n onderwerp wat vir my belangstel. As ons baie kortliks oor die belangrikste ding praat, blyk dit dat jy die opdrag moet gebruik om volhoubare databerging te organiseer fdatasync() of maak lêers oop met die vlag O_DSYNC. As jy belangstel om meer te wete te kom oor wat gebeur met data op pad van kode na skyf, kyk na hierdie artikel.

Kenmerke van die gebruik van die skryf() funksie

Stelseloproep write() in die standaard gedefinieer IEEE POSIX as 'n poging om data na 'n lêerbeskrywer te skryf. Na suksesvolle voltooiing write() Dataleesbewerkings moet presies die grepe terugstuur wat voorheen geskryf is, selfs al word toegang tot die data verkry vanaf ander prosesse of drade (hier relevante afdeling van die POSIX-standaard). Hier, in die afdeling oor hoe drade in wisselwerking tree met normale lêerbewerkings, is daar 'n nota wat sê dat as twee drade elk hierdie funksies oproep, dan moet elke oproep óf al die aangewese gevolge van die ander oproep sien, óf glad nie. gevolge. Dit lei tot die gevolgtrekking dat alle lêer I/O-bewerkings 'n slot moet hou op die hulpbron waarop hulle werk.

Beteken dit dat die operasie write() is dit atoom? Uit 'n tegniese oogpunt, ja. Dataleesbewerkings moet óf alles óf niks terugstuur van waarmee geskryf is nie write(). Maar die operasie write(), volgens die standaard, hoef nie noodwendig te eindig deur alles neer te skryf wat gevra is om neer te skryf nie. Sy word toegelaat om slegs 'n deel van die data te skryf. Ons kan byvoorbeeld twee drade hê wat elk 1024 grepe by 'n lêer voeg wat deur dieselfde lêerbeskrywer beskryf word. Uit die oogpunt van die standaard sal 'n aanvaarbare resultaat wees wanneer elke skryfbewerking slegs een greep by die lêer kan voeg. Hierdie bewerkings sal atoom bly, maar nadat hulle voltooi is, sal die data wat hulle na die lêer geskryf het deurmekaar wees. Hier baie interessante bespreking oor hierdie onderwerp op Stack Overflow.

fsync() en fdatasync() funksies

Die maklikste manier om data na skyf te spoel, is om die funksie te roep fsync(). Hierdie funksie vra die bedryfstelsel om alle gewysigde blokke van die kas na skyf oor te dra. Dit sluit alle lêermetadata in (toegangstyd, lêerwysigingstyd, ensovoorts). Ek glo dat hierdie metadata selde nodig is, so as jy weet dat dit nie vir jou belangrik is nie, kan jy die funksie gebruik fdatasync(). In hulp op fdatasync() daar word gesê dat tydens die werking van hierdie funksie so 'n hoeveelheid metadata op skyf gestoor word wat "nodig is vir die korrekte uitvoering van die volgende data-leesbewerkings." En dit is presies waaroor die meeste toepassings omgee.

Een probleem wat hier kan ontstaan, is dat hierdie meganismes nie waarborg dat die lêer na 'n moontlike mislukking opgespoor sal kan word nie. In die besonder, wanneer jy 'n nuwe lêer skep, moet jy bel fsync() vir die gids wat dit bevat. Andersins, na 'n mislukking, kan dit blyk dat hierdie lêer nie bestaan ​​nie. Die rede hiervoor is dat in UNIX, as gevolg van die gebruik van harde skakels, 'n lêer in verskeie gidse kan bestaan. Daarom, wanneer u bel fsync() daar is geen manier vir 'n lêer om te weet watter gidsdata ook na skyf gespoel moet word nie (hier Jy kan meer hieroor lees). Dit lyk of die ext4-lêerstelsel daartoe in staat is outomaties gebruik fsync() na die gidse wat die ooreenstemmende lêers bevat, maar dit is dalk nie die geval met ander lêerstelsels nie.

Hierdie meganisme kan verskillend op verskillende lêerstelsels geïmplementeer word. ek het gebruik blktrace om te leer oor watter skyfbewerkings in ext4- en XFS-lêerstelsels gebruik word. Beide reik gereelde skryfopdragte na skyf uit vir beide die lêerinhoud en die lêerstelseljoernaal, spoel die kas uit, en gaan uit deur 'n FUA (Force Unit Access, skryf data direk na skyf, omseil die kas) skryf na die joernaal uit te voer. Hulle doen dit waarskynlik om te bevestig dat die transaksie plaasgevind het. Op aandrywers wat nie FUA ondersteun nie, veroorsaak dit twee kasspoelings. My eksperimente het dit gewys fdatasync() 'n bietjie vinniger fsync(). Nut blktrace dui daarop aan fdatasync() skryf gewoonlik minder data na skyf (in ext4 fsync() skryf 20 KiB, en fdatasync() - 16 KiB). Ek het ook uitgevind dat XFS effens vinniger is as ext4. En hier met die hulp blktrace daarin geslaag om dit uit te vind fdatasync() spoel minder data na skyf (4 KiB in XFS).

Dubbelsinnige situasies wat ontstaan ​​wanneer fsync() gebruik word

Ek kan aan drie dubbelsinnige situasies dink t.o.v fsync()wat ek in die praktyk teëgekom het.

Die eerste so 'n geval het in 2008 plaasgevind. Toe het die Firefox 3-koppelvlak gevries as 'n groot aantal lêers na skyf geskryf is. Die probleem was dat die implementering van die koppelvlak 'n SQLite-databasis gebruik het om inligting oor sy toestand te stoor. Na elke verandering wat in die koppelvlak plaasgevind het, is die funksie opgeroep fsync(), wat goeie waarborge vir stabiele databerging gegee het. In die ext3-lêerstelsel wat toe gebruik is, is die funksie fsync() het alle "vuil" bladsye in die stelsel na skyf gestort, en nie net dié wat verband hou met die ooreenstemmende lêer nie. Dit het beteken dat die klik van 'n knoppie in Firefox kan veroorsaak dat megagrepe data na 'n magnetiese skyf geskryf word, wat baie sekondes kan neem. Die oplossing vir die probleem, sover ek verstaan ​​van van hierdie materiaal was om werk met die databasis oor te dra na asinchroniese agtergrondtake. Dit beteken dat Firefox voorheen strenger bergingsvereistes geïmplementeer het as wat werklik nodig was, en die kenmerke van die ext3-lêerstelsel het hierdie probleem net vererger.

Die tweede probleem het in 2009 voorgekom. Toe, na 'n stelselongeluk, het gebruikers van die nuwe ext4-lêerstelsel gekonfronteer met die feit dat baie nuutgeskepte lêers geen lengte gehad het nie, maar dit het nie met die ouer ext3-lêerstelsel gebeur nie. In die vorige paragraaf het ek gepraat oor hoe ext3 te veel data na skyf gespoel het, wat dinge baie vertraag het. fsync(). Om die situasie te verbeter, word in ext4 slegs daardie vuil bladsye wat relevant is vir 'n spesifieke lêer na skyf gespoel. En data van ander lêers bly vir 'n baie langer tyd in die geheue as met ext3. Dit is gedoen om werkverrigting te verbeter (by verstek bly die data vir 30 sekondes in hierdie toestand, jy kan dit opstel met vuil_verval_centisecs; hier Jy kan addisionele materiaal hieroor vind). Dit beteken dat 'n groot hoeveelheid data onherstelbaar verlore kan gaan na 'n mislukking. Die oplossing vir hierdie probleem is om te gebruik fsync() in toepassings wat stabiele databerging moet verseker en dit so veel as moontlik moet beskerm teen die gevolge van mislukkings. Funksie fsync() werk baie meer doeltreffend wanneer u ext4 gebruik as wanneer u ext3 gebruik. Die nadeel van hierdie benadering is dat die gebruik daarvan, soos voorheen, die uitvoering van sommige bewerkings, soos die installering van programme, vertraag. Sien besonderhede hieroor hier и hier.

Die derde probleem t.o.v fsync(), het in 2018 ontstaan. Toe, binne die raamwerk van die PostgreSQL-projek, is gevind dat as die funksie fsync() 'n fout teëkom, merk dit "vuil" bladsye as "skoon". As gevolg hiervan, die volgende oproepe fsync() Hulle doen niks met sulke bladsye nie. As gevolg hiervan word gewysigde bladsye in die geheue gestoor en word nooit na skyf geskryf nie. Dit is 'n ware ramp, aangesien die toepassing sal dink dat sommige data op die skyf geskryf is, maar dit sal in werklikheid nie wees nie. Sulke mislukkings fsync() skaars is, kan die toepassing in sulke situasies byna niks doen om die probleem te bekamp nie. Deesdae, wanneer dit gebeur, crash PostgreSQL en ander toepassings. Hier, in die materiaal "Kan toepassings herstel van fsync-foute?", word hierdie probleem in detail ondersoek. Tans is die beste oplossing vir hierdie probleem om Direct I/O met die vlag te gebruik O_SYNC of met 'n vlag O_DSYNC. Met hierdie benadering sal die stelsel foute rapporteer wat tydens spesifieke skryfbewerkings mag voorkom, maar hierdie benadering vereis dat die toepassing self die buffers bestuur. Lees meer hieroor hier и hier.

Maak lêers oop met die O_SYNC- en O_DSYNC-vlae

Kom ons keer terug na die bespreking van Linux-meganismes wat stabiele databerging verskaf. Ons praat naamlik van die gebruik van die vlag O_SYNC of vlag O_DSYNC wanneer lêers oopgemaak word met behulp van stelseloproep oop(). Met hierdie benadering word elke dataskryfbewerking uitgevoer asof na elke opdrag write() die stelsel kry dienooreenkomstig opdragte fsync() и fdatasync(). In POSIX spesifikasies dit word "Sinchronized I/O File Integrity Completion" en "Data Integrity Completion" genoem. Die grootste voordeel van hierdie benadering is dat om data-integriteit te verseker, jy net een stelseloproep hoef te maak, eerder as twee (byvoorbeeld - write() и fdatasync()). Die grootste nadeel van hierdie benadering is dat alle skryfwerk wat die ooreenstemmende lêerbeskrywer gebruik, gesinchroniseer sal word, wat die vermoë om die toepassingskode te struktureer kan beperk.

Gebruik Direct I/O met die O_DIRECT vlag

Stelseloproep open() ondersteun vlag O_DIRECT, wat ontwerp is om die bedryfstelsel-kas te omseil om I/O-bewerkings uit te voer deur direk met die skyf te kommunikeer. Dit beteken in baie gevalle dat skryfopdragte wat deur die program uitgereik word direk vertaal sal word in opdragte wat daarop gemik is om met die skyf te werk. Maar in die algemeen is hierdie meganisme nie 'n plaasvervanger vir funksies nie fsync() of fdatasync(). Die feit is dat die skyf self kan uitstel of kas ooreenstemmende dataskryfopdragte. En, om sake te vererger, in sommige spesiale gevalle die I/O-bewerkings wat uitgevoer word wanneer die vlag gebruik word O_DIRECT, uitsaai in tradisionele gebufferde bedrywighede. Die maklikste manier om hierdie probleem op te los, is om die vlag te gebruik om lêers oop te maak O_DSYNC, wat sal beteken dat elke skryfbewerking deur 'n oproep gevolg sal word fdatasync().

Dit het geblyk dat die XFS-lêerstelsel onlangs 'n "vinnige pad" vir O_DIRECT|O_DSYNC-data opname. As 'n blok herskryf word met behulp van O_DIRECT|O_DSYNC, dan sal XFS, in plaas daarvan om die kas te spoel, die FUA-skryfopdrag uitvoer as die toestel dit ondersteun. Ek het dit geverifieer deur die hulpprogram te gebruik blktrace op 'n Linux 5.4/Ubuntu 20.04-stelsel. Hierdie benadering behoort meer doeltreffend te wees, want wanneer dit gebruik word, word 'n minimale hoeveelheid data na die skyf geskryf en een bewerking word gebruik, eerder as twee (skryf en spoel die kas). Ek het 'n skakel gevind na kol 2018 kern, wat hierdie meganisme implementeer. Daar is 'n bespreking daar oor die toepassing van hierdie optimering op ander lêerstelsels, maar sover ek weet, is XFS die enigste lêerstelsel wat dit tot dusver ondersteun.

sync_file_range() funksie

Linux het 'n stelseloproep sync_file_range(), wat jou toelaat om slegs 'n deel van die lêer na skyf te spoel, eerder as die hele lêer. Hierdie oproep begin 'n asynchrone dataspoeling en wag nie vir dit om te voltooi nie. Maar in die sertifikaat sync_file_range() daar word gesê dat die span "baie gevaarlik" is. Dit word nie aanbeveel om dit te gebruik nie. Eienskappe en gevare sync_file_range() baie goed beskryf in hierdie materiaal. Spesifiek, dit blyk dat hierdie oproep RocksDB gebruik om te beheer wanneer die kern vuil data na skyf spoel. Maar terselfdertyd, om stabiele databerging te verseker, word dit ook gebruik fdatasync(). In kode RocksDB het 'n paar interessante opmerkings oor hierdie onderwerp. Dit blyk byvoorbeeld dat die oproep sync_file_range() Wanneer ZFS gebruik word, spoel dit nie data na skyf nie. Ervaring vertel my dat kode wat selde gebruik word, waarskynlik foute sal bevat. Daarom sal ek afraai om hierdie stelseloproep te gebruik, tensy dit absoluut noodsaaklik is.

Stelseloproepe wat help om databestendigheid te verseker

Ek het tot die gevolgtrekking gekom dat daar drie benaderings is wat gebruik kan word om I/O-bewerkings uit te voer wat die volharding van data verseker. Hulle benodig almal 'n funksie-oproep fsync() vir die gids waarin die lêer geskep is. Dit is die benaderings:

  1. Bel 'n funksie fdatasync() of fsync() na funksie write() (dit is beter om te gebruik fdatasync()).
  2. Werk met 'n lêerbeskrywer wat met 'n vlag oopgemaak is O_DSYNC of O_SYNC (beter - met 'n vlag O_DSYNC).
  3. Gebruik die opdrag pwritev2() met vlag RWF_DSYNC of RWF_SYNC (verkieslik met 'n vlag RWF_DSYNC).

Prestasienotas

Ek het nie die werkverrigting van die verskillende meganismes wat ek ondersoek het noukeurig gemeet nie. Die verskille wat ek opgemerk het in die spoed van hul werk is baie klein. Dit beteken dat ek verkeerd kan wees, en dat dieselfde ding onder verskillende omstandighede verskillende resultate kan lewer. Eerstens sal ek praat oor wat prestasie meer beïnvloed, en dan wat prestasie minder beïnvloed.

  1. Om lêerdata te oorskryf is vinniger as om data by 'n lêer te voeg (die prestasievoordeel kan 2-100% wees). Om data by 'n lêer te voeg, vereis bykomende veranderinge aan die lêer se metadata, selfs na 'n stelseloproep fallocate(), maar die omvang van hierdie effek kan verskil. Ek beveel aan om vir die beste prestasie te bel fallocate() om die vereiste spasie vooraf toe te wys. Dan moet hierdie spasie uitdruklik met nulle gevul word en genoem word fsync(). Dit sal verseker dat die ooreenstemmende blokke in die lêerstelsel gemerk word as "toegewys" eerder as "ongeallokeer". Dit gee 'n klein (ongeveer 2%) prestasieverbetering. Daarbenewens kan sommige skywe 'n stadiger eerste toegang tot 'n blok hê as ander. Dit beteken dat die vul van die spasie met nulle kan lei tot 'n aansienlike (ongeveer 100%) verbetering in prestasie. Dit kan veral met skywe gebeur AWS EBS (dit is nie-amptelike data, ek kon dit nie bevestig nie). Dieselfde geld vir berging GCP aanhoudende skyf (en dit is reeds amptelike inligting, bevestig deur toetse). Ander kenners het dieselfde gedoen waarneming, wat verband hou met verskeie skywe.
  2. Hoe minder stelseloproepe, hoe hoër is die werkverrigting (die wins kan ongeveer 5%). Lyk na 'n uitdaging open() met vlag O_DSYNC of bel pwritev2() met vlag RWF_SYNC vinniger as 'n oproep fdatasync(). Ek vermoed dat die punt hier is dat hierdie benadering 'n rol speel in die feit dat minder stelseloproepe uitgevoer moet word om dieselfde probleem op te los (een oproep in plaas van twee). Maar die verskil in prestasie is baie klein, so jy kan dit heeltemal ignoreer en iets in die toepassing gebruik wat nie die logika daarvan sal bemoeilik nie.

As jy belangstel in die onderwerp van volhoubare databerging, is hier 'n paar nuttige materiaal:

  • I/O Toegang metodes — oorsig van die basiese beginsels van toevoer/afvoermeganismes.
  • Verseker dat data skyf bereik — 'n storie oor wat met die data gebeur op pad van die toepassing na die skyf.
  • Wanneer moet jy die gids wat bevat fsinc - die antwoord op die vraag wanneer om te gebruik fsync() vir gidse. Om dit in 'n neutedop te stel, blyk dit dat jy dit moet doen wanneer jy 'n nuwe lêer skep, en die rede vir hierdie aanbeveling is dat daar in Linux baie verwysings na dieselfde lêer kan wees.
  • SQL Server op Linux: FUA Internals - hier is 'n beskrywing van hoe aanhoudende databerging in SQL Server op die Linux-platform geïmplementeer word. Daar is 'n paar interessante vergelykings tussen Windows- en Linux-stelseloproepe hier. Ek is amper seker dat dit danksy hierdie materiaal was dat ek geleer het oor FUA-optimering van XFS.

Het jy data verloor wat jy gedink het veilig op 'n skyf gestoor is?

Duursame databerging en Linux-lêer-API's

Duursame databerging en Linux-lêer-API's

Bron: will.com