BĂŠrekraftig datalagring og fil-API for Linux

Jeg, mens jeg undersÞker bÊrekraften til datalagring i skytjenester, bestemte meg for Ä teste min forstÄelse av de grunnleggende prinsippene. Jeg begynte med Ä lese spesifikasjonen for NVMe for Ä forstÄ hvilke garantier for bÊrekraftig datalagring (det vil si garantier for at dataene vil vÊre tilgjengelige etter et systemfeil) NVMe-disker gir oss. Jeg kom til fÞlgende hovedkonklusjoner: man mÄ anse data som skadet fra det Þyeblikket skrivekommandoen er gitt, og frem til skriveprosessen er fullfÞrt pÄ lagringsenheten. Imidlertid brukes systemanrop i de fleste dataskrivingsprogrammer helt uten problemer.

I dette materialet undersÞker jeg mekanismene for bÊrekraftig datalagring som tilbys av Linux-fil-APIene. Det virker som om dette bÞr vÊre enkelt: programmet kaller kommandoen write(), og etter at arbeidet med denne kommandoen er fullfÞrt, vil dataene vÊre pÄlitelig lagret pÄ disken. Men write() den bare kopierer applikasjonsdata inn i kjernebufferet, som ligger i RAM. For Ä tvinge systemet til Ä skrive dataene til disken, mÄ man bruke noen ekstra mekanismer.

BĂŠrekraftig datalagring og fil-API for Linux

Generelt sett er dette materialet et sett med notater om hva jeg har lÊrt om det emnet som interesserer meg. Hvis jeg kort oppsummerer det viktigste, kan jeg si at for Ä organisere bÊrekraftig datalagring mÄ man bruke kommandoen fdatasync() eller Äpne filer med flagget O_DSYNC. Hvis du Þnsker Ä vite mer detaljert om hva som skjer med dataene fra programkoden til disken, se pÄ denne artikkelen.

SĂŠrtrekk ved bruk av funksjonen write()

Systemanropet write() er definert i standarden IEEE POSIX som et forsÞk pÄ Ä skrive data til en filbeskrivelse. Etter en vellykket fullfÞring av arbeidet write() bÞr leseoperasjoner returnere akkurat de bytene som ble skrevet tidligere, ogsÄ selv om dataene blir aksessert fra andre prosesser eller trÄder (her er den relevante delen av POSIX-standarden). Her, i seksjonen som omhandler samhandling mellom trÄder og vanlige filoperasjoner, finnes det en merknad som sier at hvis hver av de to trÄdene kaller disse funksjonene, mÄ hvert kall se enten alle de angitte konsekvensene som fÞlge av utfÞrelsen av det andre kallet, eller ikke se noen konsekvenser i det hele tatt. Dette medfÞrer at alle filoperasjoner for inn- og ut-data mÄ opprettholde lÄsing av ressursen de jobber med.

Betyr dette at operasjonen write() er atomĂŠr? Tekniskt sett – ja. Data lesingsoperasjoner mĂ„ returnere enten alt, eller ingenting av det som ble skrevet med write(). Men operasjonen write(), i henhold til standarden, er ikke obligatorisk for Ă„ fullfĂžre ved Ă„ skrive alt som den ble foreslĂ„tt Ă„ skrive. Den kan tillate Ă„ skrive bare en del av dataene. For eksempel kan vi ha to trĂ„der, hver av dem legger til 1024 byte til en fil, beskrevet av den samme filbeskriveren. Fra standardens synspunkt er resultatet akseptabelt nĂ„r hver av skriveoperasjonene bare kan legge til Ă©n byte til filen. Disse operasjonene vil forbli atomĂŠre, men etter at de er fullfĂžrt, vil dataene de skrev til filen bli blandet. Her er en veldig interessant diskusjon om dette emnet pĂ„ Stack Overflow.

Funksjonene fsync() og fdatasync()

Den enkleste mÄten Ä skrive data til disk pÄ er ved Ä kalle funksjonen fsync(). Denne funksjonen ber operativsystemet om Ä overfÞre alle modifiserte blokker fra bufferen til disken. Dette inkluderer ogsÄ all metadata for filen (tilgangstid, tid for filmodifikasjon, osv.). Jeg tror behovet for disse metadataene sjelden oppstÄr, sÄ hvis du vet at de ikke er viktige for deg, kan du bruke funksjonen fdatasync(). I dokumentasjonen om fdatasync() sies det at under arbeidet med denne funksjonen lagres det pÄ disken sÄ mye metadata som "er nÞdvendig for korrekt utfÞrelse av de pÄfÞlgende lesing av data-operasjonene". Og dette er akkurat det som bekymrer de fleste applikasjoner.

Et av problemene som kan oppstÄ her, er at disse mekanismene ikke garanterer at filen vil vÊre mulig Ä oppdage etter en potensiell feil. Spesielt nÄr man oppretter en ny fil, mÄ man kalle fsync() for the directory that contains it. Otherwise, after a crash, it may happen that this file does not exist. The reason for this is that in UNIX, due to the use of hard links, a file can exist in multiple directories. Therefore, when called, fsync() there is no way for the file to know which directory's data also needs to be flushed to disk (here you can read more about it). It seems that the ext4 file system is capable of automatically applying fsync() to directories containing corresponding files, but this may not be the case with other file systems.

This mechanism may be implemented differently in different file systems. I used blktrace to find out what disk operations are used in the ext4 and XFS file systems. Both produce usual write commands to disk for both file contents and the file system journal, flushing the cache and completing the operation by executing FUA (Force Unit Access, writing data directly to disk, bypassing the cache) in the journal. They probably do this to confirm the fact of performing the operation. On disks that do not support FUA, this causes two cache flushes. My experiments showed that fdatasync() a little faster fsync(). The utility blktrace indicates that fdatasync() typically writes less data to disk (in ext4 fsync() writes 20 KiB, and fdatasync() — 16 KiB). In addition, I found out that XFS is a bit faster than ext4. And here with the help of blktrace it was possible to find out that fdatasync() flushes less data to disk (4 KiB in XFS).

Ambiguous situations arising from the use of fsync()

I can recall three ambiguous situations regarding fsync(), with which I encountered in practice.

The first such case occurred in 2008. At that time, the Firefox 3 interface would 'freeze' when writing a large number of files to disk. The problem was that in the implementation of the interface, SQLite was used to store information about its state. After each change in the interface, the function was called fsync(), which provided good guarantees for stable data storage. In the file system in use at that time, ext3, the function fsync() den nedlastet alt "skitne" sider i systemet til disken, ikke bare de som var knyttet til den aktuelle filen. Dette betydde at et klikk pÄ knappen i Firefox kunne initiere skriving av megabytes med data til harddisken, noe som kunne ta mange sekunder. LÞsningen pÄ problemet, sÄ langt jeg forstod fra dette materialet, var Ä flytte databasehÄndteringen til asynkrone bakgrunnsoppgaver. Dette betyr at tidligere i Firefox ble det implementert strengere krav til datalagringens pÄlitelighet enn det som faktisk var nÞdvendig, og egenskapene til filsystemet ext3 forverret bare dette problemet.

Det andre avviket skjedde i 2009. Da, etter et systemkrasj, opplevde brukere av det nye filsystemet ext4 at mange nylig opprettede filer hadde null lengde, mens dette ikke skjedde med det eldre filsystemet ext3. I forrige avsnitt nevnte jeg at ext3 skrev for mye data til disken, noe som betydelig forsinket ytelsen. fsync()For Ä forbedre situasjonen, skriver ext4 til disken kun de "skitne" sidene som er knyttet til den spesifikke filen. Data fra andre filer forblir i minnet i mye lengre tid enn ved bruk av ext3. Dette ble gjort for Ä forbedre ytelsen (som standard forblir data i denne tilstanden i 30 sekunder, noe som kan justeres med dirty_expire_centisecs; here man kan finne mer materiale om dette). Dette betyr at store mengder data kan gÄ tapt irreversibelt etter et krasj. LÞsningen pÄ dette problemet er Ä bruke fsync() i applikasjoner som trenger Ä sikre pÄlitelig datalagring og maksimalt beskytte dem mot konsekvensene av krasj. Funksjonen fsync() fungerer betydelig bedre med ext4 enn med ext3. Ulempen med denne tilnÊrmingen er at dens anvendelse, som fÞr, bremser utfÞrelsen av visse operasjoner, som installasjon av programmer. Se detaljer om dette her og her.

Det tredje problemet, angÄende fsync(), oppsto i 2018. Da, som en del av PostgreSQL-prosjektet, ble det fastslÄtt at hvis funksjonen fsync() mÞter en feil, merker den "skitne" sider som "rene". Som et resultat blir de pÄfÞlgende kallene fsync() man kan ikke gjÞre noe med slike sider. PÄ grunn av dette lagres de modifiserte sidene i minnet og skrives aldri til disken. Dette er en virkelig katastrofe, ettersom applikasjonen vil tro at visse data er skrevet til disken, mens det i virkeligheten ikke er tilfelle. Slike feil fsync() er sjeldne, applikasjonen kan nesten ingenting gjÞre for Ä takle problemet. I dag, nÄr dette skjer, krasjer PostgreSQL og andre applikasjoner. Her, i artikkelen «Kan applikasjoner komme seg fra fsync-feil?», blir dette problemet undersÞkt i detalj. For Þyeblikket er den beste lÞsningen pÄ dette problemet Ä bruke Direct I/O med flagget O_SYNC eller med flagget O_DSYNC. Med denne tilnÊrmingen vil systemet rapportere om feil som kan oppstÄ under spesifikke dataskriveoperasjoner, men denne tilnÊrmingen krever at applikasjonen administrerer buffere selv. Les mer om dette her og her.

Åpning av filer med flaggene O_SYNC og O_DSYNC

La oss gĂ„ tilbake til diskusjonen om mekanismer i Linux som sikrer vedvarende datalagring. Det handler spesifikt om bruk av flagget O_SYNC eller flagget O_DSYNC ved Ă„pning av filer med systemanropet open(). Med denne tilnĂŠrmingen blir hver dataskriving vurdert som om kommandoene write() systemet fĂ„r, henholdsvis, kommandoer fsync() og fdatasync(). I spesifikasjonene fra POSIX kalles dette «Synchronized I/O File Integrity Completion» og «Data Integrity Completion». Den stĂžrste fordelen med denne tilnĂŠrmingen er at for Ă„ sikre dataintegritet mĂ„ man bare gjĂžre ett systemanrop, ikke to (for eksempel — write() og fdatasync()). Den stĂžrste ulempen med denne tilnĂŠrmingen er at alle skriveoperasjoner som bruker den aktuelle filbeskrivelsen vil bli synkronisert, noe som kan begrense mulighetene for Ă„ strukturere applikasjonskoden.

Bruk av Direct I/O med flagget O_DIRECT

Systemanropet open() stÞtter flagget O_DIRECT, som er ment for Ä omgÄ operativsystemets buffer og utfÞre I/O-operasjoner ved Ä samhandle direkte med disken. Dette betyr i mange tilfeller at skrivekommandoer utstedt av programmet vil bli direkte overfÞrt til kommandoer som er mÄlrettet mot diskoperasjoner. Men generelt er ikke denne mekanismen en erstatning for funksjonene fsync() eller fdatasync(). Poenget er at selve disken kan utsette eller cache de relevante data skrive kommandoene. Og, hva som er verre, i noen spesielle tilfeller kan I/O-operasjoner utfÞrt med flagget O_DIRECT, oversettes til tradisjonelle bufferiserte operasjoner. Det er enklest Ä lÞse dette problemet ved Ä Äpne filer med flagget O_DSYNC, noe som vil bety at det vil vÊre et kall etter hver skriveoperasjon fdatasync().

Det har vist seg at filsystemet XFS nylig har fÄtt en 'hurtigvei' for O_DIRECT|O_DSYNC-skriving av data. Hvis en blokk blir overskrevet med bruk av O_DIRECT|O_DSYNC, sÄ vil XFS, i stedet for Ä tÞmme cachen, utfÞre FUA-skrivekommandoen dersom enheten stÞtter det. Jeg bekreftet dette ved Ä bruke verktÞyet blktrace i Linux 5.4/Ubuntu 20.04. Denne tilnÊrmingen bÞr vÊre mer effektiv, da den skriver et minimum av data til disken og samtidig bruker én operasjon snarere enn to (skrive og tÞmme cache). Jeg fant en lenke til patch fra kjernen i 2018, der denne mekanismen er implementert. Det er en diskusjon der som omhandler bruk av denne optimaliseringen i andre filsystemer, men sÄ vidt jeg vet, er XFS det eneste filsystemet som sÄ langt stÞtter dette.

sync_file_range()

I Linux finnes det et systemkall sync_file_range(), som lar deg skrive til disk bare en del av en fil, ikke hele filen. Dette systemkallet initierer en asynkron skriving av data og venter ikke pÄ at det skal fullfÞres. Men det stÄr i hjelpedokumentasjonen for sync_file_range() at denne kommandoen er 'svÊrt farlig'. Den anbefales ikke til bruk. Egenskapene og farene sync_file_range() er veldig godt beskrevet i dette materialet. I sÊrdeleshet ser det ut til at dette systemkallet bruker RocksDB til Ä styre nÄr kjernen tÞmmer 'skitne' data til disk. Men samtidig, for Ä sikre varig datalagring, brukes ogsÄ fdatasync(). I koden Det er interessante kommentarer om dette i RocksDB. For eksempel ser det ut til at kallet sync_file_range() nÄr det brukes med ZFS ikke fÞrer til at data skrives til disk. Erfaring forteller meg at kode som sjelden brukes, kan inneholde feil. Derfor ville jeg anbefale Ä ikke bruke dette systemkallet uten absolutt nÞdvendighet.

Systemkall som hjelper til med Ă„ sikre varig datalagring

Jeg har kommet frem til at for Ä utfÞre inn-/utgangsoperasjoner som gir pÄlitelig datalagring, kan man bruke tre tilnÊrminger. Alle krever et funksjonskall fsync() for katalogen der filen er opprettet. Her er disse tilnÊrmingene:

  1. Kall pÄ funksjonen fdatasync() eller fsync() etter funksjonen write() (bruk helst fdatasync()).
  2. Arbeid med filbeskrivelsen, Ă„pnet med flagget O_DSYNC eller O_SYNC (bedre — med flagget O_DSYNC).
  3. Bruk av kommandoen pwritev2() med flagget RWF_DSYNC eller RWF_SYNC (foretrukket — med flagget RWF_DSYNC).

Notater om ytelse

Jeg har ikke utfÞrt grundige mÄlinger av ytelsen til de forskjellige mekanismene jeg har undersÞkt. Mine observerte hastighetsforskjeller er ganske smÄ. Dette betyr at jeg kan ta feil, og at det i andre forhold kan gi andre resultater. FÞrst vil jeg fortelle hva som pÄvirker ytelsen mest, og deretter hva som har mindre innvirkning.

  1. Overskriving av fildata er raskere enn Ă„ legge til data til filen (ytelsesgevinsten kan vĂŠre 2-100%). Å legge til data til filen krever ytterligere endringer i filens metainformasjon, selv etter systemkallet fallocate(), men omfanget av denne effekten kan variere. Jeg anbefaler Ă„ kalle fallocate() for Ă„ forhĂ„ndsallokere nĂždvendig plass. Deretter mĂ„ dette omrĂ„det eksplisitt fylles med nuller og kalle fsync(). Dette sĂžrger for at de aktuelle blokkene i filsystemet merkes som «allokert», og ikke som «ikke-allokert». Dette gir en liten (omtrent 2%) ytelsesforbedring. I tillegg kan fĂžrste tilgang til en blokk pĂ„ noen disker vĂŠre tregere enn andre. Dette betyr at fylling av plass med nuller kan fĂžre til en betydelig (omtrent 100%) ytelsesforbedring. Spesielt kan dette skje med disker AWS EBS (dette er ikke-offisielle data, jeg har ikke kunnet bekrefte dem). Det samme gjelder for lagringer GCP Persistent Disk (og dette er allerede offisiell informasjon bekreftet av tester). Andre spesialister har gjort de samme observasjonene, relatert til forskjellige disker.
  2. Jo fÊrre systemkall, jo hÞyere ytelse (gevinsten kan vÊre omtrent 5%). Det ser ut til at Ä kalle open() med flagget O_DSYNC eller kalle pwritev2() med flagget RWF_SYNC raskall av fdatasync(). Eg har ei mistanke om at dette har Ä gjere med at nÄr ein tar denne tilnÊrminga, sÄ spelar det ei rolle at for Ä lÞyse den same oppgÄva mÄ ein utfÞre fÊrre systemkall (ein kall i staden for to). Men forskjellen i ytelse er svÊrt liten, sÄ du kan heilt klart ignorere det og bruke det i applikasjonen som ikkje vil fÞre til komplisering av logikken.

Dersom du er interessert i temaet omhaldande datalagring — her er nokre nyttige ressursar:

  • I/O tilgangsmetodar — ei oversikt over grunnprinsippa for inndatakontroll.
  • Sikring av at data nĂ„r disken — ein fortelling om kva som skjer med data pĂ„ veg frĂ„ applikasjonen til disken.
  • NĂ„r bĂžr du fsync katalogen som inneheld — svaret pĂ„ spĂžrsmĂ„let om nĂ„r det er nĂždvendig Ă„ bruke fsync() for katalogar. I korte trekk, dette bĂžr gjerast ved oppretting av ei ny fil, og grunnen til denne anbefalinga er at det i Linux kan finnast mange referansar til den same fila.
  • SQL Server pĂ„ Linux: FUA-internals — her finnes det ei beskrivelse av korleis robust datalagring er implementert i SQL Server pĂ„ Linux-plattforma. Her er det einige interessante samanlikningar mellom systemkall i Windows og Linux. Eg er nesten sikker pĂ„ at eg lĂŠrte om FUA-optimalisering av XFS takka vere dette materialet.

Har du tapt data som du trudde var sikkert lagra pÄ disken?

BĂŠrekraftig datalagring og fil-API for Linux

BĂŠrekraftig datalagring og fil-API for Linux

Kilde: habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster