Vzdržljivo shranjevanje podatkov in API-ji za datoteke Linux

Med raziskovanjem trajnosti shranjevanja podatkov v sistemih v oblaku sem se odločil, da se preizkusim, da se prepričam, ali razumem osnovne stvari. jaz začel z branjem specifikacije NVMe da bi razumeli, kakšna jamstva glede trajnostnega shranjevanja podatkov (torej jamstva, da bodo podatki na voljo po izpadu sistema) nam dajejo diski NMVe. Naredil sem naslednje glavne zaključke: podatke je treba šteti za poškodovane od trenutka, ko je dan ukaz za zapis podatkov, do trenutka, ko so zapisani na medij za shranjevanje. Vendar pa večina programov zelo veselo uporablja sistemske klice za beleženje podatkov.

V tej objavi raziskujem mehanizme obstojnega shranjevanja, ki jih ponujajo API-ji datotek Linux. Zdi se, da bi moralo biti tukaj vse preprosto: program pokliče ukaz write(), in po zaključku tega ukaza bodo podatki varno shranjeni na disk. Ampak write() samo kopira podatke aplikacije v predpomnilnik jedra v RAM-u. Če želite prisiliti sistem, da zapiše podatke na disk, morate uporabiti nekaj dodatnih mehanizmov.

Vzdržljivo shranjevanje podatkov in API-ji za datoteke Linux

Na splošno je to gradivo zbirka zapiskov, ki se nanašajo na to, kar sem se naučil o temi, ki me zanima. Če zelo na kratko govorimo o najpomembnejši stvari, se izkaže, da morate za organizacijo trajnostnega shranjevanja podatkov uporabiti ukaz fdatasync() ali odprite datoteke z zastavico O_DSYNC. Če vas zanima več o tem, kaj se zgodi s podatki na poti od kode do diska, si oglejte to Članek.

Značilnosti uporabe funkcije write().

Sistemski klic write() opredeljeno v standardu IEEE POSIX kot poskus zapisovanja podatkov v deskriptor datoteke. Po uspešnem zaključku write() Operacije branja podatkov morajo vrniti natanko tiste bajte, ki so bili predhodno zapisani, tudi če se do podatkov dostopa iz drugih procesov ali niti (glej ustrezni del standarda POSIX). Tukaj, v razdelku o interakciji niti z običajnimi operacijami datotek je opomba, ki pravi, da če dve niti kličeta te funkcije, potem mora vsak klic videti vse določene posledice drugega klica ali pa nobene. posledice. To vodi do zaključka, da morajo vse V/I operacije datotek imeti zaklenjen vir, na katerem delujejo.

Ali to pomeni, da operacija write() je atomsko? S tehničnega vidika ja. Operacije branja podatkov morajo vrniti vse ali nič od zapisanega write(). Toda operacija write(), v skladu s standardom, ni nujno, da se zaključi z zapisom vsega, kar je bilo zahtevano. Dovoljeno ji je napisati le del podatkov. Na primer, lahko imamo dve niti, ki dodajata 1024 bajtov v datoteko, opisano z istim deskriptorjem datoteke. Z vidika standarda bo sprejemljiv rezultat, ko lahko vsaka operacija pisanja v datoteko doda samo en bajt. Te operacije bodo ostale atomske, toda po zaključku bodo podatki, ki so jih zapisali v datoteko, pomešani. Tu zelo zanimiva razprava o tej temi na Stack Overflow.

funkciji fsync() in fdatasync().

Najlažji način za izpiranje podatkov na disk je klic funkcije fsync(). Ta funkcija od operacijskega sistema zahteva prenos vseh spremenjenih blokov iz predpomnilnika na disk. To vključuje vse metapodatke datoteke (čas dostopa, čas spreminjanja datoteke itd.). Menim, da so ti metapodatki redko potrebni, tako da, če veste, da vam niso pomembni, lahko uporabite funkcijo fdatasync(). V pomoč o fdatasync() rečeno je, da se med delovanjem te funkcije na disk shrani tolikšna količina metapodatkov, ki je »potrebna za pravilno izvajanje naslednjih operacij branja podatkov«. In prav to je tisto, kar večino aplikacij zanima.

Ena težava, ki lahko nastane pri tem, je, da ti mehanizmi ne zagotavljajo, da bo datoteka odkrita po morebitni napaki. Zlasti pri ustvarjanju nove datoteke morate poklicati fsync() za imenik, ki ga vsebuje. V nasprotnem primeru se lahko po napaki izkaže, da ta datoteka ne obstaja. Razlog za to je, da lahko v sistemu UNIX zaradi uporabe trdih povezav datoteka obstaja v več imenikih. Zato pri klicu fsync() datoteka ne more vedeti, katere podatke imenika je treba splakniti na disk (tukaj Več o tem lahko preberete). Videti je, da je datotečni sistem ext4 sposoben samodejno uporabiti fsync() v imenike, ki vsebujejo ustrezne datoteke, vendar to morda ne velja za druge datotečne sisteme.

Ta mehanizem je lahko različno implementiran v različnih datotečnih sistemih. uporabil sem blktrace če želite izvedeti, katere diskovne operacije se uporabljajo v datotečnih sistemih ext4 in XFS. Oba izdajata redne ukaze za pisanje na disk tako za vsebino datoteke kot za dnevnik datotečnega sistema, izplakneta predpomnilnik in zapreta tako, da izvedeta FUA (prisilni dostop do enote, pisanje podatkov neposredno na disk, mimo predpomnilnika) pisanje v dnevnik. Verjetno to storijo zato, da potrdijo, da je bila transakcija izvedena. Na pogonih, ki ne podpirajo FUA, to povzroči dvakratno izpiranje predpomnilnika. Moji poskusi so to pokazali fdatasync() malo hitreje fsync(). Pripomoček blktrace nakazuje, da fdatasync() običajno zapiše manj podatkov na disk (v ext4 fsync() piše 20 KiB in fdatasync() - 16 KiB). Ugotovil sem tudi, da je XFS nekoliko hitrejši od ext4. In tukaj s pomočjo blktrace uspelo ugotoviti, da fdatasync() splakne manj podatkov na disk (4 KiB v XFS).

Dvoumne situacije, ki nastanejo pri uporabi fsync()

Lahko se spomnim treh dvoumnih situacij glede fsync()s katerimi sem se srečal v praksi.

Prvi tak primer se je zgodil leta 2008. Nato je vmesnik Firefox 3 zamrznil, če je bilo na disk zapisanih veliko število datotek. Težava je bila v tem, da je implementacija vmesnika uporabljala bazo podatkov SQLite za shranjevanje informacij o svojem stanju. Po vsaki spremembi, ki se je zgodila v vmesniku, je bila funkcija poklicana fsync(), kar je dobro zagotovilo stabilno shranjevanje podatkov. V datotečnem sistemu ext3, ki se nato uporablja, funkcija fsync() je na disk odložil vse »umazane« strani v sistemu in ne le tistih, ki so bile povezane z ustrezno datoteko. To je pomenilo, da lahko klik gumba v Firefoxu sproži zapisovanje megabajtov podatkov na magnetni disk, kar lahko traja več sekund. Rešitev problema, kolikor razumem iz je material je bil prenos dela z bazo podatkov na asinhrona opravila v ozadju. To pomeni, da je Firefox prej izvajal strožje zahteve glede shranjevanja, kot je bilo v resnici potrebno, funkcije datotečnega sistema ext3 pa so to težavo le še poslabšale.

Druga težava se je pojavila leta 2009. Nato so se po sesutju sistema uporabniki novega datotečnega sistema ext4 soočili z dejstvom, da ima veliko na novo ustvarjenih datotek ničelno dolžino, vendar se to ni zgodilo pri starejšem datotečnem sistemu ext3. V prejšnjem odstavku sem govoril o tem, kako je ext3 splaknil preveč podatkov na disk, kar je zelo upočasnilo stvari. fsync(). Da bi izboljšali situacijo, se v ext4 na disk odplaknejo samo tiste umazane strani, ki so pomembne za določeno datoteko. In podatki iz drugih datotek ostanejo v pomnilniku veliko dlje kot pri ext3. To je bilo narejeno za izboljšanje zmogljivosti (privzeto podatki ostanejo v tem stanju 30 sekund, to lahko konfigurirate z dirty_expire_centisecs; tukaj O tem lahko najdete dodatna gradiva). To pomeni, da se lahko po okvari velika količina podatkov nepovratno izgubi. Rešitev te težave je uporaba fsync() v aplikacijah, ki morajo zagotoviti stabilno shranjevanje podatkov in jih v največji možni meri zaščititi pred posledicami okvar. funkcija fsync() deluje veliko bolj učinkovito pri uporabi ext4 kot pri uporabi ext3. Pomanjkljivost tega pristopa je, da njegova uporaba, kot prej, upočasni izvajanje nekaterih operacij, kot je namestitev programov. Oglejte si podrobnosti o tem tukaj и tukaj.

Tretji problem glede fsync(), nastala leta 2018. Nato je bilo v okviru projekta PostgreSQL ugotovljeno, da če funkcija fsync() naleti na napako, "umazane" strani označi kot "čiste". Kot rezultat, naslednji klici fsync() S takimi stranmi ne naredijo ničesar. Zaradi tega so spremenjene strani shranjene v pomnilniku in se nikoli ne zapišejo na disk. To je prava katastrofa, saj bo aplikacija mislila, da so nekateri podatki zapisani na disk, v resnici pa ne bodo. Takšne neuspehe fsync() so redki, aplikacija v takšnih situacijah ne more storiti skoraj nič v boju proti težavi. Dandanes, ko se to zgodi, se PostgreSQL in druge aplikacije zrušijo. Tukaj, v gradivu »Ali se lahko aplikacije obnovijo po okvarah fsync?« je ta težava podrobno raziskana. Trenutno je najboljša rešitev te težave uporaba neposrednega V/I z zastavico O_SYNC ali z zastavo O_DSYNC. S tem pristopom bo sistem poročal o napakah, ki se lahko pojavijo med določenimi operacijami pisanja, vendar ta pristop zahteva, da aplikacija sama upravlja medpomnilnike. Preberite več o tem tukaj и tukaj.

Odpiranje datotek z uporabo zastavic O_SYNC in O_DSYNC

Vrnimo se k razpravi o mehanizmih Linuxa, ki zagotavljajo stabilno shranjevanje podatkov. Govorimo namreč o uporabi zastave O_SYNC ali zastava O_DSYNC pri odpiranju datotek s sistemskim klicem odprto(). S tem pristopom se vsaka operacija zapisovanja podatkov izvede kot po vsakem ukazu write() sistem dobi ustrezne ukaze fsync() и fdatasync(). V POSIX specifikacije to se imenuje "Sinhronizirano dokončanje celovitosti V/I datoteke" in "Dokončanje celovitosti podatkov". Glavna prednost tega pristopa je, da morate za zagotovitev celovitosti podatkov opraviti samo en sistemski klic namesto dveh (na primer - write() и fdatasync()). Glavna pomanjkljivost tega pristopa je, da bodo vsa pisanja z uporabo ustreznega deskriptorja datoteke sinhronizirana, kar lahko omeji zmožnost strukturiranja kode aplikacije.

Uporaba neposrednega V/I z zastavico O_DIRECT

Sistemski klic open() podpira zastavo O_DIRECT, ki je zasnovan tako, da obide predpomnilnik operacijskega sistema za izvajanje V/I operacij z neposredno interakcijo z diskom. To v mnogih primerih pomeni, da bodo ukazi za pisanje, ki jih izda program, neposredno prevedeni v ukaze, namenjene delu z diskom. Toda na splošno ta mehanizem ni nadomestilo za funkcije fsync() ali fdatasync(). Dejstvo je, da lahko sam disk odložiti ali predpomniti ustrezne ukaze za pisanje podatkov. In, kar je še hujše, v nekaterih posebnih primerih V/I operacije, izvedene pri uporabi zastavice O_DIRECT, oddaja v tradicionalne medpomnjene operacije. Najlažji način za rešitev te težave je uporaba zastavice za odpiranje datotek O_DSYNC, kar bo pomenilo, da bo vsaki operaciji pisanja sledil klic fdatasync().

Izkazalo se je, da je datotečni sistem XFS nedavno dodal "hitro pot" za O_DIRECT|O_DSYNC- snemanje podatkov. Če je blok prepisan z uporabo O_DIRECT|O_DSYNC, potem bo XFS namesto izpiranja predpomnilnika izvedel ukaz za pisanje FUA, če ga naprava podpira. To sem preveril s pomočjo pripomočka blktrace v sistemu Linux 5.4/Ubuntu 20.04. Ta pristop bi moral biti učinkovitejši, saj se pri uporabi na disk zapiše minimalna količina podatkov in se uporabi ena operacija namesto dveh (zapisovanje in izpiranje predpomnilnika). Našel sem povezavo do obliž 2018 jedro, ki izvaja ta mehanizem. Obstaja nekaj razprav o uporabi te optimizacije za druge datotečne sisteme, a kolikor vem, je XFS edini datotečni sistem, ki to podpira.

funkcijo sync_file_range().

Linux ima sistemski klic sync_file_range(), ki vam omogoča, da na disk izbrišete le del datoteke, namesto celotne datoteke. Ta klic sproži asinhrono izpiranje podatkov in ne čaka, da se zaključi. Toda v potrdilu sync_file_range() ekipa naj bi bila "zelo nevarna". Ni ga priporočljivo uporabljati. Lastnosti in nevarnosti sync_file_range() zelo dobro opisano v to material. Natančneje, zdi se, da ta klic uporablja RocksDB za nadzor, kdaj jedro izpira umazane podatke na disk. Toda hkrati se uporablja tudi za zagotovitev stabilnega shranjevanja podatkov fdatasync(). V Koda RocksDB ima nekaj zanimivih komentarjev na to temo. Na primer, zdi se, da je klic sync_file_range() Ko uporabljate ZFS, ne izpira podatkov na disk. Izkušnje mi pravijo, da koda, ki se redko uporablja, verjetno vsebuje napake. Zato odsvetujem uporabo tega sistemskega klica, razen če je to nujno potrebno.

Sistemski klici, ki pomagajo zagotoviti obstojnost podatkov

Prišel sem do zaključka, da obstajajo trije pristopi, ki jih je mogoče uporabiti za izvajanje V/I operacij, ki zagotavljajo obstojnost podatkov. Vsi zahtevajo klic funkcije fsync() za imenik, v katerem je bila datoteka ustvarjena. To so pristopi:

  1. Klic funkcije fdatasync() ali fsync() po funkciji write() (bolje je uporabiti fdatasync()).
  2. Delo z deskriptorjem datoteke, odprto z zastavico O_DSYNC ali O_SYNC (bolje - z zastavo O_DSYNC).
  3. Uporaba ukaza pwritev2() z zastavo RWF_DSYNC ali RWF_SYNC (po možnosti z zastavo RWF_DSYNC).

Opombe o uspešnosti

Nisem skrbno izmeril delovanja različnih mehanizmov, ki sem jih pregledal. Razlike, ki sem jih opazil v hitrosti njihovega dela, so zelo majhne. To pomeni, da se lahko motim in da lahko ista stvar pod različnimi pogoji povzroči različne rezultate. Najprej bom govoril o tem, kaj bolj vpliva na uspešnost in nato kaj manj.

  1. Prepisovanje podatkov datoteke je hitrejše kot dodajanje podatkov datoteki (izboljšanje zmogljivosti je lahko 2-100 %). Dodajanje podatkov v datoteko zahteva dodatne spremembe metapodatkov datoteke, tudi po sistemskem klicu fallocate(), vendar se obseg tega učinka lahko razlikuje. Za najboljšo učinkovitost priporočam, da pokličete fallocate() vnaprej dodelite potreben prostor. Nato je treba ta prostor eksplicitno zapolniti z ničlami ​​in poklicati fsync(). To bo zagotovilo, da bodo ustrezni bloki v datotečnem sistemu označeni kot "dodeljeni" in ne kot "nedodeljeni". To daje majhno (približno 2 %) izboljšanje zmogljivosti. Poleg tega imajo lahko nekateri diski počasnejši prvi dostop do bloka kot drugi. To pomeni, da lahko polnjenje prostora z ničlami ​​vodi do znatnega (približno 100 %) izboljšanja delovanja. Še posebej se to lahko zgodi pri diskih AWS EBS (to so neuradni podatki, nisem jih mogel potrditi). Enako velja za shranjevanje Vztrajni disk GCP (in to je že uradna informacija, potrjena s testi). Drugi strokovnjaki so storili enako opazovanje, povezanih z različnimi diski.
  2. Manj kot je sistemskih klicev, večja je zmogljivost (dobitek je lahko približno 5 %). Izgleda kot izziv open() z zastavo O_DSYNC ali pokličite pwritev2() z zastavo RWF_SYNC hitreje kot klic fdatasync(). Sumim, da je bistvo tukaj v tem, da ta pristop igra vlogo pri dejstvu, da je treba izvesti manj sistemskih klicev za rešitev iste težave (en klic namesto dveh). Toda razlika v zmogljivosti je zelo majhna, zato jo lahko popolnoma zanemarite in v aplikaciji uporabite nekaj, kar ne bo kompliciralo njene logike.

Če vas zanima tema trajnostnega shranjevanja podatkov, je tu nekaj uporabnih gradiv:

  • V/I dostopne metode — pregled osnov vhodno/izhodnih mehanizmov.
  • Zagotavljanje, da podatki dosežejo disk — zgodba o tem, kaj se zgodi s podatki na poti od aplikacije do diska.
  • Kdaj naj fsinhronizirate vsebovalni imenik - odgovor na vprašanje, kdaj uporabiti fsync() za imenike. Na kratko, izkazalo se je, da morate to storiti, ko ustvarjate novo datoteko, in razlog za to priporočilo je, da je v Linuxu lahko veliko sklicev na isto datoteko.
  • SQL Server v sistemu Linux: notranji elementi FUA — tukaj je opis, kako je vztrajno shranjevanje podatkov implementirano v SQL Server na platformi Linux. Tukaj je nekaj zanimivih primerjav med sistemskimi klici Windows in Linux. Skoraj prepričan sem, da sem zahvaljujoč temu materialu izvedel za FUA optimizacijo XFS.

Ste izgubili podatke, za katere ste mislili, da so varno shranjeni na disku?

Vzdržljivo shranjevanje podatkov in API-ji za datoteke Linux

Vzdržljivo shranjevanje podatkov in API-ji za datoteke Linux

Vir: www.habr.com