HÄllbar datalagring och fil-API:er för Linux

Jag, medan jag utforskade datalagringens hÄllbarhet i molnsystem, beslutade att testa mig sjÀlv för att sÀkerstÀlla att jag förstÄr de grundlÀggande sakerna. Jag började med att lÀsa specifikationen för NVMe för att förstÄ vilka garantier gÀllande hÄllbar datalagring (det vill sÀga garantier för att data kommer att vara tillgÀngliga efter systemfel) som NVMe-enheter ger oss. Jag drog följande grundlÀggande slutsatser: man mÄste betrakta data som korrupta frÄn det ögonblick kommandot för skrivning av data ges och fram till dess att skrivningen har slutförts pÄ lagringsmediet. Men i de flesta program för dataskrivning anvÀnds utan problem systemanrop.

I detta material utforskar jag mekanismerna för hÄllbar datalagring som tillhandahÄlls av fil-API:er i Linux. Det verkar som att det hela borde vara enkelt: programmet anropar kommandot write(), och efter det att detta kommando har slutförts, ska data vara sÀkert lagrade pÄ disken. Men write() kopierar bara applikationens data till kÀrnkashen som ligger i det primÀra minnet. För att tvinga systemet att skriva data pÄ disken mÄste man anvÀnda vissa ytterligare mekanismer.

HÄllbar datalagring och fil-API:er för Linux

Sammanfattningsvis utgör detta material en samling anteckningar om vad jag har lÀrt mig om det Àmne som intresserar mig. Om man ska kortfattat beskriva det mest viktiga, sÄ innebÀr det att för att organisera hÄllbar datalagring bör man anvÀnda kommandot fdatasync() eller öppna filer med flaggan O_DSYNC. Om du Àr intresserad av att lÀsa mer ingÄende om vad som hÀnder med data pÄ vÀgen frÄn programkoden till disken, kika pÄ denna artikel.

Funktionens write() egenskaper

Systemanropet write() Àr definierat i standarden IEEE POSIX som ett försök att skriva data till en filbeskrivare. Efter att operationen har slutförts framgÄngsrikt ska write() lÀsoperationer returnera exakt de byte som tidigare har skrivits, Àven om data begÀrs frÄn andra processer eller trÄdar (hÀr den motsvarande delen av POSIX-standarden). HÀr, i avsnittet som handlar om interaktionen mellan trÄdar och vanliga filoperationer, finns en anmÀrkning som sÀger att om var och en av de tvÄ trÄdarna anropar dessa funktioner, sÄ mÄste varje anrop se antingen alla de angivna konsekvenser som ett annat anrop medför, eller inte se nÄgra konsekvenser alls. Detta leder till slutsatsen att alla filin- och utdataoperationer mÄste behÄlla lÄsning pÄ den resurs de arbetar med.

Betyder det att operationen write() Ă€r atomĂ€r? Tekniskt sett — ja. LĂ€soperationer ska returnera antingen allt eller inget av det som skrivits med hjĂ€lp av write(). Men operationen write(), enligt standarden, behöver inte nödvĂ€ndigtvis slutföras genom att skriva allt som den erbjudits att skriva. Den fĂ„r utföra skrivning av endast en del av data. Till exempel kan vi ha tvĂ„ trĂ„dar, var och en av dem lĂ€gger till 1024 byte till en fil som beskrivs av samma filbeskrivare. Ur standardens synvinkel Ă€r det acceptabelt att resultatet Ă€r att varje skrivoperation kan justera filen med endast ett byte. Dessa operationer förblir atomĂ€ra, men efter att de Ă€r fĂ€rdiga, kommer de data som skrivits av dem i filen att vara blandade. HĂ€r Ă€r en mycket intressant diskussion om detta pĂ„ Stack Overflow.

Funktionerna fsync() och fdatasync()

Det enklaste sÀttet att skriva data till disken Àr att anropa funktionen fsync(). Denna funktion begÀr av operativsystemet att överföra alla modifierade block frÄn cachen till disken. Detta inkluderar alla filmetadata (Ätkomsttid, Àndringstid för filen och sÄ vidare). Jag anser att behovet av dessa metadata sÀllan uppstÄr, sÄ om du vet att de inte Àr viktiga för dig, kan du anvÀnda funktionen fdatasync(). I hjÀlpen per fdatasync() anger att under körningen av denna funktion sparas en mÀngd metadata till disken som "Àr nödvÀndig för korrekt utförande av nÀsta datalÀsningsoperationer". Och detta Àr precis vad de flesta applikationer bryr sig om.

Ett av problemen som kan uppstÄ hÀr Àr att dessa mekanismer inte garanterar att filen kan upptÀckas efter en eventuell krasch. Specifikt, nÀr en ny fil skapas, mÄste man anropa fsync() för katalogen som den innehÄller. Annars kan det efter ett fel bli sÄ att denna fil inte finns. Anledningen till detta Àr att i UNIX, pÄ grund av anvÀndning av hÄrda lÀnkar, kan filen existera i flera kataloger. DÀrför, vid anropet fsync() kan filen inte veta vilken specifik katalog som ocksÄ ska skrivas till disken (). Ett typiskt exempel pÄ anvÀndning: om detta kan man lÀsa mer). Det verkar som att filsystemet ext4 har förmÄgan automatiskt att tillÀmpa fsync() till kataloger som innehÄller motsvarande filer, men i fallet med andra filsystem kan det vara annorlunda.

Denna mekanism kan implementeras pĂ„ olika sĂ€tt i olika filsystem. Jag anvĂ€nde blktrace för att fĂ„ veta vilka diskoperationer som anvĂ€nds i filsystemen ext4 och XFS. BĂ„da ger vanliga skrivkommandon till disken bĂ„de för filinnehĂ„ll och för filsystemets logg, skriver ut cachen och avslutar operationen med en FUA-skrivning (Force Unit Access, data skrivs direkt till disken, förbi cachen) i loggen. Sannolikt gör de sĂ„ för att bekrĂ€fta att operationen Ă€gde rum. PĂ„ diskar som inte stödjer FUA orsakar detta tvĂ„ cache-skrivningar. Mina experiment visade att fdatasync() Ă€r nĂ„got snabbare fsync(). Verktyget blktrace indikerar att fdatasync() vanligtvis skriver mindre data till disken (i ext4 fsync() skriver 20 KiB, medan fdatasync() – 16 KiB). Dessutom upptĂ€ckte jag att XFS Ă€r nĂ„got snabbare Ă€n ext4. Och dĂ€r lyckades blktrace fĂ„ veta att fdatasync() skriver mindre data till disken (4 KiB i XFS).

Otydliga situationer som uppstÄr vid anvÀndning av fsync()

Jag kan minnas tre otydliga situationer som handlar om fsync(), som jag stött pÄ i praktiken.

Det första tillfĂ€llet intrĂ€ffade 2008. DĂ„ ”frös” Firefox 3:s grĂ€nssnitt om man skrev till disken med ett stort antal filer. Problemet var att i implementeringen av grĂ€nssnittet för att lagra information om dess tillstĂ„nd anvĂ€ndes en SQLite-databas. Efter varje förĂ€ndring som Ă€gde rum i grĂ€nssnittet anropades funktionen fsync(), vilket gav bra garantier för hĂ„llbar datalagring. I det filsystem som anvĂ€ndes dĂ„, ext3, funktionen fsync() sĂ€nde alla "smutsiga" sidor till disken i systemet, och inte bara de som hade att göra med den aktuella filen. Det innebar att ett klick pĂ„ knappen i Firefox kunde initiera skrivning av megabyte data pĂ„ den magnetiska disken, vilket kunde ta mĂ„nga sekunder. Lösningen pĂ„ problemet, sĂ„ lĂ„ngt jag förstod utifrĂ„n detta materialet, var att flytta databashanteringen till asynkrona bakgrundsuppgifter. Det betyder att det tidigare i Firefox fanns strĂ€ngare krav pĂ„ datalagringens motstĂ„ndskraft Ă€n vad som faktiskt var nödvĂ€ndigt, och egenskaperna hos filsystemet ext3 förvĂ€rrade bara detta problem.

Det andra problemet intrÀffade 2009. Efter en systemkrasch stötte anvÀndare av det nya filsystemet ext4 pÄ att mÄnga nyligen skapade filer hade en noll lÀngd, medan sÄdana problem inte intrÀffade med det Àldre filsystemet ext3. I det föregÄende stycket nÀmnde jag att ext3 sÀnde för mycket data till disken, vilket kraftigt försÀmrade prestandan. fsync()För att förbÀttra situationen sÀnder ext4 endast de "smutsiga" sidorna som har att göra med den specifika filen till disken. Data frÄn andra filer stannar kvar i minnet under en mycket lÀngre tid Àn nÀr ext3 anvÀnds. Detta gjordes för att förbÀttra prestanda (som standard förblir data i detta tillstÄnd i 30 sekunder, och detta kan justeras med hjÀlp av dirty_expire_centisecs; ). Ett typiskt exempel pÄ anvÀndning: som kan hittas i ytterligare material om detta). Det innebÀr att en stor mÀngd data kan gÄ förlorad efter en krasch. Lösningen pÄ detta problem handlar om anvÀndning av fsync() i applikationer som behöver sÀkerstÀlla motstÄndskraftig datalagring och maximalt skydda den mot konsekvenserna av krasch. Funktionen fsync() Àr mycket effektivare vid anvÀndning av ext4 Àn vid anvÀndning av ext3. Nackdelen med detta tillvÀgagÄngssÀtt Àr att dess tillÀmpning, liksom tidigare, saktar ner genomförandet av vissa operationer, som installation av program. Mer information om detta finns i hÀr och hÀr.

Det tredje problemet, som rör fsync(), intrÀffade 2018. Inom ramen för PostgreSQL-projektet fick man reda pÄ att om funktionen fsync() stöter pÄ ett fel, markerar den "smutsiga" sidor som "rena". Som ett resultat markerar följande anrop fsync() Inga ÄtgÀrder vidtas med sÄdana sidor. PÄ grund av detta lagras modifierade sidor i minnet och skrivs aldrig till disken. Detta Àr en riktig katastrof, eftersom applikationen kommer att tro att vissa data har skrivits till disken, nÀr de i sjÀlva verket inte har det. SÄdana fel fsync() Àr sÀllsynta, applikationen kan nÀstan inte göra nÄgot för att hantera problemet i sÄdana situationer. Numera, nÀr detta intrÀffar, kraschar PostgreSQL och andra applikationer. HÀr, i materialet «Kan applikationer ÄterhÀmta sig frÄn fsync-fel?», undersöks detta problem i detalj. För nÀrvarande Àr den bÀsta lösningen pÄ detta problem att anvÀnda Direct I/O med flaggan O_SYNC eller med flaggan O_DSYNC. Med denna metod rapporterar systemet om fel som kan uppstÄ vid specifika dataskrivningsoperationer, men denna metod krÀver att applikationen hanterar sina egna buffertar. LÀs mer om detta hÀr och hÀr.

Öppna filer med hjĂ€lp av flaggorna O_SYNC och O_DSYNC

LĂ„t oss Ă„tervĂ€nda till diskussionen om Linux-mekanismer som sĂ€kerstĂ€ller hĂ„llbar datalagring. NĂ€mligen handlar det om att anvĂ€nda flaggan O_SYNC eller flaggan O_DSYNC vid öppning av filer med hjĂ€lp av systemanropet open(). Med denna metod utförs varje dataskrivningsoperation som om systemet efter varje kommando ger motsvarande instruktioner write() enligt POSIX-specifikationerna fsync() och fdatasync(). I detta kallas «Synchronized I/O File Integrity Completion» och «Data Integrity Completion». Den frĂ€msta fördelen med denna metod Ă€r att för att sĂ€kerstĂ€lla dataintegritet behöver bara ett systemanrop göras, inte tvĂ„ (till exempel — ). Huvudnackdelen med denna metod Ă€r att alla skrivoperationer som anvĂ€nder den relevanta filbeskrivaren kommer att synkroniseras, vilket kan begrĂ€nsa möjligheterna att strukturera applikationskoden. write() och fdatasync()AnvĂ€ndning av Direct I/O med flaggan O_DIRECT

stödjer flaggan

Systemanropet open() O_DIRECT , som Àr avsedd att, genom att kringgÄ operativsystemets cache, utföra in- och utdataoperationer genom att interagera direkt med disken. Detta innebÀr i mÄnga fall att skrivkommandon som utfÀrdas av programmet direkt översÀtts till kommandon som riktas till disken. Men i allmÀnhet Àr denna mekanism inte en ersÀttning för funktioner. Det handlar om att sjÀlva disken kan fsync() eller fdatasync()skjuta upp eller cacha skjuta upp eller cachelagra relevanta dataskriverkommandon. Och vÀrre Àn sÄ, i vissa sÀrskilda fall kan I/O-operationer som utförs med flaggan , som Àr avsedd att, genom att kringgÄ operativsystemets cache, utföra in- och utdataoperationer genom att interagera direkt med disken. Detta innebÀr i mÄnga fall att skrivkommandon som utfÀrdas av programmet direkt översÀtts till kommandon som riktas till disken. Men i allmÀnhet Àr denna mekanism inte en ersÀttning för funktioner, översÀtts till traditionella buffrade operationer. Det enklaste sÀttet att lösa detta problem Àr att anvÀnda en flagga för att öppna filer O_DSYNC, vilket kommer att innebÀra att varje skrivoperation följs av ett samtal fdatasync().

Det visade sig att ett "snabbt sÀtt" nyligen har lagts till i XFS-filsystemet för O_DIRECT|O_DSYNC-dataskrivningar. Om en block skrivs över med hjÀlp av O_DIRECT|O_DSYNC, sÄ kommer XFS, istÀllet för att spola cachen, att utföra FUA-skrivkommandot om enheten stödjer det. Jag bekrÀftade detta genom att anvÀnda verktyget blktrace i Linux 5.4/Ubuntu 20.04. Denna metod bör vara effektivare, eftersom den skriver ett minimum av data till disken och endast utför en operation i stÀllet för tvÄ (skrivning och spola cachen). Jag fann en lÀnk till patch kÀrnan frÄn 2018 dÀr denna mekanism Àr implementerad. DÀr finns en diskussion som handlar om tillÀmpningen av denna optimering i andra filsystem, men sÄvitt jag vet Àr XFS det enda filsystemet som stödjer detta hittills.

funktionen sync_file_range()

I Linux finns det ett systemanrop sync_file_range(), som tillÄter att endast en del av en fil spolas till disken, inte hela filen. Detta anrop initierar en asynkron avskrivning av data och vÀntar inte pÄ dess slutförande. Men i dokumentationen för sync_file_range() nÀmns det att detta kommando Àr "mycket farligt". Det rekommenderas inte att anvÀnda det. Funktionerna och farorna sync_file_range() Àr mycket vÀl beskrivna i detta materialet. SÀrskilt verkar det som att detta anrop anvÀnder RocksDB för att hantera nÀr kÀrnan spolar "smutsiga" data till disken. Men dÀr anvÀnds ocksÄ fdatasync(). I koden RocksDB har intressanta kommentarer om detta Àmne. Till exempel verkar det som att anropet sync_file_range() vid anvÀndning av ZFS inte leder till att data spolas till disken. Min erfarenhet sÀger mig att kod som sÀllan anvÀnds kan innehÄlla buggar. DÀrför skulle jag rÄda dig att inte anvÀnda detta systemanrop utan absolut nödvÀndighet.

Systemanrop som hjÀlper till att sÀkerstÀlla hÄllbar datalagring

Jag har kommit fram till att det finns tre tillvÀgagÄngssÀtt för att utföra in- och utdataoperationer som sÀkerstÀller stabil lagring av data. Alla krÀver att man anropar en funktion fsync() för katalogen dÀr filen skapades. HÀr Àr dessa tillvÀgagÄngssÀtt:

  1. Anrop av funktionen fdatasync() eller fsync() efter funktionen write() (bÀst att anvÀnda fdatasync()).
  2. Arbete med en filbeskrivare som öppnats med flaggan O_DSYNC eller O_SYNC (bĂ€st — med flaggan O_DSYNC).
  3. AnvĂ€nda kommandot pwritev2() med flaggan RWF_DSYNC eller RWF_SYNC (föredrar — med flaggan RWF_DSYNC).

Prestandanoteringar

Jag har inte utfört noggranna mÀtningar av prestandan för de olika mekanismer jag har undersökt. De skillnader jag har observerat i hastighet Àr ganska smÄ. Det betyder att jag kan ha fel, och att samma sak kan ge andra resultat under andra förhÄllanden. Först kommer jag att berÀtta om vad som har störst pÄverkan pÄ prestandan, och sedan om vad som pÄverkar prestandan mindre.

  1. Överskrivning av filens data Ă€r snabbare Ă€n att bifoga data till filen (prestandavinsten kan vara 2-100%). Att bifoga data till filen krĂ€ver ytterligare Ă€ndringar i filens metadata, Ă€ven efter systemanropet fallocate(), men omfattningen av denna effekt kan variera. Jag rekommenderar att man anropar fallocate() för att förhandsallokera nödvĂ€ndigt utrymme. Sedan mĂ„ste detta utrymme explicit fyllas med nollor och anropas fsync(). Tack vare detta kommer motsvarande block i filsystemet att mĂ€rkas som "allokerade" istĂ€llet för "icke-allokerade". Detta ger en liten (ungefĂ€r 2%) förbĂ€ttring av prestandan. Dessutom kan den första Ă„tkomstoperationen till en block pĂ„ vissa diskar ta lĂ€ngre tid Ă€n andra. Det betyder att fyllning av utrymme med nollor kan leda till ett betydande (ungefĂ€r 100%) förbĂ€ttring av prestandan. Speciellt kan detta intrĂ€ffa med diskar AWS EBS (detta Ă€r icke-officiell data, jag kunde inte bekrĂ€fta det). Detsamma gĂ€ller lagringarna GCP Persistent Disk (och detta Ă€r redan officiell information, bekrĂ€ftad genom tester). Andra experter har gjort liknande observationer, relaterade till olika diskar.
  2. Ju fÀrre systemanrop, desto högre prestanda (vinsten kan vara omkring 5%). Det verkar som att anropet open() med flaggan O_DSYNC eller anropet pwritev2() med flaggan RWF_SYNC snabbare anrop fdatasync(). Jag misstÀnker att det handlar om att fÀrre systemanrop krÀvs för att lösa samma uppgift (ett anrop istÀllet för tvÄ). Men skillnaden i prestanda Àr mycket liten, sÄ du kan helt enkelt ignorera den och anvÀnda det som inte försvÄrar logiken i din applikation.

Om du Àr intresserad av Àmnet hÄllbar datalagring - hÀr Àr nÄgra anvÀndbara material:

  • I/O Åtkomstmetoder — en översikt över grunderna för in- och utmatningsmekanismer.
  • SĂ€kerstĂ€llande av att data nĂ„r disken — en berĂ€ttelse om vad som hĂ€nder med data pĂ„ vĂ€gen frĂ„n applikationen till disken.
  • NĂ€r ska du fsynca den innehĂ„llande katalogen — svar pĂ„ frĂ„gan om nĂ€r fsync() ska tillĂ€mpas pĂ„ kataloger. Om man sammanfattar detta, sĂ„ ska det göras vid skapandet av en ny fil, och anledningen till denna rekommendation Ă€r att det kan finnas mĂ„nga lĂ€nkar till samma fil i Linux.
  • SQL Server pĂ„ Linux: FUA Interna — hĂ€r finns en beskrivning av hur hĂ„llbar datalagring implementeras i SQL Server pĂ„ Linux-plattformen. Det görs nĂ„gra intressanta jĂ€mförelser mellan systemanrop i Windows och Linux. Jag Ă€r nĂ€stan sĂ€ker pĂ„ att det var tack vare detta material som jag lĂ€rde mig om FUA-optimering av XFS.

Har du förlorat data som du trodde var sÀkert lagrade pÄ disken?

HÄllbar datalagring och fil-API:er för Linux

HÄllbar datalagring och fil-API:er för Linux

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster