Bij het verkennen van de duurzaamheid van gegevensopslag in cloudsystemen, besloot ik mezelf uit te dagen en te bevestigen dat ik de basisconcepten begrijp. Ik om te begrijpen welke garanties omtrent duurzame gegevensopslag (d.w.z. garanties dat de gegevens toegankelijk zullen zijn na een systeemfout) de NVMe-schijven bieden. Mijn belangrijkste conclusies waren als volgt: gegevens dienen als beschadigd te worden beschouwd vanaf het moment dat het schrijfcommando is gegeven, tot het moment dat het schrijven naar het opslagmedium is voltooid. Echter, in de meeste gegevensopslagprogramma's worden systeemaanroepen vrijelijk gebruikt.
In dit artikel onderzoek ik de mechanismen voor duurzame gegevensopslag die de Linux-bestandse API's bieden. Het lijkt erop dat dit eenvoudig zou moeten zijn: het programma roept het commando write(), en zodra het werk van dit commando is voltooid, zouden de gegevens veilig op de schijf moeten zijn opgeslagen. Maar write() dit schrijft alleen de gegevens van de applicatie naar de kerncache, die zich in het RAM bevindt. Om het systeem te dwingen de gegevens naar de schijf te schrijven, moeten er enkele aanvullende mechanismen worden gebruikt.
Uiteindelijk zijn dit aantekeningen over wat ik heb geleerd over het onderwerp dat mij interesseert. Om het belangrijkste kort samen te vatten: voor het organiseren van duurzame gegevensopslag moet je het commando fdatasync() gebruiken of bestanden openen met de vlag O_DSYNC. Als je geĆÆnteresseerd bent in de details van wat er met de gegevens gebeurt op de weg van de code naar de schijf, kijk dan naar de artikel.
De kenmerken van het gebruik van de write() functie
Systeemaanroep write() is gedefinieerd in de standaard als een poging om gegevens naar een bestandsdescriptor te schrijven. Na een succesvolle voltooiing van de operation write() moeten de leesoperaties precies die bytes retourneren die eerder zijn geschreven, zelfs als de gegevens door andere processen of threads worden benaderd ( de bijbehorende sectie van de POSIX-standaard). , in de sectie die gewijd is aan de interactie van threads met reguliere bestandbewerkingen, staat een opmerking dat als elk van de twee threads deze functies aanroept, elke aanroep ofwel alle aangeduide gevolgen van de uitvoering van de andere aanroep moet zien, of helemaal geen gevolgen moet zien. Dit leidt tot de conclusie dat alle invoer-/uitvoerbestandsbewerkingen de hulpbron waar ze mee werken, moeten vergrendelen.
Betekent dit dat de operatie write() atomair is? Technisch gezien ā ja. Gegevenslezen moet ofwel alles teruggeven of niets van wat werd geschreven met behulp van write(). Maar de operatie write(), volgens de standaard, hoeft niet noodzakelijk te eindigen met het schrijven van alles wat haar aangeboden is om te schrijven. Het is toegestaan om slechts een deel van de gegevens te schrijven. Bijvoorbeeld, we kunnen twee threads hebben, elk die 1024 bytes aan een bestand toevoegt dat wordt beschreven door dezelfde bestandsdescriptor. Vanuit het perspectief van de standaard is het aanvaardbaar dat elke schrijfbewerking slechts ƩƩn byte aan het bestand kan toevoegen. Deze bewerkingen blijven atomair, maar nadat ze zijn voltooid, zullen de door hen in het bestand geschreven gegevens door elkaar zijn gehusseld. een zeer interessante discussie hierover op Stack Overflow.
De functies fsync() en fdatasync()
De eenvoudigste manier om gegevens naar de schijf te schrijven is door de functie aan te roepen . Deze functie vraagt het besturingssysteem om alle gewijzigde blokken van de cache naar de schijf te verplaatsen. Dit omvat ook alle metadata van het bestand (toegangs- en wijzigingstijd van het bestand, enzovoort). Ik geloof dat de noodzaak voor deze metadata zelden voorkomt, dus als je weet dat ze voor jou niet belangrijk zijn, kun je de functie gebruiken fdatasync()wordt een tabel met wijzigingen en instructies voor de overgang naar de nieuwe configuratie gegeven. Voor meer informatie, zie voor fdatasync() geeft aan dat tijdens het uitvoeren van deze functie het naar de schijf schrijven van zo veel metadata gebeurt als 'nodig voor de correcte uitvoering van de volgende gegevenslezen.' En dit is precies wat de meeste applicaties betreft.
Een van de problemen die hier kan ontstaan, is dat deze mechanismen niet garanderen dat het bestand na een mogelijke storing kan worden aangetroffen. In het bijzonder, wanneer een nieuw bestand wordt aangemaakt, moet je aanroepen fsync() voor de directory die het bevat. Anders kan het na een storing zijn dat dit bestand niet bestaat. Dit komt doordat in UNIX, vanwege het gebruik van harde links, een bestand in meerdere directories kan bestaan. Daarom bij het aanroepen fsync() is er voor een bestand geen manier om te weten welke specifieke directory ook naar disk moet worden weggeschreven ( hierover kan je meer lezen). Het lijkt erop dat het bestandssysteem ext4 in staat is toepassen fsync() om directories te bevatten die de overeenkomstige bestanden bevatten, maar in het geval van andere bestandssystemen kan dit anders zijn.
Dit mechanisme kan op verschillende manieren worden geĆÆmplementeerd in verschillende bestandssystemen. Ik heb gebruik om te onderzoeken welke schijfoperaties worden gebruikt in de bestandssystemen ext4 en XFS. Beide genereren gebruikelijke schrijfcommando's naar disk, zowel voor de inhoud van bestanden als voor de journal van het bestandssysteem, wissen de cache en sluiten af door een FUA-write (Force Unit Access, het schrijven van gegevens rechtstreeks naar de schijf, omzeiling van de cache) naar het journal uit te voeren. Vermoedelijk doen ze dit om de voltooiing van een operatie te bevestigen. Op schijven die FUA niet ondersteunen, veroorzaakt dit twee cache-wissels. Mijn experimenten toonden aan dat fdatasync() iets sneller fsync(). Hulpprogramma blktrace duidt erop dat fdatasync() doorgaans minder gegevens naar disk schrijft (in ext4 fsync() schrijft 20 KiB, terwijl fdatasync() 16 KiB schrijft). Bovendien ontdekte ik dat XFS iets sneller is dan ext4. En hier met behulp van blktrace werd vastgesteld dat fdatasync() minder gegevens naar disk schrijft (4 KiB in XFS).
Ambigue situaties die zich voordoen bij het gebruik van fsync()
Ik kan me drie ambigue situaties herinneren die betrekking hebben op fsync(), waarmee ik in de praktijk te maken kreeg.
De eerste van deze gevallen deed zich voor in 2008. Toen 'hangde' de interface van Firefox 3 als er een grote hoeveelheid bestanden op disk werd weggeschreven. Het probleem was dat er bij de implementatie van de interface een SQLite-database werd gebruikt voor het opslaan van statusinformatie. Na elke wijziging die in de interface plaatsvond, werd de functie fsync(), wat goede garanties bood voor de duurzame opslag van gegevens. In het gebruikte bestandssysteem ext3 werd de functie fsync() reset alle "dirty" pagina's in het systeem naar de schijf, en niet alleen die welke verband hielden met het betreffende bestand. Dit betekende dat een klik op de knop in Firefox de opname van megabytes aan gegevens op de magnetische schijf kon initiƫren, wat vele seconden kon duren. De oplossing voor het probleem, voor zover ik begreep uit het materiaal, was om de database-werkzaamheden naar asynchrone achtergrondtaken te verplaatsen. Dit betekent dat eerder in Firefox strengere eisen stelden aan de duurzaamheid van gegevensopslag dan werkelijk nodig was, en de kenmerken van het ext3-bestandssysteem verslechterden dit probleem alleen maar.
De tweede inconsistente situatie deed zich voor in 2009. Toen, na een systeemcrash, kwamen gebruikers van het nieuwe ext4-bestandssysteem erachter dat veel recent aangemaakte bestanden een nul-lengte hadden, terwijl dit met het oudere ext3-bestandssysteem niet het geval was. In de vorige alinea sprak ik erover dat ext3 te veel gegevens naar de schijf wegschreef, waardoor de prestaties sterk verminderden. fsync()Om de situatie te verbeteren, schrijft ext4 alleen de "dirty" pagina's naar de schijf die betrekking hebben op het specifieke bestand. En gegevens van andere bestanden blijven veel langer in het geheugen dan bij het gebruik van ext3. Dit is gedaan ter verbetering van de prestaties (standaard blijven gegevens in deze staat 30 seconden, dit kan worden ingesteld met behulp van ; het is mogelijk om aanvullende materialen hierover te vinden). Dit betekent dat een groot volume aan gegevens definitief kan worden verloren na een crash. De oplossing voor dit probleem is het gebruik van fsync() in applicaties die duurzame gegevensopslag moeten garanderen en deze zoveel mogelijk willen beschermen tegen de gevolgen van crashes. De functie fsync() werkt bij gebruik van ext4 veel efficiƫnter dan bij gebruik van ext3. Een nadeel van deze aanpak is dat het, zoals voorheen, de uitvoering van bepaalde operaties, zoals het installeren van programma's, vertraagt. Voor verdere details kunt u kijken naar en .
Het derde probleem, met betrekking tot fsync(), deed zich voor in 2018. Toen, in het kader van het PostgreSQL-project, werd ontdekt dat als de functie fsync() tegen een fout aanloopt, het "dirty" pagina's als "schoon" markeert. Als gevolg hiervan worden de volgende aanroepen fsync() met zulke pagina's gebeurt niets. Hierdoor worden de gemodificeerde pagina's in het geheugen opgeslagen en nooit op de schijf geschreven. Dit is een echte ramp, omdat de applicatie zal denken dat sommige gegevens op de schijf zijn geschreven, terwijl dat in werkelijkheid niet het geval is. Dergelijke storingen fsync() komen zelden voor, de applicatie kan in zulke situaties bijna niets doen om het probleem op te lossen. Tegenwoordig, wanneer dit gebeurt, sluiten PostgreSQL en andere applicaties onverwacht af. , in het artikel "Kan Applicaties Herstellen van fsync Fouten?", wordt dit probleem in detail onderzocht. Momenteel is de beste oplossing voor dit probleem het gebruik van Direct I/O met de vlag O_SYNC of met de vlag O_DSYNC. Met deze aanpak zal het systeem fouten rapporteren die kunnen optreden bij het uitvoeren van specifieke gegevensschrijfacties, maar deze aanpak vereist dat de applicatie de buffers zelf beheert. Lees meer hierover en .
Bestanden openen met de vlaggen O_SYNC en O_DSYNC
Laten we terugkomen op de discussie over de mechanismen van Linux die zorgen voor duurzame gegevensopslag. Namelijk, het gaat om het gebruik van de vlag O_SYNC of de vlag O_DSYNC bij het openen van bestanden met behulp van de systeemaanroep . Met deze aanpak wordt elke gegevensschrijfactie uitgevoerd alsof het systeem na elke opdracht write() de opdrachten geeft, overeenkomstig fsync() en fdatasync()wordt een tabel met wijzigingen en instructies voor de overgang naar de nieuwe configuratie gegeven. Voor meer informatie, zie dit wordt "Synchronized I/O File Integrity Completion" en "Data Integrity Completion" genoemd. Het belangrijkste voordeel van deze aanpak is dat voor gegevensintegriteit slechts ƩƩn systeemaanroep nodig is, in plaats van twee (bijvoorbeeld ā write() en fdatasync()). Het grootste nadeel van deze aanpak is dat alle schrijfacties die de overeenkomstige bestandshandle gebruiken, gesynchroniseerd zullen worden, wat de mogelijkheden om de code van de applicatie te structureren kan beperken.
Het gebruik van Direct I/O met de vlag O_DIRECT
Systeemaanroep open() ondersteunt de vlag O_DIRECT, die is ontworpen om invoer- en uitvoeroperaties uit te voeren door rechtstreeks met de schijf te communiceren, waarbij de cache van het besturingssysteem wordt omzeild. Dit betekent in veel gevallen dat de schrijfopdrachten die door het programma worden gegeven, rechtstreeks worden omgezet in opdrachten die op de schijf werken. Maar over het algemeen is dit mechanisme geen vervanging voor de functies fsync() of fdatasync(). Het punt is dat de schijf zelf kan de bijbehorende commando's voor gegevensopslag. En wat nog erger is, in sommige specifieke gevallen worden invoer- en uitvoeroperaties, uitgevoerd met de vlag O_DIRECT, in traditionele gebufferde operaties. Dit probleem kan het gemakkelijkst worden opgelost door ook de vlag O_DSYNC, wat betekent dat na elke schrijfoperatie een aanroep zal volgen fdatasync().
Het bleek dat er onlangs een 'snelle route' is toegevoegd aan het XFS-bestandssysteem voor O_DIRECT|O_DSYNC-gegevensopslag. Als een blok opnieuw wordt geschreven met behulp van O_DIRECT|O_DSYNC, dan zal XFS, in plaats van de cache te wissen, de FUA-schrijfopdracht uitvoeren, als het apparaat dit ondersteunt. Dit heb ik bevestigd met behulp van de utility blktrace in Linux 5.4/Ubuntu 20.04. Deze aanpak zou efficiënter moeten zijn, omdat er bij gebruik hiervan een minimaal aantal gegevens naar de schijf wordt geschreven en er slechts één opdracht wordt uitgevoerd, niet twee (schrijven en cache wissen). Ik vond een verwijzing naar de kernel uit 2018 waarin dit mechanisme is geïmplementeerd. Daarin is een discussie over het gebruik van deze optimalisatie in andere bestandssystemen, maar voor zover ik weet, is XFS momenteel het enige bestandssysteem dat dit ondersteunt.
De functie sync_file_range()
In Linux is er een systeemaanroep , waarmee slechts een deel van een bestand naar de schijf kan worden gewist, niet het hele bestand. Deze aanroep initieert een asynchrone wissen van gegevens en wacht niet op de voltooiing hiervan. Maar in de documentatie van sync_file_range() staat dat deze opdracht 'zeer gevaarlijk' is. Het gebruik ervan wordt niet aanbevolen. De kenmerken en gevaren sync_file_range() worden zeer goed beschreven in het materiaal. In het bijzonder lijkt deze aanroep RocksDB te gebruiken om te beheren wanneer de kernel 'vuile' gegevens naar de schijf wegschrijft. Maar daarvoor wordt ook gebruikt om een betrouwbare gegevensopslag te waarborgen fdatasync()wordt een tabel met wijzigingen en instructies voor de overgang naar de nieuwe configuratie gegeven. Voor meer informatie, zie RocksDB heeft interessante opmerkingen hierover. Bijvoorbeeld, het lijkt erop dat de aanroep sync_file_range() bij het gebruik van ZFS niet leidt tot het wissen van gegevens op de schijf. Mijn ervaring leert me dat code die zelden wordt gebruikt, mogelijk fouten bevat. Daarom zou ik aanraden deze systeemaanroep niet te gebruiken zonder dringende noodzaak.
Systeemaanroepen die helpen bij het waarborgen van een betrouwbare gegevensopslag
Ik heb geconcludeerd dat voor het uitvoeren van invoer-/uitvoeroperaties, die een duurzame opslag van gegevens waarborgen, drie benaderingen kunnen worden gebruikt. Alle vereisen een functieaanroep fsync() voor de directory waarin het bestand is aangemaakt. Dit zijn de benaderingen:
- De functieaanroep
fdatasync()offsync()na de functiewrite()(het is beter om te gebruikenfdatasync()). - Werken met een bestandsdescriptor die is geopend met de vlag
O_DSYNCofO_SYNC(bij voorkeur ā met de vlagO_DSYNC). - Het gebruik van het commando
pwritev2()met de vlagRWF_DSYNCofRWF_SYNC(bij voorkeur ā met de vlagRWF_DSYNC).
Notities over prestaties
Ik heb me niet beziggehouden met gedetailleerde metingen van de prestaties van de verschillende mechanismen die ik heb onderzocht. De verschillen in snelheid die ik heb opgemerkt zijn redelijk klein. Dit betekent dat ik me kan vergissen, en dat onder andere omstandigheden hetzelfde andere resultaten kan laten zien. Eerst zal ik vertellen wat een grotere impact heeft op de prestaties, en daarna wat minder invloed heeft.
- Het overschrijven van gegevens in een bestand is sneller dan het toevoegen van gegevens aan een bestand (de prestatiewinst kan 2-100% zijn). Het toevoegen van gegevens aan een bestand vereist extra wijzigingen in de metadata van het bestand, zelfs na de systeemaanroep
fallocate(), maar de omvang van dit effect kan variƫren. Ik raad aan, voor de beste prestaties, om aan te roepenfallocate()voor het vooraf reserveren van de benodigde ruimte. Vervolgens moet deze ruimte expliciet met nullen worden gevuld en moet worden aangeroepenfsync(). Hierdoor worden de overeenkomstige blokken in het bestandssysteem gemarkeerd als 'gereserveerd', en niet als 'niet-gereserveerd'. Dit zorgt voor een kleine (ongeveer 2%) prestatieverbetering. Bovendien kan bij sommige schijven de eerste toegang tot een blok langzamer zijn dan bij andere. Dit betekent dat het vullen van ruimte met nullen kan leiden tot een significante (ongeveer 100%) prestatieverbetering. Dit kan vooral gebeuren met schijven (dit zijn niet-officiƫle gegevens, ik kon ze niet bevestigen). Hetzelfde geldt voor opslag (dit is al officiƫle informatie, bevestigd door tests). Andere specialisten hebben soortgelijke , die betrekking hebben op verschillende schijven. - Hoe minder systeemaanroepen, hoe hoger de prestaties (de winst kan ongeveer 5% zijn). Het lijkt erop dat de aanroep
open()met de vlagO_DSYNCof de aanroeppwritev2()met de vlagRWF_SYNCsneller is dan de aanroepfdatasync()Ik vermoed dat dit te maken heeft met het feit dat met deze benadering het aantal systeemaanroepen voor het oplossen van dezelfde taak verminderd wordt (ƩƩn aanroep in plaats van twee). Maar het prestatieverschil is zeer klein, dus je kunt er gerust overheen kijken en in de toepassing gebruiken wat de logica niet complicert.
Als je geĆÆnteresseerd bent in het onderwerp van duurzaam dataverzending, hier zijn enkele nuttige artikelen:
- ā een overzicht van de basismechanismen voor invoer/uitvoer.
- ā een verhaal over wat er gebeurt met de gegevens onderweg van de toepassing naar de schijf.
- ā het antwoord op de vraag wanneer men het moet toepassen
fsync()voor directories. Om het in twee woorden samen te vatten, moet dit gebeuren bij het aanmaken van een nieuw bestand, en de reden voor deze aanbeveling is dat er in Linux veel koppelingen naar hetzelfde bestand kunnen zijn. - ā hier is een beschrijving van hoe duurzaam dataverzending is geĆÆmplementeerd in SQL Server op het Linux-platform. Hier zijn enkele interessante vergelijkingen tussen de systeemaanroepen van Windows en Linux. Ik ben er bijna zeker van dat ik dankzij dit artikel over de FUA-optimalisatie van XFS heb geleerd.
Heb je ooit gegevens verloren waarvan je dacht dat ze veilig op de schijf waren opgeslagen?
Bron: habr.com
