Ich habe die Zuverlässigkeit der Datenspeicherung in Cloud-Systemen untersucht und wollte mich selbst prüfen, um sicherzustellen, dass ich die Grundlagen verstehe. Ich , um herauszufinden, welche Garantien in Bezug auf die zuverlässige Datenspeicherung (also die Garantien, dass Daten nach einem Systemausfall verfügbar sind) uns NVMe-Laufwerke bieten. Meine wichtigsten Erkenntnisse waren: Man muss Daten als beschädigt betrachten, sobald der Befehl zur Datenaufnahme gegeben wird, und bis der Schreibvorgang auf dem Speichermedium abgeschlossen ist. In den meisten Datensatzprogrammen werden allerdings system calls ganz ungeniert verwendet.
In diesem Material untersuche ich die Mechanismen der zuverlässigen Datenspeicherung, die die Dateisystem-APIs von Linux bereitstellen. Es scheint, als sollte das einfach sein: Das Programm ruft den Befehl write(),, und nachdem dieser Befehl ausgeführt wurde, sollten die Daten sicher auf der Festplatte gespeichert sein. Aber write(), kopiert nur die Anwendungsdaten in den Kernel-Cache, der im Arbeitsspeicher liegt. Um das System dazu zu zwingen, die Daten auf die Festplatte zu schreiben, müssen einige zusätzliche Mechanismen verwendet werden.
Insgesamt handelt es sich bei diesem Material um eine Sammlung von Notizen zu dem, was ich über das für mich interessante Thema gelernt habe. Um das Wichtigste kurz zusammenzufassen: Für eine nachhaltige Datenspeicherung sollte man den Befehl verwenden fdatasync() oder Dateien mit dem Flag O_DSYNC. Wenn Sie mehr über die Details erfahren möchten, die mit den Daten auf dem Weg vom Programmcode zur Festplatte verbunden sind, werfen Sie einen Blick auf den Artikel.
Eigenschaften der Funktion write()
Systemaufruf write(), wird im Standard als Versuch definiert, Daten in einen Dateideskriptor zu schreiben. Nach erfolgreichem Abschluss write(), sollten Leseoperationen genau die Bytes zurückgeben, die zuvor geschrieben wurden, selbst wenn auf die Daten von anderen Prozessen oder Threads zugegriffen wird ( entsprechender Abschnitt des POSIX-Standards). , im Abschnitt, der sich mit der Interaktion von Threads und herkömmlichen Dateioperationen befasst, gibt es einen Hinweis, der besagt, dass, wenn jeder der beiden Threads diese Funktionen aufruft, jeder Aufruf entweder alle spezifizierten Nebenwirkungen des anderen Aufrufs sehen sollte oder überhaupt keine Nebenwirkungen. Dies führt zu der Schlussfolgerung, dass alle Datei-Eingabe/Ausgabe-Operationen die Ressource, mit der sie arbeiten, sperren sollten.
Bedeutet das, dass die Operation write(), atomar ist? Technisch gesehen – ja. Lesevorgänge sollten entweder alles oder nichts von dem zurückgeben, was durch write(),geschrieben wurde. Aber die Operation write(),, gemäß dem Standard, muss nicht unbedingt alles, was ihr angeboten wurde, aufgezeichnet werden. Es ist ihr erlaubt, nur einen Teil der Daten zu speichern. Zum Beispiel können wir zwei Streams haben, die jeweils 1024 Byte zu einer Datei hinzufügen, die durch denselben Descriptor beschrieben wird. Aus Sicht des Standards wäre das akzeptable Ergebnis, wenn jede der Schreiboperationen nur ein Byte an die Datei anhängen kann. Diese Operationen bleiben atomar, aber nachdem sie abgeschlossen sind, werden die von ihnen in die Datei geschriebenen Daten vermischt sein. 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 aufzurufen. 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 glaube, dass der Bedarf an diesen Metadaten selten auftritt, daher können Sie die Funktion fdatasync()In zum fdatasync() Es wird gesagt, dass bei der Ausführung dieser Funktion eine Menge an Metadaten auf die Festplatte gespeichert wird, die "notwendig ist, um die folgenden Datenlesevorgänge korrekt durchzuführen". Und das ist genau das, was die meisten Anwendungen betrifft.
Ein Problem, das hier auftreten kann, besteht darin, dass diese Mechanismen nicht garantieren, dass die Datei nach einem möglichen Ausfall gefunden werden kann. Insbesondere muss beim Erstellen einer neuen Datei fsync() auf das Verzeichnis, das sie enthält, aufgerufen werden. Andernfalls kann es nach einem Ausfall passieren, dass diese Datei nicht existiert. Der Grund dafür ist, dass in UNIX, aufgrund von harten Verlinkungen, eine Datei in mehreren Verzeichnissen existieren kann. Daher gibt es beim Aufruf von fsync() keinen Weg zu erfahren, dass die Daten eines bestimmten Verzeichnisses ebenfalls auf die Festplatte geschrieben werden müssen ( darüber kann man näher lesen). Anscheinend ist das Dateisystem ext4 fähig, anzuwenden fsync() zu Verzeichnissen zu verlinken, die die entsprechenden Dateien enthalten, aber bei anderen Dateisystemen könnte dies anders sein.
Dieser Mechanismus kann in verschiedenen Dateisystemen unterschiedlich implementiert sein. Ich habe verwendet um herauszufinden, welche Diskoperationen in den Dateisystemen ext4 und XFS verwendet werden. Beide führen normale Schreibbefehle auf die Festplatte sowohl für den Dateiinhalte als auch für das Dateisystemprotokoll aus, leeren den Cache und beenden den Vorgang, indem sie eine FUA-Aufzeichnung (Force Unit Access, das direkte Schreiben von Daten auf die Festplatte, um den Cache zu umgehen) im Protokoll durchführen. Wahrscheinlich machen sie das, um den Abschluss der Operation zu bestätigen. Bei Festplatten, die FUA nicht unterstützen, führt dies zu zwei Cache-Leerungen. Meine Experimente haben gezeigt, dass fdatasync() etwas schneller fsync(). Das Tool blktrace darauf hinweist, dass fdatasync() normalerweise weniger Daten auf die Festplatte schreibt (in ext4 fsync() werden 20 KiB geschrieben, während fdatasync() 16 KiB). Außerdem habe ich festgestellt, dass XFS etwas schneller ist als ext4. Dabei konnte ich blktrace herausfinden, dass fdatasync() weniger Daten auf die Festplatte leert (4 KiB in XFS).
Ambivalente Situationen, die bei der Verwendung von fsync() auftreten
Ich kann mich an drei ambivalente Situationen erinnern, die ich in der Praxis erlebt habe. fsync(), mit denen ich konfrontiert wurde.
Der erste solche Fall trat 2008 auf. Damals kam es bei der Benutzeroberfläche von Firefox 3 zu Verzögerungen, wenn eine große Zahl von Dateien auf die Festplatte geschrieben wurde. Das Problem lag darin, dass bei der Implementierung der Benutzeroberfläche zur Speicherung ihrer Zustandsinformationen eine SQLite-Datenbank verwendet wurde. Nach jeder Änderung in der Benutzeroberfläche wurde eine Funktion aufgerufen fsync(), die gute Garantien für die zuverlässige Speicherung der Daten gab. Im damals verwendeten Dateisystem ext3 sorgte die Funktion fsync() dafür, dass alle "schmutzigen" Seiten im System auf die Festplatte geschrieben wurden, und nicht nur die Seiten, die zu der betreffenden Datei gehörten. Das bedeutete, dass ein Klick auf eine Schaltfläche in Firefox das Schreiben von Megabyte an Daten auf die magnetische Festplatte auslösen konnte, was viele Sekunden in Anspruch nehmen konnte. Die Lösung des Problems, so wie ich das aus dem Material verstanden habe, bestand darin, die Arbeit mit der Datenbank in asynchrone Hintergrundaufgaben zu verlagern. Das bedeutet, dass früher in Firefox strengere Anforderungen an die zuverlässige Speicherung der Daten umgesetzt wurden, als tatsächlich nötig gewesen wären, und die Eigenschaften des Dateisystems ext3 dieses Problem nur verschärften.
Das zweite Problem trat im Jahr 2009 auf. Nach einem Systemausfall hatten Benutzer des neuen Dateisystems ext4 das Problem, dass viele neu erstellte Dateien eine Nulllänge hatten, während bei dem älteren Dateisystem ext3 dies nicht der Fall war. Im vorherigen Absatz erwähnte ich, dass ext3 zu viele Daten auf die Festplatte schrieb, was die Leistung erheblich verlangsamte. fsync()Um die Situation zu verbessern, schreibt ext4 nur die „schmutzigen“ Seiten auf die Festplatte, die sich auf eine bestimmte Datei beziehen. Die Daten anderer Dateien bleiben viel länger im Speicher als dies bei ext3 der Fall war. Dies wurde zur Leistungssteigerung eingeführt (standardmäßig bleiben die Daten 30 Sekunden in diesem Zustand; dies kann mit Hilfe von ; zusätzlichen Materialien darüber gefunden werden). Dies bedeutet, dass ein großer Datenumfang nach einem Ausfall unwiderruflich verloren gehen kann. Die Lösung dieses Problems liegt im Einsatz von fsync() Anwendungen, die eine dauerhafte Datenspeicherung sicherstellen und sie bestmöglich vor den Folgen von Ausfällen schützen müssen. Die Funktion fsync() arbeitet bei der Anwendung von ext4 deutlich effizienter als bei ext3. Der Nachteil dieses Ansatzes besteht darin, dass seine Anwendung, wie zuvor, die Ausführung bestimmter Operationen, wie die Installation von Programmen, verlangsamt. Weitere Einzelheiten dazu finden Sie и .
Das dritte Problem, das sich bezieht auf fsync(), trat im Jahr 2018 auf. Damals wurde im Rahmen des PostgreSQL-Projekts herausgefunden, dass wenn die Funktion fsync() auf einen Fehler stößt, sie »schmutzige« Seiten als »sauber« kennzeichnet. Infolgedessen führen die folgenden Aufrufe fsync() nichts mit solchen Seiten durch. Aufgrund dessen verbleiben modifizierte Seiten im Speicher und werden nie auf die Festplatte geschrieben. Dies ist eine echte Katastrophe, da die Anwendung glauben wird, dass bestimmte Daten auf die Festplatte geschrieben wurden, in Wirklichkeit aber nicht. Solche Fehler fsync() sind selten, die Anwendung kann in solchen Situationen fast nichts tun, um das Problem zu bekämpfen. Heutzutage, wenn dies passiert, beenden PostgreSQL und andere Anwendungen unerwartet. , im Artikel „Can Applications Recover from fsync Failures?“, wird dieses Problem ausführlich behandelt. Derzeit besteht die beste Lösung für dieses Problem in der Verwendung von Direct I/O mit dem Flag O_SYNC oder mit dem Flag O_DSYNC. Bei diesem Ansatz wird das System über Fehler informieren, die bei der Durchführung bestimmter Datenaufzeichnungsoperationen auftreten können, erfordert jedoch, dass die Anwendung die Puffer selbst verwaltet. Weitere Informationen dazu finden Sie и .
Öffnen von Dateien mit den Flags O_SYNC und O_DSYNC
Kehren wir zur Diskussion über die Mechanismen von Linux zurück, die eine zuverlässige Datenspeicherung gewährleisten. Insbesondere beziehen wir uns auf die Verwendung des Flags O_SYNC oder des Flags O_DSYNC beim Öffnen von Dateien mit einem Systemaufruf . Bei diesem Ansatz wird jede Datenaufzeichnungsoperation so ausgeführt, als ob nach jedem Befehl write(), dem System entsprechend Befehle erteilt werden fsync() и fdatasync()In wird dies als „Synchronized I/O File Integrity Completion“ und „Data Integrity Completion“ bezeichnet. Der Hauptvorteil dieses Ansatzes besteht darin, dass für die Gewährleistung der Datenintegrität lediglich ein Systemaufruf erforderlich ist, nicht zwei (zum Beispiel — write(), и fdatasync()). Der Hauptnachteil dieses Ansatzes besteht darin, dass alle Schreiboperationen, die den entsprechenden Dateideskriptor verwenden, synchronisiert werden, was die Möglichkeiten zur Strukturierung des Anwendungscodes einschränken kann.
Verwendung von Direct I/O mit dem O_DIRECT-Flag
Systemaufruf open(), unterstützt das Flag O_DIRECT, das dazu dient, Eingabe-/Ausgabeoperationen direkt mit der Festplatte durchzuführen, indem der Cache des Betriebssystems umgangen wird. Das bedeutet in vielen Fällen, dass die von der Software ausgegebenen Schreibbefehle direkt in Befehle für die Arbeit mit der Festplatte umgesetzt werden. Generally, this mechanism is not a replacement for the functions fsync() или fdatasync(). Das Problem ist, dass die Festplatte selbst verzögern oder cachen kann. Und was noch schlimmer ist, in einigen speziellen Fällen werden die Eingabe-/Ausgabeoperationen, die unter Verwendung des Flags O_DIRECT, Das Problem lässt sich am einfachsten lösen, indem man beim Öffnen von Dateien auch das Flag O_DSYNCverwendet, was bedeutet, dass jeder Schreiboperation ein Aufruf folgt fdatasync().
Es stellte sich heraus, dass in das XFS-Dateisystem kürzlich ein 'schneller Pfad' für O_DIRECT|O_DSYNCzum Schreiben von Daten hinzugefügt wurde. Wenn ein Block mit Verwendung von O_DIRECT|O_DSYNC, dann wird XFS anstelle des Cache-Flushs den FUA-Schreibbefehl ausführen, sofern das Gerät dies unterstützt. Das habe ich mit dem Tool überprüft blktrace unter Linux 5.4/Ubuntu 20.04. Dieser Ansatz sollte effizienter sein, da bei seiner Verwendung eine minimale Menge an Daten auf die Festplatte geschrieben wird und nur eine Operation verwendet wird, nicht zwei (Schreiben und Cache-Flush). Ich fand einen Link zu einem Kernel aus dem Jahr 2018, in dem dieser Mechanismus implementiert ist. Dort wird diskutiert, ob diese Optimierung auch in anderen Dateisystemen angewendet werden kann, aber soweit ich weiß, ist XFS derzeit 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 eine asynchrone Datenübertragung und wartet nicht auf deren Abschluss. Aber in der Dokumentation zu sync_file_range() wird gesagt, dass dieser Befehl "sehr gefährlich" ist. Es wird nicht empfohlen, ihn zu verwenden. Die Besonderheiten und Gefahren sync_file_range() sind sehr gut beschrieben in In dem Material. Insbesondere scheint dieser Aufruf RocksDB zu verwenden, um zu steuern, wann der Kernel "schmutzige" Daten auf die Festplatte schreibt. Dabei wird jedoch auch für die dauerhafte Speicherung von Daten fdatasync()In Bei RocksDB gibt es interessante Kommentare zu diesem Thema. Zum Beispiel scheint es, dass der Aufruf sync_file_range() bei der Verwendung von ZFS nicht zu einem Schreiben von Daten auf die Festplatte führt. Die Erfahrung zeigt mir, dass seltener genutzter Code möglicherweise Fehler enthält. Daher würde ich raten, diesen Systemaufruf nur im äußersten Notfall zu verwenden.
Systemaufrufe, die die dauerhafte Speicherung von Daten unterstützen
Ich bin zu dem Schluss gekommen, dass für die Durchführung von Ein-/Ausgabe-Operationen, die die dauerhafte Speicherung von Daten 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 diese Ansätze:
- Der Aufruf der Funktion
fdatasync()илиfsync()nach der Funktionwrite(),(es ist besser,fdatasync()). - Mit einem Dateideskriptor zu arbeiten, der mit der Flagge
O_DSYNCилиO_SYNC(besser — mit der FlaggeO_DSYNC). - Verwendung des Befehls
pwritev2()das FlagRWF_DSYNCилиRWF_SYNC(bevorzugt — mit der FlaggeRWF_DSYNC).
Leistungsnotizen
Ich habe die Leistung der verschiedenen von mir untersuchten Mechanismen nicht sorgfältig gemessen. Die Unterschiede in der Geschwindigkeit, die mir aufgefallen sind, sind recht gering. Das bedeutet, dass ich mich irren könnte und dass unter anderen Bedingungen dasselbe zu anderen Ergebnissen führen kann. Zuerst werde ich darüber sprechen, was einen stärkeren Einfluss auf die Leistung hat, und danach, was weniger Einfluss auf die Leistung hat.
- Das Überschreiben von Daten in einer Datei ist schneller als das Anhängen von Daten an eine Datei (der Leistungsgewinn kann 2-100% betragen). Das Anhängen von Daten an eine 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, einen Aufruf zu tätigenfallocate()zum vorherigen Reservieren des benötigten Speicherplatzes. Danach sollte dieser Speicherplatz ausdrücklich mit Nullen gefüllt und aufgerufen werden.fsync(). Dadurch werden die entsprechenden Blöcke im Dateisystem als „ausgewählt“ und nicht als „nicht ausgewählt“ markiert. Dies ermöglicht eine geringe (etwa 2%) Leistungsverbesserung. Außerdem kann bei einigen Laufwerken die erste Zugriffsoperation auf einen Block langsamer sein als bei anderen. Das bedeutet, dass das Auffüllen des Speichers mit Nullen eine signifikante (von etwa 100%) Leistungsverbesserung zur Folge haben kann. Insbesondere kann dies bei Laufwerken auftreten. (dies sind inoffizielle Daten, die ich nicht bestätigen konnte). Dasselbe gilt für die Speicherlösungen. (dies ist bereits offizielle Information, die durch Tests bestätigt wurde). Andere Experten haben ähnliche , die sich auf verschiedene Laufwerke beziehen. - Je weniger Systemaufrufe – desto höher die Leistung (die Verbesserung kann etwa 5% betragen). Es scheint, dass der Aufruf
open(),das FlagO_DSYNCoder der Aufrufpwritev2()das FlagRWF_SYNCschneller ist als der Aufruffdatasync(). Ich vermute, dass es daran liegt, dass bei einem solchen Ansatz die Rolle spielt, dass für die Lösung derselben Aufgabe weniger Systemaufrufe erforderlich sind (ein Aufruf anstelle von zwei). Aber der Leistungsunterschied ist sehr gering, sodass Sie ihn getrost ignorieren und in der Anwendung verwenden können, was die Logik nicht komplizierter macht.
Wenn Sie sich für das Thema der nachhaltigen Datenspeicherung interessieren – hier sind einige nützliche Materialien:
- – eine Übersicht über die Grundlagen der Eingabe-/Ausgabemechanismen.
- – ein Bericht darüber, was mit den Daten auf dem Weg von der Anwendung zur Festplatte passiert.
- – eine Antwort auf die Frage, wann es notwendig ist,
fsync()für Verzeichnisse anzuwenden. Zusammengefasst sollte dies beim Erstellen einer neuen Datei erfolgen, und der Grund für diese Empfehlung ist, dass es in Linux viele Verweise auf dieselbe Datei geben kann. - — hier wird beschrieben, wie zuverlässige Datenspeicherung in SQL Server auf der Linux-Plattform implementiert ist. Es gibt einige interessante Vergleiche zwischen den Systemaufrufen von Windows und Linux. Ich bin mir fast sicher, dass ich durch dieses Material von der FUA-Optimierung von XFS erfahren habe.
Haben Sie Daten verloren, von denen Sie dachten, sie seien sicher auf der Festplatte gespeichert?
Quelle: habr.com
