Slitesterk datalagring og Linux File APIer

Mens jeg undersøkte bærekraften til datalagring i skysystemer, bestemte jeg meg for å teste meg selv for å være sikker på at jeg forsto de grunnleggende tingene. Jeg startet med å lese NVMe-spesifikasjonen for å forstå hvilke garantier angående bærekraftig datalagring (det vil si garantier for at dataene vil være tilgjengelige etter en systemfeil) som gis til oss av NMVe-disker. Jeg kom med følgende hovedkonklusjoner: data må anses som skadet fra det øyeblikket kommandoen for å skrive data gis til det øyeblikket det skrives til lagringsmediet. Imidlertid bruker de fleste programmer ganske fornøyd systemanrop for å registrere data.

I dette innlegget utforsker jeg de vedvarende lagringsmekanismene som tilbys av Linux-fil-API-ene. Det ser ut til at alt skal være enkelt her: programmet kaller kommandoen write(), og etter at denne kommandoen er fullført, lagres dataene sikkert på disken. Men write() kopierer kun programdata til kjernebufferen som ligger i RAM. For å tvinge systemet til å skrive data til disk, må du bruke noen ekstra mekanismer.

Slitesterk datalagring og Linux File APIer

Totalt sett er dette materialet en samling av notater knyttet til det jeg har lært om et emne som interesserer meg. Hvis vi snakker veldig kort om det viktigste, viser det seg at for å organisere bærekraftig datalagring må du bruke kommandoen fdatasync() eller åpne filer med flagget O_DSYNC. Hvis du er interessert i å lære mer om hva som skjer med data på vei fra kode til disk, ta en titt på dette artikkel.

Funksjoner ved bruk av skrive()-funksjonen

Systemanrop write() definert i standarden IEEE POSIX som et forsøk på å skrive data til en filbeskrivelse. Etter vellykket gjennomføring write() Dataleseoperasjoner må returnere nøyaktig de bytene som tidligere ble skrevet, og gjøre dette selv om dataene er aksessert fra andre prosesser eller tråder (her relevant del av POSIX-standarden). Her, i seksjonen om hvordan tråder samhandler med vanlige filoperasjoner, er det en merknad som sier at hvis to tråder hver kaller disse funksjonene, må hvert anrop enten se alle de angitte konsekvensene av det andre anropet, eller ingen i det hele tatt. konsekvenser. Dette fører til konklusjonen at alle fil-I/O-operasjoner må holde en lås på ressursen de opererer på.

Betyr dette at operasjonen write() er det atomært? Fra et teknisk synspunkt, ja. Dataleseoperasjoner må returnere enten alt eller ingenting av det som ble skrevet med write(). Men operasjonen write(), ifølge standarden, trenger ikke nødvendigvis å avslutte med å skrive ned alt den ble bedt om å skrive ned. Hun har bare lov til å skrive en del av dataene. For eksempel kan vi ha to tråder som hver legger til 1024 byte til en fil beskrevet av den samme filbeskrivelsen. Fra standardens synspunkt vil et akseptabelt resultat være når hver skriveoperasjon kan legge til bare én byte til filen. Disse operasjonene vil forbli atomære, men etter at de er fullført, vil dataene de skrev til filen blandes sammen. Her veldig interessant diskusjon om dette emnet på Stack Overflow.

fsync() og fdatasync() funksjoner

Den enkleste måten å skylle data til disk er å kalle opp funksjonen fsync(). Denne funksjonen ber operativsystemet overføre alle modifiserte blokker fra cachen til disken. Dette inkluderer alle filmetadata (tilgangstid, filmodifiseringstid og så videre). Jeg tror at disse metadataene sjelden er nødvendige, så hvis du vet at det ikke er viktig for deg, kan du bruke funksjonen fdatasync(). I hjelpfdatasync() det sies at under driften av denne funksjonen lagres en slik mengde metadata på disken som er "nødvendig for riktig utførelse av følgende dataleseoperasjoner." Og det er akkurat dette de fleste applikasjoner bryr seg om.

Et problem som kan oppstå her er at disse mekanismene ikke garanterer at filen vil bli oppdaget etter en mulig feil. Spesielt når du oppretter en ny fil, må du ringe fsync() for katalogen som inneholder den. Ellers, etter en feil, kan det vise seg at denne filen ikke eksisterer. Grunnen til dette er at i UNIX, på grunn av bruken av harde lenker, kan en fil eksistere i flere kataloger. Derfor, når du ringer fsync() det er ingen måte for en fil å vite hvilke katalogdata som også skal tømmes til disk (her Du kan lese mer om dette). Det ser ut som ext4-filsystemet er i stand til automatisk gjelder fsync() til katalogene som inneholder de tilsvarende filene, men dette er kanskje ikke tilfellet med andre filsystemer.

Denne mekanismen kan implementeres forskjellig på forskjellige filsystemer. jeg brukte blktrace for å lære om hvilke diskoperasjoner som brukes i filsystemer ext4 og XFS. Begge gir vanlige skrivekommandoer til disk for både filinnholdet og filsystemjournalen, tømme hurtigbufferen og avslutte ved å utføre en FUA (Force Unit Access, skrive data direkte til disk, omgå hurtigbufferen) skrive til journalen. Dette gjør de sannsynligvis for å bekrefte at transaksjonen har funnet sted. På stasjoner som ikke støtter FUA, forårsaker dette to cache-tømminger. Mine eksperimenter viste det fdatasync() litt raskere fsync(). Nytte blktrace indikerer at fdatasync() skriver vanligvis mindre data til disken (i ext4 fsync() skriver 20 KiB, og fdatasync() - 16 KiB). Dessuten fant jeg ut at XFS er litt raskere enn ext4. Og her med hjelp blktrace klarte å finne ut det fdatasync() skyller mindre data til disk (4 KiB i XFS).

Tvetydige situasjoner som oppstår ved bruk av fsync()

Jeg kan tenke meg tre tvetydige situasjoner mht fsync()som jeg møtte i praksis.

Det første tilfellet skjedde i 2008. Da ville Firefox 3-grensesnittet fryse hvis et stort antall filer ble skrevet til disk. Problemet var at implementeringen av grensesnittet brukte en SQLite-database for å lagre informasjon om tilstanden. Etter hver endring som skjedde i grensesnittet, ble funksjonen kalt opp fsync(), som ga gode garantier for stabil datalagring. I ext3-filsystemet som da ble brukt, funksjonen fsync() dumpet alle "skitne" sider i systemet til disk, og ikke bare de som var relatert til den tilsvarende filen. Dette betydde at å klikke på en knapp i Firefox kunne utløse megabyte med data som ble skrevet til en magnetisk disk, noe som kunne ta mange sekunder. Løsningen på problemet, så vidt jeg forstår av det materialet skulle overføre arbeid med databasen til asynkrone bakgrunnsoppgaver. Dette betyr at Firefox tidligere implementerte strengere lagringskrav enn det som egentlig var nødvendig, og funksjonene til ext3-filsystemet bare forverret dette problemet.

Det andre problemet oppsto i 2009. Så, etter en systemkrasj, ble brukere av det nye ext4-filsystemet møtt med det faktum at mange nyopprettede filer hadde null lengde, men dette skjedde ikke med det eldre ext3-filsystemet. I forrige avsnitt snakket jeg om hvordan ext3 tømte for mye data til disken, noe som bremset ting mye. fsync(). For å forbedre situasjonen, i ext4 blir bare de skitne sidene som er relevante for en bestemt fil tømt til disken. Og data fra andre filer forblir i minnet i mye lengre tid enn med ext3. Dette ble gjort for å forbedre ytelsen (som standard forblir dataene i denne tilstanden i 30 sekunder, du kan konfigurere dette ved å bruke dirty_expire_centisecs; her Du kan finne tilleggsmateriell om dette). Dette betyr at en stor mengde data kan gå uopprettelig tapt etter en feil. Løsningen på dette problemet er å bruke fsync() i applikasjoner som må sikre stabil datalagring og beskytte dem så mye som mulig mot konsekvensene av feil. Funksjon fsync() fungerer mye mer effektivt når du bruker ext4 enn når du bruker ext3. Ulempen med denne tilnærmingen er at bruken, som før, bremser utførelsen av enkelte operasjoner, for eksempel installasjon av programmer. Se detaljer om dette her и her.

Det tredje problemet vedr fsync(), oppsto i 2018. Så, innenfor rammen av PostgreSQL-prosjektet, ble det funnet at hvis funksjonen fsync() støter på en feil, merker den "skitne" sider som "rene". Som et resultat ringer følgende fsync() De gjør ikke noe med slike sider. På grunn av dette blir modifiserte sider lagret i minnet og blir aldri skrevet til disk. Dette er en virkelig katastrofe, siden applikasjonen vil tro at noen data er skrevet til disken, men det vil det faktisk ikke være. Slike feil fsync() er sjeldne, kan applikasjonen i slike situasjoner nesten ikke gjøre noe for å bekjempe problemet. I disse dager, når dette skjer, krasjer PostgreSQL og andre applikasjoner. Her, i materialet "Kan applikasjoner gjenopprette fra fsync-feil?", utforskes dette problemet i detalj. For øyeblikket er den beste løsningen på dette problemet å bruke Direct I/O med flagget O_SYNC eller med flagg O_DSYNC. Med denne tilnærmingen vil systemet rapportere feil som kan oppstå under spesifikke skriveoperasjoner, men denne tilnærmingen krever at applikasjonen administrerer bufferne selv. Les mer om dette her и her.

Åpne filer ved å bruke flaggene O_SYNC og O_DSYNC

La oss gå tilbake til diskusjonen om Linux-mekanismer som gir stabil datalagring. Vi snakker nemlig om å bruke flagget O_SYNC eller flagg O_DSYNC når du åpner filer ved hjelp av systemanrop åpen(). Med denne tilnærmingen utføres hver dataskriveoperasjon som etter hver kommando write() systemet gis kommandoer tilsvarende fsync() и fdatasync(). I POSIX spesifikasjoner dette kalles "Synchronized I/O File Integrity Completion" og "Data Integrity Completion". Hovedfordelen med denne tilnærmingen er at for å sikre dataintegritet trenger du bare å foreta ett systemanrop, i stedet for to (for eksempel - write() и fdatasync()). Den største ulempen med denne tilnærmingen er at alle skrivinger som bruker den tilsvarende filbeskrivelsen vil bli synkronisert, noe som kan begrense muligheten til å strukturere applikasjonskoden.

Bruke Direct I/O med O_DIRECT-flagget

Systemanrop open() støtter flagg O_DIRECT, som er designet for å omgå operativsystemets hurtigbuffer for å utføre I/O-operasjoner ved å samhandle direkte med disken. Dette betyr i mange tilfeller at skrivekommandoer utstedt av programmet vil bli direkte oversatt til kommandoer rettet mot å jobbe med disken. Men generelt sett er ikke denne mekanismen en erstatning for funksjoner fsync() eller fdatasync(). Faktum er at disken selv kan defer eller cache tilsvarende dataskrivingskommandoer. Og for å gjøre saken verre, i noen spesielle tilfeller I/O-operasjonene som ble utført når flagget ble brukt O_DIRECT, kringkaste inn i tradisjonell bufret operasjon. Den enkleste måten å løse dette problemet på er å bruke flagget til å åpne filer O_DSYNC, som vil bety at hver skriveoperasjon vil bli fulgt av et anrop fdatasync().

Det viste seg at XFS-filsystemet nylig hadde lagt til en "rask sti" for O_DIRECT|O_DSYNC-dataregistrering. Hvis en blokk skrives om ved hjelp av O_DIRECT|O_DSYNC, så vil XFS, i stedet for å tømme hurtigbufferen, utføre FUA-skrivekommandoen hvis enheten støtter det. Jeg bekreftet dette ved å bruke verktøyet blktrace på et Linux 5.4/Ubuntu 20.04-system. Denne tilnærmingen bør være mer effektiv, siden når den brukes, skrives en minimal mengde data til disken og én operasjon brukes, i stedet for to (skriving og tømming av hurtigbufferen). Jeg fant en link til lapp 2018-kjernen, som implementerer denne mekanismen. Det er en del diskusjon der om å bruke denne optimaliseringen til andre filsystemer, men så langt jeg vet er XFS det eneste filsystemet som støtter dette så langt.

sync_file_range() funksjon

Linux har et systemanrop sync_file_range(), som lar deg tømme bare en del av filen til disken, i stedet for hele filen. Dette anropet starter en asynkron dataspyling og venter ikke på at den skal fullføres. Men i sertifikatet sync_file_range() laget sies å være "veldig farlig". Det anbefales ikke å bruke det. Funksjoner og farer sync_file_range() veldig godt beskrevet i dette materiale. Nærmere bestemt ser det ut til at dette kallet bruker RocksDB til å kontrollere når kjernen skyller skitne data til disken. Men samtidig, for å sikre stabil datalagring, brukes den også fdatasync(). I kode RocksDB har noen interessante kommentarer om dette emnet. For eksempel ser det ut til at samtalen sync_file_range() Når du bruker ZFS, skyller den ikke data til disk. Erfaring sier meg at kode som sjelden brukes sannsynligvis inneholder feil. Derfor vil jeg fraråde å bruke denne systemanropet med mindre det er absolutt nødvendig.

Systemanrop som bidrar til å sikre datavedvarende

Jeg har kommet til den konklusjonen at det er tre tilnærminger som kan brukes til å utføre I/O-operasjoner som sikrer datautholdenhet. De krever alle et funksjonskall fsync() for katalogen der filen ble opprettet. Dette er tilnærmingene:

  1. Funksjonsanrop fdatasync() eller fsync() etter funksjon write() (det er bedre å bruke fdatasync()).
  2. Arbeide med en filbeskrivelse åpnet med et flagg O_DSYNC eller O_SYNC (bedre - med et flagg O_DSYNC).
  3. Ved å bruke kommandoen pwritev2() med flagg RWF_DSYNC eller RWF_SYNC (helst med flagg RWF_DSYNC).

Ytelsesnotater

Jeg har ikke nøye målt ytelsen til de ulike mekanismene jeg har undersøkt. Forskjellene jeg la merke til i hastigheten på arbeidet deres er veldig små. Dette betyr at jeg kan ta feil, og at det samme under forskjellige forhold kan gi forskjellige resultater. Først skal jeg snakke om hva som påvirker ytelsen mer, og deretter hva som påvirker ytelsen mindre.

  1. Å overskrive fildata er raskere enn å legge til data til en fil (ytelsesfordelen kan være 2-100%). Å legge til data til en fil krever ytterligere endringer i filens metadata, selv etter et systemanrop fallocate(), men størrelsen på denne effekten kan variere. Jeg anbefaler, for best ytelse, å ringe fallocate() for å forhåndstildele nødvendig plass. Da må denne plassen eksplisitt fylles med nuller og kalles fsync(). Dette vil sikre at de tilsvarende blokkene i filsystemet er merket som "allokert" i stedet for "uallokert". Dette gir en liten (ca. 2%) ytelsesforbedring. I tillegg kan noen disker ha en tregere første tilgang til en blokk enn andre. Dette betyr at å fylle plassen med nuller kan føre til en betydelig (omtrent 100 %) forbedring i ytelsen. Spesielt kan dette skje med disker AWS EBS (dette er uoffisielle data, jeg kunne ikke bekrefte det). Det samme gjelder oppbevaring GCP Persistent Disk (og dette er allerede offisiell informasjon, bekreftet av tester). Andre eksperter har gjort det samme observasjon, relatert til forskjellige disker.
  2. Jo færre systemanrop, desto høyere ytelse (gevinsten kan være ca. 5%). Ser ut som en utfordring open() med flagg O_DSYNC eller ring pwritev2() med flagg RWF_SYNC raskere enn en samtale fdatasync(). Jeg mistenker at poenget her er at denne tilnærmingen spiller en rolle i det faktum at færre systemanrop må utføres for å løse det samme problemet (ett anrop i stedet for to). Men forskjellen i ytelse er veldig liten, så du kan ignorere den fullstendig og bruke noe i applikasjonen som ikke vil komplisere logikken.

Hvis du er interessert i emnet bærekraftig datalagring, her er noen nyttige materialer:

  • I/O-tilgangsmetoder — oversikt over det grunnleggende om input/output-mekanismer.
  • Sikre at data når disken — en historie om hva som skjer med dataene på vei fra applikasjonen til disken.
  • Når bør du fsynkronisere katalogen som inneholder - svaret på spørsmålet om når du skal bruke fsync() for kataloger. For å sette dette i et nøtteskall, viser det seg at du må gjøre dette når du oppretter en ny fil, og årsaken til denne anbefalingen er at det i Linux kan være mange referanser til samme fil.
  • SQL Server på Linux: FUA Internals — her er en beskrivelse av hvordan vedvarende datalagring implementeres i SQL Server på Linux-plattformen. Det er noen interessante sammenligninger mellom Windows- og Linux-systemanrop her. Jeg er nesten sikker på at det var takket være dette materialet jeg lærte om FUA-optimalisering av XFS.

Har du mistet data som du trodde var sikkert lagret på en disk?

Slitesterk datalagring og Linux File APIer

Slitesterk datalagring og Linux File APIer

Kilde: www.habr.com