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 , 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.
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 Artikel an.
Besonderheiten der Verwendung der Funktion write()
Systemaufruf write() wird im Standard 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 ( entsprechender Abschnitt des POSIX-Standards). , 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. 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 . 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 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 ( darüber kann man ausführlicher lesen). Es scheint, dass das Dateisystem ext4 in der Lage ist 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 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, 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 ; 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 und .
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. , 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 und .
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 . 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 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 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, 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 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 , 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 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 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:
- Aufruf der Funktion
fdatasync()oderfsync()nach der Funktionwrite()(es ist besser, zu verwendenfdatasync()). - Arbeiten mit einem Dateideskriptor, der mit dem Flag geöffnet wurde
O_DSYNCoderO_SYNC(besser – mit dem FlagO_DSYNC). - Verwendung des Befehls
pwritev2()mit dem FlagRWF_DSYNCoderRWF_SYNC(vorzugsweise – mit dem FlagRWF_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.
- 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 Aufruffallocate()zur Voraballokation des benötigten Raums. Danach muss dieser Raum explizit mit Nullen gefüllt und der Aufruffsync()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 (das sind inoffizielle Daten, die ich nicht bestätigen konnte). Das Gleiche gilt für die Speicher (und das ist bereits offizielle Information, die durch Tests bestätigt wurde). Andere Spezialisten haben ähnliche , die verschiedene Festplatten betreffen. - Je weniger Systemaufrufe – desto höher die Leistung (der Gewinn kann etwa 5% betragen). Es scheint, dass der Aufruf
open()mit dem FlagO_DSYNCoder der Aufrufpwritev2()mit dem FlagRWF_SYNCschneller ist als der Aufruffdatasync()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:
- – eine Übersicht der Grundlagen der Eingabe-/Ausgabemechanismen.
- – eine Erzählung darüber, was mit Daten auf dem Weg von der Anwendung zur Festplatte passiert.
- – 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. - – 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?
Quelle: habr.com
