Kestävä tiedontallennus ja Linux-tiedostojen sovellusliittymät

Tutkiessani pilvijärjestelmien tietojen tallennuksen kestävyyttä päätin testata itseäni varmistaakseni, että ymmärrän perusasiat. minä aloitettiin lukemalla NVMe-spesifikaatio ymmärtääksemme, mitkä takeet kestävästä tiedontallennuksesta (eli takuut siitä, että tiedot ovat saatavilla järjestelmävian jälkeen) antavat meille NMVe-levyjä. Tein seuraavat tärkeimmät johtopäätökset: data on katsottava vahingoittuneeksi siitä hetkestä, kun tiedon kirjoituskomento annetaan, siihen hetkeen, kun se kirjoitetaan tallennusvälineelle. Useimmat ohjelmat käyttävät kuitenkin mielellään järjestelmäkutsuja tietojen tallentamiseen.

Tässä viestissä tutkin Linux-tiedostojen API:iden tarjoamia pysyviä tallennusmekanismeja. Näyttää siltä, ​​​​että kaiken pitäisi olla yksinkertaista: ohjelma kutsuu komennon write(), ja kun tämä komento on valmis, tiedot tallennetaan turvallisesti levylle. Mutta write() kopioi vain sovellustiedot RAM-muistissa olevaan ytimen välimuistiin. Jos haluat pakottaa järjestelmän kirjoittamaan tietoja levylle, sinun on käytettävä joitain lisämekanismeja.

Kestävä tiedontallennus ja Linux-tiedostojen sovellusliittymät

Kaiken kaikkiaan tämä materiaali on kokoelma muistiinpanoja, jotka liittyvät siihen, mitä olen oppinut minua kiinnostavasta aiheesta. Jos puhumme hyvin lyhyesti tärkeimmästä asiasta, käy ilmi, että kestävän datan tallennuksen järjestämiseksi sinun on käytettävä komentoa fdatasync() tai avaa tiedostoja lipulla O_DSYNC. Jos olet kiinnostunut oppimaan lisää siitä, mitä tapahtuu datalle sen matkalla koodista levylle, katso tämä artikkeli.

Write()-funktion käytön ominaisuudet

Järjestelmäpuhelu write() määritelty standardissa IEEE POSIX yrityksenä kirjoittaa dataa tiedostokuvaajaan. Onnistuneen suorittamisen jälkeen write() Tietojen lukutoimintojen on palautettava täsmälleen aiemmin kirjoitetut tavut, vaikka dataa käytettäisiin muista prosesseista tai säikeistä (täällä POSIX-standardin asiaankuuluva osa). Täällä, osiossa kuinka säikeet toimivat vuorovaikutuksessa normaaleiden tiedostotoimintojen kanssa, on huomautus, jonka mukaan jos kaksi säiettä kutsuu näitä toimintoja, jokaisen kutsun on nähtävä joko kaikki toisen kutsun määritetyt seuraukset tai ei ollenkaan. ei seuraukset. Tämä johtaa siihen johtopäätökseen, että kaikkien tiedostojen I/O-toimintojen on oltava lukittuna resurssille, jota ne käyttävät.

Tarkoittaako tämä, että operaatio write() onko se atomista? Tekniseltä kannalta kyllä. Tietojen lukutoimintojen on palautettava joko kaikki tai ei mitään siitä, mikä on kirjoitettu write(). Mutta operaatio write(), standardin mukaan, ei välttämättä tarvitse päättyä kirjoittamalla ylös kaikkea, mitä pyydettiin kirjoittamaan. Hän saa kirjoittaa vain osan tiedoista. Meillä voi esimerkiksi olla kaksi säiettä, joista kumpikin lisää 1024 tavua saman tiedostokuvaajan kuvaamaan tiedostoon. Standardin kannalta hyväksyttävä tulos on, kun jokainen kirjoitustoiminto voi liittää tiedostoon vain yhden tavun. Nämä toiminnot jäävät atomiksi, mutta niiden suorittamisen jälkeen tiedostoon kirjoittamansa tiedot sekoittuvat. Täällä erittäin mielenkiintoinen keskustelu tästä aiheesta Stack Overflow -sivustolla.

fsync()- ja fdatasync()-funktiot

Helpoin tapa huuhdella tiedot levylle on kutsua toiminto fsync(). Tämä toiminto pyytää käyttöjärjestelmää siirtämään kaikki muokatut lohkot välimuistista levylle. Tämä sisältää kaikki tiedostojen metatiedot (käyttöaika, tiedostojen muokkausaika ja niin edelleen). Uskon, että näitä metatietoja tarvitaan harvoin, joten jos tiedät, että ne eivät ole sinulle tärkeitä, voit käyttää toimintoa fdatasync(). Sisään auta päälle fdatasync() sanotaan, että tämän toiminnon toiminnan aikana levylle tallennetaan sellainen määrä metadataa, joka on "välttämätön seuraavien tietojen lukutoimintojen suorittamiseksi oikein". Ja tämä on juuri se, mitä useimmat sovellukset välittävät.

Yksi ongelma, joka voi syntyä tässä, on se, että nämä mekanismit eivät takaa, että tiedosto on löydettävissä mahdollisen vian jälkeen. Erityisesti uutta tiedostoa luotaessa sinun on soitettava fsync() sen sisältävälle hakemistolle. Muussa tapauksessa vian jälkeen saattaa ilmetä, että tätä tiedostoa ei ole olemassa. Syynä tähän on se, että UNIXissa tiedosto voi olla useissa hakemistoissa kovien linkkien käytön vuoksi. Siksi, kun soitat fsync() tiedosto ei voi tietää, minkä hakemiston tiedot tulee myös huuhdella levylle (täällä Voit lukea tästä lisää). Näyttää siltä, ​​​​että ext4-tiedostojärjestelmä pystyy automaattisesti käyttää fsync() vastaavat tiedostot sisältävät hakemistot, mutta näin ei välttämättä ole muissa tiedostojärjestelmissä.

Tämä mekanismi voidaan toteuttaa eri tavalla eri tiedostojärjestelmissä. käytin blktrace oppiaksesi mitä levytoimintoja käytetään ext4- ja XFS-tiedostojärjestelmissä. Molemmat antavat säännöllisiä kirjoituskomentoja levylle sekä tiedoston sisällölle että tiedostojärjestelmän päiväkirjalle, tyhjentävät välimuistin ja poistuvat suorittamalla FUA-kirjoituksen (Force Unit Access, tietojen kirjoittaminen suoraan levylle, välimuistin ohittaminen) kirjoituspäiväkirjaan. He tekevät tämän luultavasti varmistaakseen, että kauppa on tapahtunut. Asemissa, jotka eivät tue FUA:ta, tämä aiheuttaa kaksi välimuistin tyhjennystä. Kokeiluni osoittivat sen fdatasync() vähän nopeammin fsync(). Apuohjelma blktrace osoittaa sen fdatasync() kirjoittaa yleensä vähemmän tietoa levylle (ext4 fsync() kirjoittaa 20 KiB, ja fdatasync() - 16 KiB). Lisäksi huomasin, että XFS on hieman nopeampi kuin ext4. Ja tässä avulla blktrace onnistui saamaan sen selville fdatasync() huuhtelee vähemmän tietoa levylle (4 KiB XFS:ssä).

Epäselvät tilanteet, joita syntyy käytettäessä fsync()

Voin ajatella kolmea epäselvää tilannetta fsync()joihin törmäsin käytännössä.

Ensimmäinen vastaava tapaus tapahtui vuonna 2008. Sitten Firefox 3 -käyttöliittymä jumiutui, jos levylle kirjoitettiin suuri määrä tiedostoja. Ongelmana oli, että käyttöliittymän toteutus käytti SQLite-tietokantaa tallentamaan tietoja sen tilasta. Jokaisen käyttöliittymässä tapahtuneen muutoksen jälkeen funktio kutsuttiin fsync(), joka antoi hyvät takeet vakaasta tietojen tallentamisesta. Silloin käytetyssä ext3-tiedostojärjestelmässä toiminto fsync() tyhjensi kaikki järjestelmän "likaiset" sivut levylle, ei vain niitä, jotka liittyvät vastaavaan tiedostoon. Tämä tarkoitti, että painikkeen napsauttaminen Firefoxissa voi laukaista megatavujen tiedon kirjoittamisen magneettilevylle, mikä voi kestää useita sekunteja. Ratkaisu ongelmaan, sikäli kuin ymmärrän se materiaalina oli siirtää tietokannan kanssa työskentely asynkronisiin taustatehtäviin. Tämä tarkoittaa, että Firefox otti aiemmin käyttöön tiukemmat tallennusvaatimukset kuin todella tarvittiin, ja ext3-tiedostojärjestelmän ominaisuudet vain pahensivat tätä ongelmaa.

Toinen ongelma ilmeni vuonna 2009. Sitten järjestelmän kaatumisen jälkeen uuden ext4-tiedostojärjestelmän käyttäjät kohtasivat sen tosiasian, että monien uusien tiedostojen pituus oli nolla, mutta näin ei tapahtunut vanhemman ext3-tiedostojärjestelmän kanssa. Edellisessä kappaleessa puhuin siitä, kuinka ext3 huuhtoi liikaa tietoa levylle, mikä hidasti toimintaa paljon. fsync(). Tilanteen parantamiseksi ext4:ssä vain ne likaiset sivut, jotka liittyvät tiettyyn tiedostoon, huuhdellaan levylle. Ja tiedot muista tiedostoista säilyvät muistissa paljon pidempään kuin ext3:lla. Tämä tehtiin suorituskyvyn parantamiseksi (oletusarvoisesti tiedot pysyvät tässä tilassa 30 sekuntia, voit määrittää tämän käyttämällä dirty_expire_centisecs; täällä Löydät tästä lisämateriaalia). Tämä tarkoittaa, että suuri määrä tietoja voi kadota peruuttamattomasti vian jälkeen. Ratkaisu tähän ongelmaan on käyttää fsync() sovelluksissa, joiden on varmistettava vakaa tietojen tallennus ja suojattava niitä mahdollisimman hyvin vikojen seurauksilta. Toiminto fsync() toimii paljon tehokkaammin ext4:ää käytettäessä kuin ext3:a käytettäessä. Tämän lähestymistavan haittana on, että sen käyttö, kuten ennenkin, hidastaa joidenkin toimintojen suorittamista, kuten ohjelmien asennusta. Katso lisätietoja tästä täällä и täällä.

Kolmas ongelma liittyen fsync(), syntyi vuonna 2018. Sitten PostgreSQL-projektin puitteissa todettiin, että jos funktio fsync() havaitsee virheen, se merkitsee "likaiset" sivut "puhtaiksi". Tämän seurauksena seuraavat puhelut fsync() He eivät tee mitään sellaisilla sivuilla. Tämän vuoksi muokatut sivut tallennetaan muistiin, eikä niitä koskaan kirjoiteta levylle. Tämä on todellinen katastrofi, koska sovellus luulee, että joitain tietoja kirjoitetaan levylle, mutta todellisuudessa se ei ole. Sellaisia ​​epäonnistumisia fsync() ovat harvinaisia, sovellus tällaisissa tilanteissa ei voi tehdä juuri mitään ongelman torjumiseksi. Näinä päivinä, kun tämä tapahtuu, PostgreSQL ja muut sovellukset kaatuvat. Täällä, materiaalissa "Voivatko sovellukset toipua fsync-virheistä?", tätä ongelmaa tarkastellaan yksityiskohtaisesti. Tällä hetkellä paras ratkaisu tähän ongelmaan on käyttää suoraa I/O:ta lipun kanssa O_SYNC tai lipulla O_DSYNC. Tällä lähestymistavalla järjestelmä raportoi virheistä, joita saattaa ilmetä tiettyjen kirjoitustoimintojen aikana, mutta tämä lähestymistapa edellyttää, että sovellus hallitsee puskureita itse. Lue lisää tästä täällä и täällä.

Tiedostojen avaaminen O_SYNC- ja O_DSYNC-lippujen avulla

Palataan keskusteluun Linux-mekanismeista, jotka tarjoavat vakaan tiedontallennustilan. Puhumme nimittäin lipun käytöstä O_SYNC tai lippu O_DSYNC avattaessa tiedostoja järjestelmäkutsulla avata(). Tällä lähestymistavalla jokainen tietojen kirjoitustoiminto suoritetaan ikään kuin jokaisen komennon jälkeen write() järjestelmälle annetaan komennot vastaavasti fsync() и fdatasync(). Sisään POSIX tekniset tiedot tätä kutsutaan "Synchronized I/O File Integrity Completion" ja "Data Integrity Completion". Tämän lähestymistavan tärkein etu on, että tietojen eheyden varmistamiseksi sinun tarvitsee tehdä vain yksi järjestelmäkutsu kahden sijaan (esim. write() и fdatasync()). Tämän lähestymistavan suurin haittapuoli on, että kaikki vastaavaa tiedostokuvaajaa käyttävät kirjoitukset synkronoidaan, mikä voi rajoittaa kykyä jäsentää sovelluskoodia.

Suoran I/O:n käyttö O_DIRECT-lipun kanssa

Järjestelmäpuhelu open() tukee lippua O_DIRECT, joka on suunniteltu ohittamaan käyttöjärjestelmän välimuisti I/O-toimintojen suorittamiseksi vuorovaikutuksessa suoraan levyn kanssa. Tämä tarkoittaa monissa tapauksissa sitä, että ohjelman antamat kirjoituskomennot käännetään suoraan komentoiksi, joiden tarkoituksena on työskennellä levyn kanssa. Mutta yleensä tämä mekanismi ei korvaa toimintoja fsync() tai fdatasync(). Tosiasia on, että levy itse voi lykkäys tai välimuisti vastaavat tietojen kirjoituskomennot. Ja vielä pahempaa, joissakin erikoistapauksissa I/O-toiminnot suoritettiin lippua käytettäessä O_DIRECT, lähettää perinteiseen puskuroituun toimintaan. Helpoin tapa ratkaista tämä ongelma on käyttää lippua tiedostojen avaamiseen O_DSYNC, mikä tarkoittaa, että jokaista kirjoitustoimintoa seuraa kutsu fdatasync().

Kävi ilmi, että XFS-tiedostojärjestelmä oli äskettäin lisännyt "nopea polun" tiedostolle O_DIRECT|O_DSYNC-tietojen tallennus. Jos lohko kirjoitetaan uudelleen käyttämällä O_DIRECT|O_DSYNC, XFS suorittaa välimuistin tyhjentämisen sijaan FUA-kirjoituskomennon, jos laite tukee sitä. Varmistin tämän apuohjelman avulla blktrace Linux 5.4/Ubuntu 20.04 -järjestelmässä. Tämän lähestymistavan pitäisi olla tehokkaampi, koska sitä käytettäessä levylle kirjoitetaan pieni määrä tietoa ja käytetään yhtä toimintoa kahden sijaan (välimuistin kirjoittaminen ja tyhjennys). Löysin linkin läikkä 2018-ydin, joka toteuttaa tämän mekanismin. Siellä on keskustelua tämän optimoinnin soveltamisesta muihin tiedostojärjestelmiin, mutta tietääkseni XFS on toistaiseksi ainoa tiedostojärjestelmä, joka tukee tätä.

sync_file_range()-funktio

Linuxissa on järjestelmäkutsu sync_file_range(), jonka avulla voit huuhdella vain osan tiedostosta koko tiedoston sijaan. Tämä puhelu aloittaa asynkronisen tietojen huuhtelun eikä odota sen päättymistä. Mutta todistuksessa sync_file_range() joukkueen sanotaan olevan "erittäin vaarallinen". Sen käyttöä ei suositella. Ominaisuudet ja vaarat sync_file_range() erittäin hyvin kuvattu tämä materiaalia. Tarkemmin sanottuna tämä kutsu näyttää käyttävän RocksDB:tä hallitsemaan, milloin ydin huuhtelee likaiset tiedot levylle. Mutta samalla sitä käytetään myös vakaan tietojen tallennuksen varmistamiseksi fdatasync(). Sisään koodi RocksDB:llä on mielenkiintoisia kommentteja tästä aiheesta. Näyttää esimerkiksi siltä, ​​että puhelu sync_file_range() ZFS:ää käytettäessä se ei huuhtele tietoja levylle. Kokemus kertoo minulle, että harvoin käytetty koodi sisältää todennäköisesti virheitä. Siksi suosittelen olemaan käyttämättä tätä järjestelmäkutsua, ellei se ole ehdottoman välttämätöntä.

Järjestelmäkutsut, jotka auttavat varmistamaan tietojen pysyvyyden

Olen tullut siihen tulokseen, että on olemassa kolme lähestymistapaa, joilla voidaan suorittaa I/O-toimintoja, jotka varmistavat tietojen pysyvyyden. Ne kaikki vaativat funktiokutsun fsync() hakemistolle, johon tiedosto luotiin. Nämä ovat lähestymistavat:

  1. Toimintokutsu fdatasync() tai fsync() toiminnon jälkeen write() (on parempi käyttää fdatasync()).
  2. Työskentely lipulla avatun tiedostokuvaajan kanssa O_DSYNC tai O_SYNC (parempi - lipulla O_DSYNC).
  3. Komennon käyttö pwritev2() lipun kanssa RWF_DSYNC tai RWF_SYNC (mieluiten lipulla RWF_DSYNC).

Suorituskyvyn huomautukset

En ole tarkkaan mitannut tutkimieni eri mekanismien suorituskykyä. Erot, joita huomasin heidän työnsä nopeudessa, ovat hyvin pieniä. Tämä tarkoittaa, että voin olla väärässä ja että eri olosuhteissa sama asia voi tuottaa erilaisia ​​​​tuloksia. Ensin puhun siitä, mikä vaikuttaa suorituskykyyn enemmän, ja sitten mikä vaikuttaa suorituskykyyn vähemmän.

  1. Tiedostotietojen korvaaminen on nopeampaa kuin tietojen liittäminen tiedostoon (suorituskykyhyöty voi olla 2-100 %). Tietojen liittäminen tiedostoon vaatii lisämuutoksia tiedoston metatietoihin jopa järjestelmäkutsun jälkeen fallocate(), mutta tämän vaikutuksen suuruus voi vaihdella. Parhaan suorituskyvyn saavuttamiseksi suosittelen soittamista fallocate() varata tarvittava tila etukäteen. Sitten tämä tila on täytettävä eksplisiittisesti nolilla ja kutsuttava fsync(). Tämä varmistaa, että tiedostojärjestelmän vastaavat lohkot merkitään "allokoituiksi" eikä "varaamattomiksi". Tämä antaa pienen (noin 2 %) suorituskyvyn parannuksen. Lisäksi joillakin levyillä voi olla hitaampi ensimmäinen pääsy lohkoon kuin toisilla. Tämä tarkoittaa, että tilan täyttäminen nollalla voi parantaa suorituskykyä merkittävästi (noin 100 %). Tämä voi tapahtua erityisesti levyjen kanssa AWS EBS (tämä on epävirallista tietoa, en voinut vahvistaa sitä). Sama pätee varastointiin GCP-pysyvä levy (ja tämä on jo virallista tietoa, joka on vahvistettu testeillä). Muut asiantuntijat ovat tehneet samoin havainto, jotka liittyvät erilaisiin levyihin.
  2. Mitä vähemmän järjestelmäkutsuja, sitä parempi suorituskyky (vahvistus voi olla noin 5 %). Näyttää haasteelta open() lipun kanssa O_DSYNC tai soita pwritev2() lipun kanssa RWF_SYNC nopeammin kuin puhelu fdatasync(). Epäilen, että tässä on kysymys siitä, että tämä lähestymistapa vaikuttaa siihen, että saman ongelman ratkaisemiseksi on suoritettava vähemmän järjestelmäkutsuja (yksi puhelu kahden sijaan). Mutta ero suorituskyvyssä on hyvin pieni, joten voit jättää sen kokonaan huomiotta ja käyttää sovelluksessa jotain, joka ei vaikeuta sen logiikkaa.

Jos olet kiinnostunut kestävän tiedon tallennuksen aiheesta, tässä on hyödyllisiä materiaaleja:

  • I/O-käyttötavat — yleiskatsaus syöttö-/tulostusmekanismien perusteisiin.
  • Varmistetaan, että tiedot saavuttavat levyn — tarina siitä, mitä datalle tapahtuu matkalla sovelluksesta levylle.
  • Milloin fsync sisältää sisältävän hakemiston - vastaus kysymykseen, milloin käyttää fsync() hakemistoja varten. Pähkinänkuoressa selviää, että sinun on tehtävä tämä, kun luot uutta tiedostoa, ja syy tähän suositukseen on, että Linuxissa voi olla useita viittauksia samaan tiedostoon.
  • SQL Server Linuxissa: FUA Internals — Tässä on kuvaus siitä, kuinka pysyvä tiedontallennus on toteutettu SQL Serverissä Linux-alustalla. Täällä on mielenkiintoisia vertailuja Windowsin ja Linuxin järjestelmäkutsujen välillä. Olen melkein varma, että juuri tämän materiaalin ansiosta opin XFS:n FUA-optimoinnista.

Oletko menettänyt tietoja, joiden luulit olevan turvallisesti tallennettu levylle?

Kestävä tiedontallennus ja Linux-tiedostojen sovellusliittymät

Kestävä tiedontallennus ja Linux-tiedostojen sovellusliittymät

Lähde: will.com