Jeg, mens jeg undersøger datalagringsholdbarhed i cloud-systemer, besluttede at teste mig selv for at sikre, at jeg forstår de grundlæggende ting. Jeg for at forstå, hvilke garantier vedrørende holdbar lagring af data (det vil sige garantier for, at dataene vil være tilgængelige efter et systemfejl), NVMe-diskene giver os. Mine følgende hovedkonklusioner er: Det skal betragtes, at dataene er beskadigede fra det øjeblik, hvor skrivekommandoen er givet, til det øjeblik, hvor de er færdige med at blive skrevet til informationsmediet. Dog anvendes systemopkald helt afslappet i de fleste programmer til dataoptagelse.
I dette materiale undersøger jeg de mekanismer for holdbar datalagring, der tilbydes af Linux-filsystem-API'er. Det virker til at være enkelt: Programmet kalder kommandoen write(), og efter at denne kommando er afsluttet, skulle dataene være sikkert gemt på disken. Men write() kopierer blot applikationernes data til kernelens cache, der ligger i RAM. For at tvinge systemet til at skrive data til disken, kræves nogle ekstra mekanismer.
Generelt er dette materiale en samling noter om, hvad jeg har lært om det emne, der interesserer mig. Hvis man meget kort skal fortælle om det vigtigste, er det, at for at organisere holdbar datalagring, skal man bruge kommandoen fdatasync() eller åbne filer med flaget O_DSYNC. Hvis du er interesseret i detaljerne om, hvad der sker med dataene, når de transporteres fra programkoden til disken, kan du se på artiklen.
Egenskaber ved brug af funktionen write()
Systemopkald write() defineres i standarden som et forsøg på at skrive data til en filbeskriver. Efter en vellykket afslutning write() skal datalæsningsoperationer returnere netop de bytes, der tidligere var skrevet, hvilket skal ske, selvom dataene tilgås fra andre processer eller tråde ( den relevante sektion i POSIX-standarden). , i afsnittet om interaktionen mellem tråde og almindelige filoperationer er der en bemærkning, der siger, at hvis hver af de to tråde kalder disse funktioner, så skal hver opkald se enten alle de angivne konsekvenser, som udførelsen af den anden opkald medfører, eller slet ingen konsekvenser. Dette fører til den konklusion, at alle fil I/O-operationer skal holde lås på den ressource, de arbejder med.
Betyder det, at operationen write() er atomar? Teknisk set - ja. Data læseoperationer skal returnere enten alt eller intet af det, der er blevet skrevet med write(). Men operationen write(), ifølge standarden, behøver ikke nødvendigvis at afslutte ved at skrive alt, hvad den blev bedt om at skrive. Den må kun skrive en del af dataene. For eksempel kan vi have to tråde, der hver tilføjer 1024 byte til en fil, der beskrives af den samme filbeskrivelse. Fra standardens synspunkt er et acceptabelt resultat, hvor hver af skriveoperationerne kun kan tilføje en byte til filen. Disse operationer vil forblive atomare, men når de er færdige, vil de data, der er skrevet af dem til filen, være blandede. meget interessant diskussion om dette emne på Stack Overflow.
Funktionerne fsync() og fdatasync()
Den enkleste måde at tømme data til disk på er ved at kalde funktionen . Denne funktion beder operativsystemet om at overføre alle modificerede blokke fra cachen til disken. Dette inkluderer også alle metadata for filen (adgangstid, ændringstid for filen osv.). Jeg antager, at behovet for disse metadata sjældent opstår, så hvis du ved, at de ikke er vigtige for dig, kan du bruge funktionen fdatasync(). I efter fdatasync() siger, at under denne funktions arbejde overføres der til disken så meget metadata, som "er nødvendigt for korrekt udførelse af de følgende data læseoperationer". Og det er netop det, der bekymrer de fleste applikationer.
Et af de problemer, der kan opstå her, er, at disse mekanismer ikke garanterer, at filen kan findes efter et muligt sammenbrud. Især når man opretter en ny fil, skal man kalde fsync() for the directory that contains it. Otherwise, after a crash, it may turn out 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 several directories. Therefore, when calling fsync() there is no way for the file to know which specific directory also needs to be flushed to disk ( you can read more about this). It seems that the ext4 file system is capable at anvende fsync() of directories that contain the corresponding files, but this may not be the case with other file systems.
This mechanism can be implemented differently in various file systems. I used to find out which disk operations are used in the ext4 and XFS file systems. Both issue standard write commands to the disk for both file contents and the file system journal, flush the cache, and finish by performing an FUA write (Force Unit Access, writing data directly to disk, bypassing the cache) in the journal. They probably do this to confirm the operation's occurrence. On disks that do not support FUA, this causes two cache flushes. My experiments showed that fdatasync() a bit faster fsync(). Utility blktrace indicates that fdatasync() usually writes less data to disk (in ext4 fsync() writes 20 KiB, while fdatasync() — 16 KiB). Additionally, I found that XFS is slightly 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 when using fsync()
I can recall three ambiguous situations regarding fsync(), that I encountered in practice.
The first such case occurred in 2008. At that time, the Firefox 3 interface would 'hang' if a large number of files were being written to disk. The problem was that the implementation of the interface used an SQLite database to store information about its state. After every change occurring in the interface, the function was called fsync(), which provided good guarantees of data durability. In the ext3 file system used at that time, the function fsync() skrev alt "beskidt" indhold tilbage til disken i systemet, og ikke kun dem, der var relateret til den relevante fil. Det betød, at et klik på knappen i Firefox kunne initiere skrivning af megabyte data til den magnetiske disk, hvilket kunne tage mange sekunder. Problemet blev løst, som jeg forstod det fra materialet, ved at flytte arbejdet med databasen til asynkrone baggrundsopgaver. Det betyder, at tidligere i Firefox var der implementeret strengere krav til datalagringsimulering, end hvad der egentlig var nødvendigt, og egenskaberne ved filsystemet ext3 forværrede blot dette problem.
Det andet problem opstod i 2009. Dengang, efter et systemnedbrud, stødte brugerne af det nye filsystem ext4 på, at mange nyligt oprettede filer havde nul længde, mens det ikke skete med det ældre filsystem ext3. I den foregående sætning nævnte jeg, at ext3 skrev for meget data til disken, hvilket bremse fsync(). For at forbedre situationen skriver ext4 kun de "beskidte" sider tilbage til disken, der er relateret til en specifik fil. Data fra andre filer forbliver i hukommelsen i meget længere tid end med brug af ext3. Dette blev gjort for at forbedre ydeevnen (som standard forbliver data i denne tilstand i 30 sekunder, dette kan justeres ved hjælp af ; , hvor du kan finde yderligere oplysninger om dette). Det betyder, at et stort antal data kan gå tabt permanent efter et nedbrud. Løsningen på dette problem ligger i brugen af fsync() i applikationer, der skal sikre vedvarende datalagring og maksimalt beskytte dem mod konsekvenserne af nedbrud. Funktionen fsync() fungerer langt mere effektivt med ext4 end med ext3. Ulempen ved denne tilgang er, at dens anvendelse, som før, bremser udførelsen af visse operationer, såsom installation af programmer. Se detaljer om dette og .
Det tredje problem, der angår fsync(), opstod i 2018. Dengang, inden for rammerne af PostgreSQL-projektet, blev det afsløret, at hvis funktionen fsync() støder på en fejl, markerer den "beskidte" sider som "rene". Som et resultat vil de efterfølgende kald fsync() der bliver ikke gjort noget med sådanne sider. På grund af dette gemmes de modificerede sider i hukommelsen og skrives aldrig til disken. Dette er en reel katastrofe, da applikationen vil tro, at nogle data er gemt på disken, når det i virkeligheden ikke er tilfældet. Sådanne fejl fsync() forekommer sjældent, og applikationen kan i sådanne situationer næsten intet gøre for at bekæmpe problemet. I dag, når dette sker, afslutter PostgreSQL og andre applikationer ofte ned. , i materialet "Kan applikationer komme sig efter fsync-fejl?" bliver dette problem udforsket i detaljer. I øjeblikket er den bedste løsning på dette problem at bruge Direct I/O med flaget O_SYNC eller med flaget O_DSYNC. Med denne tilgang vil systemet rapportere fejl, der kan opstå under specifikke data skriveoperationer, men denne tilgang kræver, at applikationen selv håndterer buffere. Læs mere om dette og .
Åbning af filer med flagene O_SYNC og O_DSYNC
Lad os vende tilbage til diskussionen om de mekanismer, der muliggør holdbar datalagring i Linux. Det handler specifikt om at bruge flaget O_SYNC eller flaget O_DSYNC ved åbning af filer ved hjælp af systemopkald . Med denne tilgang udføres hver data skriveoperation, som om hver kommando write() systemet gives, henholdsvis kommandoer fsync() og fdatasync(). I kaldes "Synchronized I/O File Integrity Completion" og "Data Integrity Completion". Den primære fordel ved denne tilgang er, at der kun er behov for at udføre ét systemopkald for at sikre dataintegritet, og ikke to (f.eks. - write() og fdatasync()). Den største ulempe ved denne tilgang er, at alle skriveoperationer, der bruger den relevante filbeskriver, vil være synkroniserede, hvilket kan begrænse mulighederne for struktureret kode i applikationen.
Brugen af Direct I/O med flaget O_DIRECT
Systemopkald open() understøtter flaget O_DIRECT, som er beregnet til at udføre input-output operationer ved at omgå operativsystemets cache og interagere direkte med disken. Dette betyder i mange tilfælde, at skrivekommandoerne, der gives af programmet, vil blive direkte oversat til kommandoer, der er rettet mod diskoperationer. Men generelt er denne mekanisme ikke en erstatning for funktionerne fsync() eller fdatasync(). Problemet er, at selve disken kan de relevante dataskrivekommandoer. Og hvad der er værre, i nogle særlige tilfælde vil input-output-operationer, der udføres med flaget O_DIRECT, til traditionelle bufferede operationer. Den nemmeste måde at løse dette problem på er at bruge et flag, når man åbner filer O_DSYNC, hvilket vil betyde, at hver skrivingoperation vil blive efterfulgt af et kald fdatasync().
Det viser sig, at der for nylig er blevet tilføjet en 'hurtig vej' til filsystemet XFS for O_DIRECT|O_DSYNC-skrivning af data. Hvis blokken skrives over ved hjælp af O_DIRECT|O_DSYNC, så vil XFS, i stedet for at tømme cachen, udføre FUA-skrivningskommandoen, hvis enheden understøtter det. Jeg bekræftede dette ved hjælp af værktøjet blktrace i Linux 5.4/Ubuntu 20.04. Denne tilgang bør være mere effektiv, da den minimalt skriver data til disken og samtidig anvender én operation i stedet for to (skrivning og tømning af cache). Jeg fandt et link til kernen fra 2018, hvor denne mekanisme er implementeret. Der er en diskussion om anvendelse af denne optimering i andre filsystemer, men så vidt jeg ved, er XFS det eneste filsystem, der understøtter det indtil videre.
Funktionen sync_file_range()
I Linux er der et systemopkald , som gør det muligt at tømme kun en del af en fil til disken, ikke hele filen. Dette opkald initierer en asynkron tømning af data og venter ikke på, at det er afsluttet. Men i dokumentationen til sync_file_range() siges det, at denne kommando er 'meget farlig'. Det anbefales ikke at bruge den. Egenskaberne og farerne sync_file_range() er meget godt beskrevet i materialet. Især ser det ud til, at dette opkald bruger RocksDB til at styre, hvornår kernen tømmer 'beskidte' data til disken. Men samtidig bruges fdatasync(). I RocksDB har interessante kommentarer om dette emne. For eksempel ser det ud til, at opkaldet sync_file_range() ikke fører til tømning af data til disken, når ZFS anvendes. Erfaring fortæller mig, at kode, der sjældent benyttes, måske indeholder fejl. Derfor ville jeg råde imod at bruge dette systemopkald uden en ekstrem nødvendighed.
Systemopkald, der hjælper med at sikre holdbar datalagring
Jeg er kommet frem til, at man til udførelse af ind- og udtægningsoperationer, der sikrer vedvarende datalagring, kan benytte tre metoder. Alle kræver et funktionskald fsync() for den mappe, hvor filen er oprettet. Her er disse metoder:
- Kald af funktionen
fdatasync()ellerfsync()efter funktionenwrite()(det er bedre at benyttefdatasync()). - Arbejdet med filbeskrivere, der er åbnet med flaget
O_DSYNCellerO_SYNC(bedre — med flagetO_DSYNC). - Brug af kommandoen
pwritev2()med flagRWF_DSYNCellerRWF_SYNC(fortrinsvis — med flagetRWF_DSYNC).
Noter om ydeevne
Jeg har ikke udført grundige målinger af ydeevnen for de forskellige mekanismer, jeg har undersøgt. De hastighedsforskelle, jeg har bemærket, er ret små. Det betyder, at jeg kan tage fejl, og at det samme i andre omstændigheder kan vise andre resultater. Først vil jeg tale om, hvad der har den største indflydelse på ydeevnen, og derefter, hvad der har mindre indflydelse på ydeevnen.
- Oprettelse af data i en fil er hurtigere end at tilføje data til filen (ydelsesgevinsten kan være 2-100%). At tilføje data til en fil kræver yderligere ændringer i filens metadata, selv efter systemkaldet
fallocate(), men omfanget af denne effekt kan variere. Jeg anbefaler, at man for at sikre den bedste ydeevne kalderfallocate()til at forudallokere det nødvendige plads. Derefter skal dette rum eksplicit fyldes med nuller og kaldesfsync(). Dette gør, at de relevante blokke i filsystemet bliver markeret som "allokerede", og ikke som "ikke-allokerede". Dette giver en lille (ca. 2%) forbedring af ydeevnen. Desuden kan det hos nogle diske tage længere tid at få adgang til blokkerne som første operation i forhold til andre. Det betyder, at udfyldning af rummet med nuller kan føre til en betydelig (ca. 100%) forbedring af ydeevnen. Særligt kan dette være gældende for diske (dette er uofficielle data, som jeg ikke har kunnet bekræfte). Det samme gælder for lagre (og dette er allerede officielle oplysninger, bekræftet af test). Andre specialister har gjort de samme , der relaterer sig til forskellige diske. - Jo færre systemkald, desto højere ydeevne (gevinsten kan være omkring 5%). Det ser ud til, at kaldet
open()med flagO_DSYNCeller kaldetpwritev2()med flagRWF_SYNCer hurtigere end kaldetfdatasync()Jeg mistænker, at det har noget at gøre med, at der med denne tilgang er færre systemopkald, der kræves for at løse den samme opgave (et opkald i stedet for to). Men forskellen i ydeevnen er meget lille, så du kan fuldstændig ignorere den og bruge det, der ikke vil komplicere logikken i din applikation.
Hvis du er interesseret i emnet bæredygtig datalagring — her er nogle nyttige materialer:
- — en oversigt over grundlæggende mekanismer for input/output.
- — en beskrivelse af, hvad der sker med dataene på vej fra applikationen til disken.
- — svaret på spørgsmålet om, hvornår det er nødvendigt at anvende
fsync()til mapper. For at sige det kort, skal dette gøres, når der oprettes en ny fil, og grunden til denne anbefaling er, at der i Linux kan være mange links til den samme fil. - — her gives der en beskrivelse af, hvordan bæredygtig datalagring er implementeret i SQL Server på Linux-platformen. Her er der nogle interessante sammenligninger mellem systemopkald i Windows og Linux. Jeg er næsten sikker på, at det var dette materiale, der fik mig til at lære om FUA-optimering i XFS.
Har du mistet data, som du troede var sikkert gemt på disken?
Kilde: habr.com
