Nachhaltige Datenspeicherung und Linux-Datei-APIs

Ich habe, wĂ€hrend ich die StabilitĂ€t der Datenspeicherung in Cloud-Systemen untersuchte, beschlossen, mich selbst zu ĂŒberprĂŒfen und sicherzustellen, dass ich die grundlegenden Dinge verstehe. Ich begann mit dem Lesen der NVMe-Spezifikation , um herauszufinden, welche Garantien hinsichtlich der stabilen Datenspeicherung (also die Garantie, dass die Daten nach einem Systemausfall verfĂŒgbar sind) uns NVMe-Disks bieten. Ich habe die folgenden wesentlichen Erkenntnisse gewonnen: Daten sollten als beschĂ€digt betrachtet werden, vom Zeitpunkt der Ausgabe des Schreibbefehls bis zum Abschluss des Schreibvorgangs auf das Speichermedium. In den meisten Datenaufzeichnungsprogrammen werden jedoch ganz normal Systemaufrufe verwendet.

In diesem Material untersuche ich die Mechanismen der stabilen Datenspeicherung, die durch die Datei-APIs von Linux bereitgestellt werden. Es scheint, als sollte alles einfach sein: Das Programm ruft den Befehl write(), und nachdem dieser Befehl abgeschlossen ist, sollten die Daten sicher auf der Festplatte gespeichert sein. Aber write() kopiert lediglich die Anwendungsdaten in den Cache des Kernels, der sich im Hauptspeicher befindet. Um das System zum Schreiben der Daten auf die Festplatte zu zwingen, mĂŒssen einige zusĂ€tzliche Mechanismen verwendet werden.

Nachhaltige Datenspeicherung und Linux-Datei-APIs

Insgesamt stellt dieses Material eine Sammlung von Notizen zu dem dar, was ich ĂŒber das Thema, das mich interessiert, herausgefunden habe. Kurz gesagt: FĂŒr die Organisation der stabilen Datenspeicherung sollte der Befehl fdatasync() oder das Öffnen von Dateien mit dem Flag O_DSYNCverwendet werden. Wenn Sie mehr ĂŒber die Details erfahren möchten, was mit den Daten auf dem Weg vom Programmcode zur Festplatte passiert, schauen Sie sich die Maschine verwendet. Artikel an.

Besonderheiten der Verwendung der Funktion write()

Systemaufruf write() wird im Standard IEEE POSIX definiert als ein Versuch, Daten in einen Dateideskriptor zu schreiben. Nach erfolgreichem Abschluss write() sollten DatenlesevorgĂ€nge genau die Bytes zurĂŒckgeben, die vorher geschrieben wurden, selbst wenn auf die Daten aus anderen Prozessen oder Threads zugegriffen wird (hier entsprechender Abschnitt des POSIX-Standards). Hier, im Abschnitt ĂŒber die Interaktion von Threads mit herkömmlichen Dateioperationen gibt es eine Anmerkung, die besagt, dass wenn jeder von zwei Threads diese Funktionen aufruft, jeder Aufruf entweder alle angegebenen Auswirkungen, die aus der AusfĂŒhrung des anderen Aufrufs resultieren, oder gar keine Auswirkungen sehen muss. Das fĂŒhrt zu dem Schluss, dass alle Ein- und Ausgabe-Dateioperationen die Sperre der Ressource, mit der sie arbeiten, aufrechterhalten mĂŒssen.

Bedeutet das, dass die Operation write() atomar ist? Aus technischer Sicht — ja. Leseoperationen sollten entweder alles oder nichts von dem zurĂŒckgeben, was durch write()geschrieben wurde. Aber die Operation write()muss gemĂ€ĂŸ dem Standard nicht unbedingt abgeschlossen werden, indem sie alles, was ihr angeboten wurde, schreibt. Es ist erlaubt, nur einen Teil der Daten zu schreiben. Zum Beispiel können wir zwei Threads haben, die jeweils 1024 Bytes an eine Datei anhĂ€ngen, die durch denselben Dateideskriptor beschrieben wird. Aus der Sicht des Standards ist es akzeptabel, wenn jede der Schreiboperationen nur ein Byte an die Datei anhĂ€ngen kann. Diese Operationen bleiben atomic, aber nachdem sie abgeschlossen sind, sind die Daten, die sie in die Datei geschrieben haben, vermischt. Hier eine sehr interessante Diskussion zu diesem Thema auf Stack Overflow.

Die Funktionen fsync() und fdatasync()

Der einfachste Weg, Daten auf die Festplatte zu schreiben, besteht darin, die Funktion fsync(). Diese Funktion fordert das Betriebssystem auf, alle modifizierten Blöcke aus dem Cache auf die Festplatte zu ĂŒbertragen. Dazu gehören auch alle Metadaten der Datei (Zugriffszeit, Änderungszeit der Datei usw.). Ich denke, dass der Bedarf an diesen Metadaten selten auftritt, also wenn Sie wissen, dass sie fĂŒr Sie nicht wichtig sind, können Sie die Funktion fdatasync(). In Hilfe nach fdatasync() sagt, dass im Laufe dieser Funktion eine solche Menge an Metadaten auf die Festplatte gespeichert wird, die 'notwendig ist, um die nĂ€chsten Leseoperationen korrekt auszufĂŒhren'. Und das ist genau das, was die meisten Anwendungen beschĂ€ftigt.

Ein Problem, das hier auftreten kann, besteht darin, dass diese Mechanismen nicht garantieren, dass die Datei nach einem möglichen Absturz wieder gefunden werden kann. Insbesondere muss beim Erstellen einer neuen Datei ein Aufruf getĂ€tigt werden fsync() fĂŒr das Verzeichnis, das ihn enthĂ€lt. Andernfalls kann es nach einem Fehler so sein, dass diese Datei nicht existiert. Der Grund dafĂŒr liegt darin, dass unter UNIX, aufgrund der Verwendung von Hardlinks, eine Datei in mehreren Verzeichnissen existieren kann. Daher bei einem Aufruf fsync() gibt es keine Möglichkeit, herauszufinden, welche Daten eines bestimmten Verzeichnisses ebenfalls auf die Festplatte geschrieben werden mĂŒssen (hier darĂŒber kann man ausfĂŒhrlicher lesen). Es scheint, dass das Dateisystem ext4 in der Lage ist automatisch einzusetzen fsync() zu Verzeichnissen, die die entsprechenden Dateien enthalten, aber in FĂ€llen mit anderen Dateisystemen könnte das anders sein.

Dieser Mechanismus kann in verschiedenen Dateisystemen unterschiedlich implementiert werden. Ich habe verwendet blktrace um herauszufinden, welche Festplattenoperationen in den Dateisystemen ext4 und XFS verwendet werden. Beide geben herkömmliche Schreibbefehle sowohl fĂŒr den Inhalt von Dateien als auch fĂŒr das Journal des Dateisystems aus, schreiben den Cache zurĂŒck und beenden die AusfĂŒhrung, indem sie eine FUA-Schreibung (Force Unit Access, direkte Datenschreibung auf die Festplatte, um den Cache herum) ins Journal ausfĂŒhren. Wahrscheinlich machen sie dies, um den Abschluss der Operation zu bestĂ€tigen. Auf Festplatten, die FUA nicht unterstĂŒtzen, fĂŒhrt dies zu zwei Cache-RĂŒckschreibungen. Meine Experimente zeigten, dass fdatasync() ein wenig schneller fsync(). Das Tool blktrace darauf hinweist, dass fdatasync() in der Regel weniger Daten auf die Festplatte schreibt (in ext4 fsync() schreibt 20 KiB, und fdatasync() — 16 KiB). Außerdem habe ich festgestellt, dass XFS etwas schneller ist als ext4. Und dabei konnte ich blktrace herausfinden, dass fdatasync() weniger Daten auf die Festplatte zurĂŒckschreibt (4 KiB in XFS).

Uneindeutige Situationen, die bei der Verwendung von fsync() auftreten

Ich kann mich an drei uneindeutige Situationen erinnern, die sich beziehen auf fsync(), mit denen ich in der Praxis konfrontiert war.

Der erste solche Fall trat 2008 auf. Damals „hing“ die BenutzeroberflĂ€che von Firefox 3, wenn eine große Anzahl von Dateien auf die Festplatte geschrieben wurde. Das Problem lag darin, dass bei der Implementierung der BenutzeroberflĂ€che eine SQLite-Datenbank verwendet wurde, um Informationen ĂŒber ihren Status zu speichern. Nach jeder Änderung, die in der BenutzeroberflĂ€che stattfand, wurde die Funktion fsync()aufgerufen, was gute Garantien fĂŒr die dauerhafte Speicherung der Daten bot. Im damals verwendeten Dateisystem ext3 war die Funktion fsync() Sie hat alle "schmutzigen" Seiten im System auf die Festplatte zurĂŒckgesetzt, nicht nur die, die mit der entsprechenden Datei zu tun hatten. Das bedeutete, dass ein Klick auf die SchaltflĂ€che in Firefox die Aufzeichnung von Megabytes an Daten auf die magnetische Festplatte initiieren konnte, was viele Sekunden in Anspruch nehmen konnte. Die Lösung des Problems bestand, wie ich verstand, darin, dieses die Arbeiten mit der Datenbank in asynchrone Hintergrundaufgaben zu verlagern. Das bedeutet, dass Firefox frĂŒher strengere Anforderungen an die StabilitĂ€t der Datenspeicherung hatte, als es wirklich nötig war, und die Besonderheiten des ext3-Dateisystems das Problem nur verschĂ€rften.

Das zweite MissverhĂ€ltnis trat im Jahr 2009 auf. Nach einem Systemausfall begegneten Benutzer des neuen ext4-Dateisystems dem Problem, dass viele neu erstellte Dateien eine null LĂ€nge hatten, wĂ€hrend dies beim Ă€lteren ext3-Dateisystem nicht der Fall war. Im vorherigen Absatz sprach ich darĂŒber, dass ext3 zu viele Daten auf die Festplatte zurĂŒcksetzte, was die Leistung stark verlangsamte fsync(). Um die Situation zu verbessern, werden beim ext4 nur die "schmutzigen" Seiten auf die Festplatte zurĂŒckgesetzt, die mit einer bestimmten Datei zu tun haben. Die Daten anderer Dateien bleiben viel lĂ€nger im Speicher als bei der Verwendung von ext3. Dies wurde zur Verbesserung der Leistung gemacht (standardmĂ€ĂŸig verweilen die Daten in diesem Zustand 30 Sekunden, dies kann mit Hilfe von dirty_expire_centisecs; hier weiteren Materialien dazu gefunden werden). Das bedeutet, dass ein großer Datenvolumen nach einem Ausfall unwiderruflich verloren gehen kann. Die Lösung dieses Problems liegt in der Verwendung von fsync() Anwendungen, die eine stabile Datenspeicherung gewĂ€hrleisten und diese so gut wie möglich vor den Folgen von AusfĂ€llen schĂŒtzen mĂŒssen. Die Funktion fsync() arbeitet bei Verwendung von ext4 wesentlich effektiver als bei ext3. Der Nachteil eines solchen Ansatzes besteht darin, dass seine Anwendung, wie zuvor, die AusfĂŒhrung bestimmter VorgĂ€nge, wie die Installation von Programmen, verlangsamt. Einzelheiten dazu finden Sie hier und hier.

Das dritte Problem, das fsync(), trat im Jahr 2018 auf. Im Rahmen des PostgreSQL-Projekts wurde festgestellt, dass, wenn die Funktion fsync() auf einen Fehler stĂ¶ĂŸt, sie die "schmutzigen" Seiten als "sauber" markiert. Infolgedessen fĂŒhrten die folgenden Aufrufe fsync() Mit solchen Seiten wird nichts gemacht. Daher bleiben modifizierte Seiten im Speicher und werden niemals auf die Festplatte geschrieben. Dies ist eine echte Katastrophe, da die Anwendung denken wird, dass bestimmte Daten auf die Festplatte geschrieben wurden, was jedoch nicht der Fall sein wird. Solche Fehler fsync() kommen selten vor, und die Anwendung kann in solchen Situationen kaum etwas unternehmen, um das Problem zu beheben. Heutzutage, wenn so etwas passiert, beendet PostgreSQL und andere Anwendungen die Arbeit unerwartet. Hier, im Material "Können Anwendungen von fsync-Fehlern wiederherstellen?", wird dieses Problem im Detail untersucht. Derzeit ist die beste Lösung fĂŒr dieses Problem die Verwendung von Direct I/O mit dem Flag O_SYNC oder mit dem Flag O_DSYNC. Bei diesem Ansatz meldet das System Fehler, die bei der DurchfĂŒhrung konkreter Schreiboperationen auftreten können, jedoch erfordert dieser Ansatz, dass die Anwendung die Puffer selbst verwaltet. Einzelheiten dazu finden Sie hier und hier.

Die Öffnung von Dateien mit den Flags O_SYNC und O_DSYNC

Kehren wir zurĂŒck zur Diskussion ĂŒber die Mechanismen von Linux, die eine zuverlĂ€ssige Datenspeicherung gewĂ€hrleisten. Insbesondere geht es um die Verwendung des Flags O_SYNC oder des Flags O_DSYNC beim Öffnen von Dateien mit dem Systemaufruf open(). Bei diesem Ansatz wird jede Schreiboperation so ausgefĂŒhrt, als ob nach jedem Befehl write() dem System entsprechende Befehle erteilt werden, fsync() und fdatasync(). In den Spezifikationen von POSIX. Dies wird als "Synchronisierte I/O-DateiintegritĂ€tsvollstĂ€ndigung" und "DatenintegritĂ€tsvollstĂ€ndigung" bezeichnet. Der Hauptvorteil dieses Ansatzes besteht darin, dass zur GewĂ€hrleistung der Datensicherheit nur ein Systemaufruf notwendig ist, nicht zwei (zum Beispiel – write() und fdatasync()). Der Hauptnachteil dieses Ansatzes ist, dass alle Schreiboperationen, die den entsprechenden Dateideskriptor verwenden, synchronisiert werden, was die Möglichkeiten zur Strukturierung des Anwendungscodes einschrĂ€nken kann.

Die Verwendung von Direct I/O mit dem Flag O_DIRECT

Systemaufruf open() unterstĂŒtzt das Flag O_DIRECT, das dazu dient, Eingabe- und Ausgabeoperationen direkt mit der Festplatte durchzufĂŒhren, indem der Cache des Betriebssystems umgangen wird. Dies bedeutet in vielen FĂ€llen, dass die von der Software ausgegebenen Schreibbefehle direkt in die Befehle ĂŒbersetzt werden, die mit der Festplatte arbeiten. Im Allgemeinen ist dieser Mechanismus jedoch kein Ersatz fĂŒr die Funktionen fsync() oder fdatasync(). Das Problem ist, dass die Festplatte selbst verschieben oder cachen entsprechende Befehle zur Datenspeicherung. Und noch schlimmer, in einigen speziellen FĂ€llen werden Ein- und Ausgabeoperationen durchgefĂŒhrt, wĂ€hrend das Flag verwendet wird O_DIRECT, ubertragen in traditionelle gepufferte Operationen. Das Problem lĂ€sst sich am einfachsten lösen, indem man beim Öffnen von Dateien auch das Flag verwendet O_DSYNC, was bedeutet, dass auf jede Schreiboperation ein Aufruf folgt fdatasync().

Es stellte sich heraus, dass im XFS-Dateisystem kĂŒrzlich ein "schneller Pfad" hinzugefĂŒgt wurde fĂŒr O_DIRECT|O_DSYNC-Datenspeicherung. Wenn ein Block unter Verwendung von O_DIRECT|O_DSYNCĂŒberschrieben wird, wird XFS anstelle des Cache-Flushs den Befehl FUA-Schreibung ausfĂŒhren, sofern das GerĂ€t dies unterstĂŒtzt. Ich habe das selbst getestet mit dem Dienstprogramm blktrace in Linux 5.4/Ubuntu 20.04. Dieser Ansatz sollte effizienter sein, da dabei die minimale Datenmenge auf die Festplatte geschrieben wird und zudem nur eine Operation angewendet wird und nicht zwei (Schreiben und Cache-Flush). Ich fand einen Hinweis auf Patch den Kernel aus dem Jahr 2018, in dem dieser Mechanismus implementiert ist. Dort gibt es eine Diskussion ĂŒber die Anwendung dieser Optimierung in anderen Dateisystemen, aber soweit ich weiß, ist XFS bisher das einzige Dateisystem, das dies unterstĂŒtzt.

Die Funktion sync_file_range()

In Linux gibt es einen Systemaufruf sync_file_range(), der es ermöglicht, nur einen Teil einer Datei auf die Festplatte zu schreiben, anstatt die gesamte Datei. Dieser Aufruf initiiert einen asynchronen Daten-Flush und wartet nicht auf dessen Abschluss. Doch in der Dokumentation zu sync_file_range() wird gesagt, dass dieser Befehl "sehr gefĂ€hrlich" ist. Die Nutzung wird nicht empfohlen. Die Besonderheiten und Gefahren sync_file_range() sind sehr gut beschrieben in das dem Material. Insbesondere scheint es, dass dieser Aufruf RocksDB verwendet, um zu steuern, wann der Kernel "verunreinigte" Daten auf die Festplatte schreibt. Dabei wird auch fdatasync(). In Eine neue SQLite-Version mit dem Patch ist bisher RocksDB gibt es interessante Kommentare zu diesem Thema. Beispielsweise scheint der Aufruf sync_file_range() bei der Verwendung von ZFS nicht zu einem Flush der Daten auf die Festplatte zu fĂŒhren. ErfahrungsgemĂ€ĂŸ vermute ich, dass Code, der selten verwendet wird, möglicherweise Fehler enthĂ€lt. Daher wĂŒrde ich empfehlen, diesen Systemaufruf nur im Ă€ußersten Notfall zu verwenden.

Systemaufrufe, die helfen, die DatenintegritÀt zu gewÀhrleisten

Ich komme zu dem Schluss, dass zur DurchfĂŒhrung von Ein-/Ausgabeoperationen, die eine nachhaltige Datenspeicherung gewĂ€hrleisten, drei AnsĂ€tze verwendet werden können. Alle erfordern den Aufruf der Funktion fsync() fĂŒr das Verzeichnis, in dem die Datei erstellt wurde. Hier sind die AnsĂ€tze:

  1. Aufruf der Funktion fdatasync() oder fsync() nach der Funktion write() (es ist besser, zu verwenden fdatasync()).
  2. Arbeiten mit einem Dateideskriptor, der mit dem Flag geöffnet wurde O_DSYNC oder O_SYNC (besser – mit dem Flag O_DSYNC).
  3. Verwendung des Befehls pwritev2() mit dem Flag RWF_DSYNC oder RWF_SYNC (vorzugsweise – mit dem Flag RWF_DSYNC).

Hinweise zur Leistung

Ich habe keine grĂŒndlichen Leistungsmessungen der verschiedenen von mir untersuchten Mechanismen durchgefĂŒhrt. Die von mir festgestellten Geschwindigkeitsunterschiede sind ziemlich gering. Das bedeutet, dass ich mich irren kann und dass das gleiche unter anderen Bedingungen andere Ergebnisse zeigen kann. ZunĂ€chst werde ich darĂŒber berichten, was sich stĂ€rker auf die Leistung auswirkt, und dann, was sich weniger auf die Leistung auswirkt.

  1. Das Überschreiben von Dateidaten ist schneller als das AnhĂ€ngen von Daten an die Datei (der Leistungsgewinn kann 2-100% betragen). Das AnhĂ€ngen von Daten an die Datei erfordert zusĂ€tzliche Änderungen an den Metadaten der Datei, selbst nach dem Systemaufruf fallocate(), aber das Ausmaß dieses Effekts kann variieren. Ich empfehle, um die beste Leistung zu gewĂ€hrleisten, den Aufruf fallocate() zur Voraballokation des benötigten Raums. Danach muss dieser Raum explizit mit Nullen gefĂŒllt und der Aufruf fsync()getĂ€tigt werden. Dadurch werden die entsprechenden Blöcke im Dateisystem als "allokiert" und nicht als "nicht allokiert" markiert. Dies fĂŒhrt zu einer geringfĂŒgigen (circa 2%) Verbesserung der Leistung. Zudem kann bei einigen Festplatten die erste Zugriffsoperation auf einen Block langsamer sein als bei anderen. Das bedeutet, dass das FĂŒllen des Raums mit Nullen zu einer erheblichen (circa 100%) Verbesserung der Leistung fĂŒhren kann. Dies kann insbesondere bei den Festplatten AWS EBS (das sind inoffizielle Daten, die ich nicht bestĂ€tigen konnte). Das Gleiche gilt fĂŒr die Speicher GCP Persistent Disk (und das ist bereits offizielle Information, die durch Tests bestĂ€tigt wurde). Andere Spezialisten haben Ă€hnliche Beobachtungen, die verschiedene Festplatten betreffen.
  2. Je weniger Systemaufrufe – desto höher die Leistung (der Gewinn kann etwa 5% betragen). Es scheint, dass der Aufruf open() mit dem Flag O_DSYNC oder der Aufruf pwritev2() mit dem Flag RWF_SYNC schneller ist als der Aufruf fdatasync()Ich vermute, dass der Grund darin liegt, dass bei solch einem Ansatz die Anzahl der Systemaufrufe zur Lösung derselben Aufgabe reduziert werden kann (ein Aufruf statt zwei). Der Leistungsunterschied ist jedoch sehr gering, sodass Sie dies vernachlĂ€ssigen und in der Anwendung verwenden können, was die Logik nicht zusĂ€tzlich verkompliziert.

Wenn Sie sich fĂŒr das Thema robustes Datenmanagement interessieren – hier sind einige nĂŒtzliche Materialien:

  • I/O-Zugriffsmethoden – eine Übersicht der Grundlagen der Eingabe-/Ausgabemechanismen.
  • Sicherstellen, dass Daten die Festplatte erreichen – eine ErzĂ€hlung darĂŒber, was mit Daten auf dem Weg von der Anwendung zur Festplatte passiert.
  • Wann sollten Sie das enthaltene Verzeichnis fsync? – eine Antwort auf die Frage, wann fsync() fĂŒr Verzeichnisse angewendet werden muss. Kurz gesagt, sollte dies beim Erstellen einer neuen Datei geschehen, und der Grund fĂŒr diese Empfehlung ist, dass in Linux viele Verweise auf dieselbe Datei existieren können.
  • SQL Server auf Linux: FUA-Internas – hier wird beschrieben, wie robuster Datenmanagement im SQL Server auf der Linux-Plattform umgesetzt wird. Es gibt einige interessante Vergleiche zwischen den Systemaufrufen von Windows und Linux. Ich bin mir ziemlich sicher, dass ich gerade durch dieses Material von der FUA-Optimierung von XFS erfahren habe.

Haben Sie Daten verloren, die Sie fĂŒr sicher auf der Festplatte gespeichert hielten?

Nachhaltige Datenspeicherung und Linux-Datei-APIs

Nachhaltige Datenspeicherung und Linux-Datei-APIs

Quelle: habr.com

ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server đŸ”„ ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster