Holdbar datalagring og Linux File API'er

Mens jeg undersøgte bæredygtigheden af ​​datalagring i cloud-systemer, besluttede jeg at teste mig selv for at sikre mig, at jeg forstod de grundlæggende ting. jeg startede med at læse NVMe-specifikationen for at forstå, hvilke garantier vedrørende bæredygtig datalagring (det vil sige garantier for, at dataene vil være tilgængelige efter en systemfejl) giver os NMVe-diske. Jeg kom med følgende hovedkonklusioner: data må anses for beskadiget fra det øjeblik kommandoen til at skrive data gives, til det øjeblik det skrives til lagermediet. De fleste programmer bruger dog med glæde systemopkald til at optage data.

I dette indlæg udforsker jeg de vedvarende lagringsmekanismer, der leveres af Linux-fil-API'erne. Det ser ud til, at alt skal være enkelt her: programmet kalder kommandoen write(), og efter at denne kommando er fuldført, gemmes dataene sikkert på disken. Men write() kopierer kun applikationsdata til kernecachen placeret i RAM. For at tvinge systemet til at skrive data til disken, skal du bruge nogle ekstra mekanismer.

Holdbar datalagring og Linux File API'er

Overordnet set er dette materiale en samling noter, der relaterer til, hvad jeg har lært om et emne, der interesserer mig. Hvis vi taler meget kort om det vigtigste, viser det sig, at for at organisere bæredygtig datalagring skal du bruge kommandoen fdatasync() eller åbne filer med flaget O_DSYNC. Hvis du er interesseret i at lære mere om, hvad der sker med data på vej fra kode til disk, så tag et kig på dette artikel.

Funktioner ved at bruge skrive()-funktionen

Systemkald write() defineret i standarden IEEE POSIX som et forsøg på at skrive data til en filbeskrivelse. Efter vellykket afslutning write() Datalæseoperationer skal returnere nøjagtigt de bytes, der tidligere blev skrevet, hvilket gør dette, selvom dataene tilgås fra andre processer eller tråde (her relevante afsnit af POSIX-standarden). Her, i afsnittet om, hvordan tråde interagerer med normale filoperationer, er der en note, der siger, at hvis to tråde hver kalder disse funktioner, så skal hvert opkald se enten alle de udpegede konsekvenser af det andet opkald, eller slet ingen. konsekvenser. Dette fører til den konklusion, at alle fil-I/O-operationer skal holde en lås på den ressource, de opererer på.

Betyder det, at operationen write() er det atomare? Fra et teknisk synspunkt, ja. Datalæseoperationer skal returnere enten alt eller intet af det, der blev skrevet med write(). Men operationen write(), ifølge standarden, behøver det ikke nødvendigvis at slutte med at skrive alt ned, som den blev bedt om at skrive ned. Hun må kun skrive en del af dataene. For eksempel kan vi have to tråde, der hver tilføjer 1024 bytes til en fil beskrevet af den samme filbeskrivelse. Fra standardens synspunkt vil et acceptabelt resultat være, når hver skriveoperation kun kan tilføje en byte til filen. Disse operationer vil forblive atomare, men efter at de er afsluttet, vil de data, de skrev til filen, blive blandet sammen. her meget interessant diskussion om dette emne på Stack Overflow.

fsync() og fdatasync() funktioner

Den nemmeste måde at skylle data til disk er at kalde funktionen fsync(). Denne funktion beder operativsystemet om at overføre alle modificerede blokke fra cachen til disken. Dette inkluderer alle filmetadata (adgangstid, filændringstid og så videre). Jeg mener, at disse metadata sjældent er nødvendige, så hvis du ved, at det ikke er vigtigt for dig, kan du bruge funktionen fdatasync(). I Hjælpfdatasync() det siges, at under driften af ​​denne funktion gemmes en sådan mængde metadata på disken, som er "nødvendig for den korrekte udførelse af følgende datalæsningsoperationer." Og det er præcis, hvad de fleste applikationer bekymrer sig om.

Et problem, der kan opstå her, er, at disse mekanismer ikke garanterer, at filen vil være synlig efter en mulig fejl. Især når du opretter en ny fil, skal du ringe fsync() for den mappe, der indeholder den. Ellers kan det efter en fejl vise sig, at denne fil ikke eksisterer. Grunden til dette er, at i UNIX, på grund af brugen af ​​hårde links, kan en fil eksistere i flere mapper. Derfor, når du ringer fsync() der er ingen måde for en fil at vide, hvilke mappedata der også skal tømmes til disken (her Det kan du læse mere om). Det ser ud til, at ext4-filsystemet er i stand til automatisk anvende fsync() til de mapper, der indeholder de tilsvarende filer, men dette er muligvis ikke tilfældet med andre filsystemer.

Denne mekanisme kan implementeres forskelligt på forskellige filsystemer. jeg brugte blktrace for at lære om, hvilke diskoperationer der bruges i ext4- og XFS-filsystemer. Begge udsteder regelmæssige skrivekommandoer til disken for både filindholdet og filsystemjournalen, tømmer cachen og afslutter ved at udføre en FUA (Force Unit Access, skrivning af data direkte til disk, omgåelse af cachen)-skrivning til journalen. Det gør de sandsynligvis for at bekræfte, at transaktionen har fundet sted. På drev, der ikke understøtter FUA, forårsager dette to cache-skylninger. Det viste mine eksperimenter fdatasync() lidt hurtigere fsync(). Nytte blktrace indikerer det fdatasync() skriver normalt færre data til disken (i ext4 fsync() skriver 20 KiB, og fdatasync() - 16 KiB). Jeg fandt også ud af, at XFS er lidt hurtigere end ext4. Og her med hjælpen blktrace lykkedes at finde ud af det fdatasync() skyller færre data til disk (4 KiB i XFS).

Tvetydige situationer, der opstår, når du bruger fsync()

Jeg kan komme i tanke om tre tvetydige situationer vedr fsync()som jeg stødte på i praksis.

Det første tilfælde af denne art fandt sted i 2008. Så frøs Firefox 3-grænsefladen, hvis et stort antal filer blev skrevet til disken. Problemet var, at implementeringen af ​​grænsefladen brugte en SQLite-database til at gemme information om dens tilstand. Efter hver ændring, der skete i grænsefladen, blev funktionen kaldt fsync(), hvilket gav gode garantier for stabil datalagring. I det ext3-filsystem, der derefter blev brugt, er funktionen fsync() dumpede alle "beskidte" sider i systemet til disken, og ikke kun dem, der var relateret til den tilsvarende fil. Det betød, at et klik på en knap i Firefox kunne udløse megabyte data til at blive skrevet til en magnetisk disk, hvilket kunne tage mange sekunder. Løsningen på problemet, så vidt jeg forstår ud fra det materiale var at overføre arbejde med databasen til asynkrone baggrundsopgaver. Dette betyder, at Firefox tidligere implementerede strengere lagerkrav, end der virkelig var behov for, og funktionerne i ext3-filsystemet forværrede kun dette problem.

Det andet problem opstod i 2009. Så, efter et systemnedbrud, blev brugere af det nye ext4 filsystem konfronteret med, at mange nyoprettede filer havde nul længde, men det skete ikke med det ældre ext3 filsystem. I det foregående afsnit talte jeg om, hvordan ext3 skyllede for meget data til disken, hvilket bremsede tingene meget. fsync(). For at forbedre situationen tømmes kun de beskidte sider, der er relevante for en bestemt fil, til disken i ext4. Og data fra andre filer forbliver i hukommelsen i meget længere tid end med ext3. Dette blev gjort for at forbedre ydeevnen (som standard forbliver data i denne tilstand i 30 sekunder, du kan konfigurere dette vha. dirty_expire_centisecs; her Du kan finde yderligere materialer om dette). Det betyder, at en stor mængde data kan gå uigenkaldeligt tabt efter en fejl. Løsningen på dette problem er at bruge fsync() i applikationer, der skal sikre stabil datalagring og beskytte dem så meget som muligt mod konsekvenserne af fejl. Fungere fsync() fungerer meget mere effektivt, når du bruger ext4, end når du bruger ext3. Ulempen ved denne tilgang er, at dens brug, som før, forsinker udførelsen af ​​nogle operationer, såsom installation af programmer. Se detaljer om dette her и her.

Det tredje problem vedr fsync(), opstod i 2018. Derefter blev det inden for rammerne af PostgreSQL-projektet konstateret, at hvis funktionen fsync() støder på en fejl, markerer den "beskidte" sider som "rene". Som følge heraf kalder følgende fsync() De gør ikke noget med sådanne sider. På grund af dette gemmes ændrede sider i hukommelsen og bliver aldrig skrevet til disk. Dette er en rigtig katastrofe, da applikationen vil tro, at nogle data er skrevet til disken, men det vil det faktisk ikke være. Sådanne fiaskoer fsync() er sjældne, kan applikationen i sådanne situationer næsten ikke gøre noget for at bekæmpe problemet. I disse dage, når dette sker, går PostgreSQL og andre applikationer ned. Her, i materialet "Can Applications Recover from fsync Failures?", udforskes dette problem i detaljer. I øjeblikket er den bedste løsning på dette problem at bruge Direct I/O med flaget O_SYNC eller med et flag O_DSYNC. Med denne tilgang vil systemet rapportere fejl, der kan opstå under specifikke skriveoperationer, men denne tilgang kræver, at applikationen selv administrerer bufferne. Læs mere om dette her и her.

Åbning af filer ved hjælp af O_SYNC og O_DSYNC flag

Lad os vende tilbage til diskussionen om Linux-mekanismer, der giver stabil datalagring. Vi taler nemlig om at bruge flaget O_SYNC eller flag O_DSYNC når du åbner filer ved hjælp af systemkald åben(). Med denne tilgang udføres hver dataskriveoperation som efter hver kommando write() systemet får kommandoer i overensstemmelse hermed fsync() и fdatasync(). I POSIX specifikationer dette kaldes "Synchronized I/O File Integrity Completion" og "Data Integrity Completion". Den største fordel ved denne tilgang er, at for at sikre dataintegritet behøver du kun at foretage et systemopkald i stedet for to (f.eks. write() и fdatasync()). Den største ulempe ved denne tilgang er, at alle skrivninger ved hjælp af den tilsvarende filbeskrivelse vil blive synkroniseret, hvilket kan begrænse muligheden for at strukturere applikationskoden.

Brug af Direct I/O med O_DIRECT flaget

Systemkald open() understøtter flag O_DIRECT, som er designet til at omgå operativsystemets cache for at udføre I/O-operationer ved at interagere direkte med disken. Dette betyder i mange tilfælde, at skrivekommandoer udstedt af programmet vil blive direkte oversat til kommandoer rettet mod at arbejde med disken. Men generelt er denne mekanisme ikke en erstatning for funktioner fsync() eller fdatasync(). Faktum er, at disken selv kan defer eller cache tilsvarende dataskrivekommandoer. Og for at gøre tingene værre, i nogle specielle tilfælde udføres I/O-operationerne ved brug af flaget O_DIRECT, udsende ind i traditionelle bufferoperationer. Den nemmeste måde at løse dette problem på er at bruge flaget til at åbne filer O_DSYNC, hvilket vil betyde, at hver skriveoperation vil blive efterfulgt af et opkald fdatasync().

Det viste sig, at XFS-filsystemet for nylig havde tilføjet en "hurtig sti" til O_DIRECT|O_DSYNC-dataregistrering. Hvis en blok omskrives vha O_DIRECT|O_DSYNC, så vil XFS, i stedet for at tømme cachen, udføre FUA-skrivekommandoen, hvis enheden understøtter det. Jeg bekræftede dette ved at bruge værktøjet blktrace på et Linux 5.4/Ubuntu 20.04-system. Denne tilgang burde være mere effektiv, da når den bruges, skrives en minimal mængde data til disken, og en operation bruges i stedet for to (skrivning og skylning af cachen). Jeg fandt et link til lappe 2018 kerne, som implementerer denne mekanisme. Der er en del diskussion der om at anvende denne optimering på andre filsystemer, men så vidt jeg ved, er XFS det eneste filsystem, der understøtter dette indtil videre.

sync_file_range() funktion

Linux har et systemkald sync_file_range(), som giver dig mulighed for kun at tømme en del af filen til disken i stedet for hele filen. Dette opkald starter en asynkron datafluch og venter ikke på, at den er fuldført. Men i attesten sync_file_range() holdet siges at være "meget farligt". Det anbefales ikke at bruge det. Egenskaber og farer sync_file_range() meget godt beskrevet i dette materiale. Specifikt ser dette ud til at bruge RocksDB til at kontrollere, hvornår kernen skyller snavsede data til disken. Men samtidig, for at sikre stabil datalagring, bruges den også fdatasync(). I kode RocksDB har nogle interessante kommentarer om dette emne. For eksempel fremgår det, at opkaldet sync_file_range() Når du bruger ZFS, skyller den ikke data til disken. Erfaring siger mig, at kode, der sjældent bruges, sandsynligvis indeholder fejl. Derfor vil jeg fraråde at bruge dette systemopkald, medmindre det er absolut nødvendigt.

Systemkald, der hjælper med at sikre datapersistens

Jeg er kommet til den konklusion, at der er tre tilgange, der kan bruges til at udføre I/O-operationer, der sikrer datapersistens. De kræver alle et funktionskald fsync() for den mappe, hvor filen blev oprettet. Disse er tilgangene:

  1. Funktionsopkald fdatasync() eller fsync() efter funktion write() (det er bedre at bruge fdatasync()).
  2. Arbejde med en filbeskrivelse åbnet med et flag O_DSYNC eller O_SYNC (bedre - med et flag O_DSYNC).
  3. Kommandobrug pwritev2() med flag RWF_DSYNC eller RWF_SYNC (gerne med flag RWF_DSYNC).

Præstationsnoter

Jeg har ikke nøje målt ydeevnen af ​​de forskellige mekanismer, jeg har undersøgt. De forskelle, jeg bemærkede i hastigheden af ​​deres arbejde, er meget små. Det betyder, at jeg kan tage fejl, og at det samme under forskellige forhold kan give forskellige resultater. Først vil jeg tale om, hvad der påvirker præstationen mere, og derefter hvad der påvirker præstationen mindre.

  1. Overskrivning af fildata er hurtigere end at tilføje data til en fil (ydelsesfordelen kan være 2-100%). Tilføjelse af data til en fil kræver yderligere ændringer af filens metadata, selv efter et systemkald fallocate(), men størrelsen af ​​denne effekt kan variere. Jeg anbefaler, for den bedste ydeevne, at ringe fallocate() at forhåndstildele den nødvendige plads. Så skal dette mellemrum udtrykkeligt udfyldes med nuller og kaldes fsync(). Dette vil sikre, at de tilsvarende blokke i filsystemet er markeret som "allokeret" i stedet for "uallokeret". Dette giver en lille (ca. 2%) præstationsforbedring. Derudover kan nogle diske have en langsommere første adgang til en blok end andre. Det betyder, at udfyldning af rummet med nuller kan føre til en betydelig (ca. 100%) forbedring af ydeevnen. Dette kan især ske med diske AWS EBS (dette er uofficielle data, jeg kunne ikke bekræfte det). Det samme gælder opbevaring GCP Persistent Disk (og dette er allerede officiel information, bekræftet af tests). Andre eksperter har gjort det samme observationer, relateret til forskellige diske.
  2. Jo færre systemkald, jo højere ydeevne (forstærkningen kan være omkring 5%). Det ligner en udfordring open() med flag O_DSYNC eller ring pwritev2() med flag RWF_SYNC hurtigere end et opkald fdatasync(). Jeg formoder, at pointen her er, at denne tilgang spiller en rolle i, at der skal udføres færre systemkald for at løse det samme problem (et opkald i stedet for to). Men forskellen i ydeevne er meget lille, så du kan fuldstændig ignorere det og bruge noget i applikationen, der ikke vil komplicere dens logik.

Hvis du er interesseret i emnet bæredygtig datalagring, er her nogle nyttige materialer:

  • I/O-adgangsmetoder — oversigt over det grundlæggende i input/output-mekanismer.
  • Sikring af data når disken — en historie om, hvad der sker med dataene på vej fra applikationen til disken.
  • Hvornår skal du fsynkronisere den indeholdende mappe - svaret på spørgsmålet om, hvornår det skal bruges fsync() for mapper. For at sætte dette i en nøddeskal, så viser det sig, at du skal gøre dette, når du opretter en ny fil, og grunden til denne anbefaling er, at der i Linux kan være mange referencer til den samme fil.
  • SQL Server på Linux: FUA Internals — her er en beskrivelse af, hvordan vedvarende datalagring implementeres i SQL Server på Linux-platformen. Der er nogle interessante sammenligninger mellem Windows og Linux systemopkald her. Jeg er næsten sikker på, at det var takket være dette materiale, at jeg lærte om FUA-optimering af XFS.

Har du mistet data, som du troede var sikkert gemt på en disk?

Holdbar datalagring og Linux File API'er

Holdbar datalagring og Linux File API'er

Kilde: www.habr.com