Langlebige Datenspeicherung und Linux-Datei-APIs

Als ich die Stabilität der Datenspeicherung in Cloud-Systemen erforschte, beschloss ich, mich selbst zu testen, um sicherzustellen, dass ich die grundlegenden Dinge verstehe. ICH Ich begann mit dem Lesen der NVMe-Spezifikation Um zu verstehen, welche Garantien hinsichtlich der Datenpersistenz (d. h. Garantien, dass Daten nach einem Systemausfall verfügbar sind) uns NMVe-Festplatten geben. Ich habe die folgenden Hauptschlussfolgerungen gezogen: Sie müssen die beschädigten Daten ab dem Moment berücksichtigen, in dem der Datenschreibbefehl gegeben wird, und bis zu dem Moment, in dem sie auf das Speichermedium geschrieben werden. In den meisten Programmen werden Systemaufrufe jedoch recht sicher zum Schreiben von Daten verwendet.

In diesem Artikel untersuche ich die Persistenzmechanismen, die von den Linux-Datei-APIs bereitgestellt werden. Hier scheint alles einfach zu sein: Das Programm ruft den Befehl auf write(), und nachdem die Ausführung dieses Befehls abgeschlossen ist, werden die Daten sicher auf der Festplatte gespeichert. Aber write() Kopiert Anwendungsdaten nur in den Kernel-Cache im RAM. Um das System zu zwingen, Daten auf die Festplatte zu schreiben, müssen einige zusätzliche Mechanismen verwendet werden.

Langlebige Datenspeicherung und Linux-Datei-APIs

Im Allgemeinen handelt es sich bei diesem Material um eine Reihe von Notizen zu dem, was ich zu einem für mich interessanten Thema gelernt habe. Wenn wir ganz kurz auf das Wichtigste eingehen, stellt sich heraus, dass Sie den Befehl verwenden müssen, um eine nachhaltige Datenspeicherung zu organisieren fdatasync() oder Dateien mit Flag öffnen O_DSYNC. Wenn Sie mehr darüber erfahren möchten, was mit Daten auf dem Weg vom Code zur Festplatte passiert, werfen Sie einen Blick auf diese Artikel.

Merkmale der Verwendung der Funktion write()

Systemaufruf write() im Standard definiert IEEE POSIX als Versuch, Daten in einen Dateideskriptor zu schreiben. Nach erfolgreichem Abschluss der Arbeiten write() Datenlesevorgänge müssen genau die Bytes zurückgeben, die zuvor geschrieben wurden, auch wenn auf die Daten von anderen Prozessen oder Threads aus zugegriffen wird (hier entsprechenden Abschnitt des POSIX-Standards). HierIm Abschnitt über die Interaktion von Threads mit normalen Dateioperationen gibt es einen Hinweis, der besagt, dass, wenn jeweils zwei Threads diese Funktionen aufrufen, jeder Aufruf entweder alle angegebenen Konsequenzen sehen muss, zu denen die Ausführung des anderen Aufrufs führt, oder Ich sehe überhaupt keine Konsequenzen. Dies führt zu der Schlussfolgerung, dass alle Datei-E/A-Vorgänge eine Sperre für die Ressource enthalten müssen, an der gearbeitet wird.

Bedeutet dies, dass die Operation write() ist atomar? Aus technischer Sicht ja. Datenlesevorgänge müssen entweder alles oder nichts von dem zurückgeben, womit geschrieben wurde write(). Aber die Operation write(), gemäß dem Standard, muss nicht enden, nachdem sie alles aufgeschrieben hat, was sie aufschreiben sollte. Es ist erlaubt, nur einen Teil der Daten zu schreiben. Beispielsweise könnten zwei Streams vorhanden sein, die jeweils 1024 Bytes an eine Datei anhängen, die durch denselben Dateideskriptor beschrieben wird. Aus Sicht des Standards ist das Ergebnis akzeptabel, wenn jeder der Schreibvorgänge nur ein Byte an die Datei anhängen kann. Diese Vorgänge bleiben atomar, aber nach Abschluss werden die Daten, die sie in die Datei schreiben, durcheinander gebracht. Hier Sehr interessante Diskussion zu diesem Thema auf Stack Overflow.

Funktionen fsync() und fdatasync()

Der einfachste Weg, Daten auf die Festplatte zu übertragen, besteht darin, die Funktion aufzurufen fsync(). Diese Funktion fordert das Betriebssystem auf, alle geänderten Blöcke vom Cache auf die Festplatte zu verschieben. Dazu gehören alle Metadaten der Datei (Zugriffszeit, Dateiänderungszeit usw.). Ich glaube, dass diese Metadaten selten benötigt werden. Wenn Sie also wissen, dass sie für Sie nicht wichtig sind, können Sie die Funktion verwenden fdatasync(). In Hilfe auf fdatasync() Darin heißt es, dass während der Ausführung dieser Funktion eine solche Menge an Metadaten auf der Festplatte gespeichert wird, die „für die korrekte Ausführung der folgenden Datenlesevorgänge erforderlich ist“. Und genau darum geht es den meisten Anwendungen.

Ein Problem, das hierbei auftreten kann, besteht darin, dass diese Mechanismen nicht garantieren, dass die Datei nach einem möglichen Ausfall gefunden werden kann. Insbesondere wenn eine neue Datei erstellt wird, sollte man anrufen fsync() für das Verzeichnis, das es enthält. Andernfalls kann es nach einem Absturz dazu kommen, dass diese Datei nicht existiert. Der Grund dafür ist, dass unter UNIX durch die Verwendung von Hardlinks eine Datei in mehreren Verzeichnissen existieren kann. Deshalb beim Anruf fsync() Es gibt für eine Datei keine Möglichkeit zu wissen, welche Verzeichnisdaten auch auf die Festplatte geschrieben werden sollen (hier Sie können mehr darüber lesen). Es sieht so aus, als ob das ext4-Dateisystem dazu in der Lage ist automatisch применять fsync() in Verzeichnisse, die die entsprechenden Dateien enthalten, bei anderen Dateisystemen ist dies jedoch möglicherweise nicht der Fall.

Dieser Mechanismus kann in verschiedenen Dateisystemen unterschiedlich implementiert werden. ich benutzte blktspur um zu erfahren, welche Festplattenoperationen in ext4- und XFS-Dateisystemen verwendet werden. Beide geben die üblichen Schreibbefehle auf die Festplatte sowohl für den Inhalt der Dateien als auch für das Dateisystemjournal aus, leeren den Cache und beenden den Vorgang, indem sie einen FUA-Schreibvorgang (Force Unit Access, Daten direkt auf die Festplatte schreiben, den Cache umgehen) in das Journal ausführen. Sie tun dies wahrscheinlich nur, um die Tatsache der Transaktion zu bestätigen. Auf Laufwerken, die FUA nicht unterstützen, führt dies zu zwei Cache-Leerungen. Das haben meine Experimente gezeigt fdatasync() etwas schneller fsync(). Dienstprogramm blktrace weist darauf hin, dass fdatasync() schreibt normalerweise weniger Daten auf die Festplatte (in ext4 fsync() schreibt 20 KiB, und fdatasync() - 16 KiB). Außerdem habe ich herausgefunden, dass XFS etwas schneller ist als ext4. Und hier mit der Hilfe blktrace konnte das herausfinden fdatasync() schreibt weniger Daten auf die Festplatte (4 KiB in XFS).

Mehrdeutige Situationen bei der Verwendung von fsync()

Ich kann mir drei unklare Situationen vorstellen fsync()was mir in der Praxis aufgefallen ist.

Der erste derartige Vorfall ereignete sich im Jahr 2008. Zu diesem Zeitpunkt „frierte“ die Firefox-3-Oberfläche ein, wenn viele Dateien auf die Festplatte geschrieben wurden. Das Problem bestand darin, dass bei der Implementierung der Schnittstelle eine SQLite-Datenbank zum Speichern von Informationen über ihren Status verwendet wurde. Nach jeder Änderung an der Schnittstelle wurde die Funktion aufgerufen fsync(), was gute Garantien für eine stabile Datenspeicherung gab. Im damals verwendeten ext3-Dateisystem ist die Funktion fsync() Alle „schmutzigen“ Seiten im System wurden auf die Festplatte geleert, und nicht nur diejenigen, die mit der entsprechenden Datei in Zusammenhang standen. Das bedeutete, dass durch Klicken auf eine Schaltfläche in Firefox Megabytes an Daten auf eine Magnetplatte geschrieben werden konnten, was viele Sekunden dauern konnte. Die Lösung des Problems, soweit ich es verstanden habe sie Material bestand darin, die Arbeit mit der Datenbank auf asynchrone Hintergrundaufgaben zu verlagern. Das bedeutet, dass Firefox früher strengere Anforderungen an die Speicherpersistenz implementiert hat, als eigentlich nötig war, und die Funktionen des ext3-Dateisystems haben dieses Problem nur noch verschärft.

Das zweite Problem trat 2009 auf. Dann, nach einem Systemabsturz, stellten Benutzer des neuen ext4-Dateisystems fest, dass viele neu erstellte Dateien die Länge Null hatten, was beim älteren ext3-Dateisystem jedoch nicht der Fall war. Im vorherigen Absatz habe ich darüber gesprochen, wie ext3 zu viele Daten auf der Festplatte abgelegt hat, was die Dinge erheblich verlangsamt hat. fsync(). Um die Situation zu verbessern, löscht ext4 nur die „schmutzigen“ Seiten, die für eine bestimmte Datei relevant sind. Und die Daten anderer Dateien bleiben viel länger im Speicher als bei ext3. Dies wurde durchgeführt, um die Leistung zu verbessern (standardmäßig bleiben die Daten 30 Sekunden lang in diesem Zustand, Sie können dies mit konfigurieren dirty_expire_centisecs; hier Weitere Informationen dazu finden Sie hier). Das bedeutet, dass nach einem Absturz große Datenmengen unwiederbringlich verloren gehen können. Die Lösung für dieses Problem ist die Verwendung fsync() in Anwendungen, die eine stabile Datenspeicherung gewährleisten und diese bestmöglich vor den Folgen von Ausfällen schützen müssen. Funktion fsync() Arbeitet mit ext4 viel effizienter als mit ext3. Der Nachteil dieses Ansatzes besteht darin, dass seine Verwendung nach wie vor einige Vorgänge verlangsamt, beispielsweise die Installation von Programmen. Einzelheiten hierzu finden Sie hier hier и hier.

Das dritte Problem bzgl fsync(), entstand im Jahr 2018. Dann wurde im Rahmen des PostgreSQL-Projekts herausgefunden, dass die Funktion fsync() Wenn ein Fehler auftritt, werden „verschmutzte“ Seiten als „sauber“ markiert. Als Ergebnis die folgenden Aufrufe fsync() Machen Sie nichts mit solchen Seiten. Aus diesem Grund werden geänderte Seiten im Speicher gespeichert und nie auf die Festplatte geschrieben. Dies ist eine echte Katastrophe, da die Anwendung denkt, dass einige Daten auf die Festplatte geschrieben werden, dies jedoch nicht der Fall ist. Solche Misserfolge fsync() selten sind, kann die Anwendung in solchen Situationen nahezu nichts gegen das Problem unternehmen. Heutzutage stürzen PostgreSQL und andere Anwendungen in diesem Fall ab. HierIm Artikel „Können Anwendungen nach fsync-Fehlern wiederhergestellt werden?“ wird dieses Problem ausführlich untersucht. Derzeit ist die beste Lösung für dieses Problem die Verwendung von Direct I/O mit dem Flag O_SYNC oder mit einer Fahne O_DSYNC. Bei diesem Ansatz meldet das System Fehler, die bei der Ausführung bestimmter Datenschreibvorgänge auftreten können. Dieser Ansatz erfordert jedoch, dass die Anwendung die Puffer selbst verwaltet. Lesen Sie mehr darüber hier и hier.

Öffnen von Dateien mit den Flags O_SYNC und O_DSYNC

Kehren wir zur Diskussion der Linux-Mechanismen zurück, die eine dauerhafte Datenspeicherung ermöglichen. Es geht nämlich um die Verwendung der Flagge O_SYNC oder Flagge O_DSYNC beim Öffnen von Dateien per Systemaufruf öffnen(). Bei diesem Ansatz wird jeder Datenschreibvorgang wie nach jedem Befehl ausgeführt write() Dem System werden jeweils Befehle gegeben fsync() и fdatasync(). In POSIX-Spezifikationen Dies wird als „Synchronized I/O File Integrity Completion“ und „Data Integrity Completion“ bezeichnet. Der Hauptvorteil dieses Ansatzes besteht darin, dass nur ein Systemaufruf ausgeführt werden muss, um die Datenintegrität sicherzustellen, und nicht zwei (z. B. −). write() и fdatasync()). Der Hauptnachteil dieses Ansatzes besteht darin, dass alle Schreibvorgänge, die den entsprechenden Dateideskriptor verwenden, synchronisiert werden, was die Strukturierungsfähigkeit des Anwendungscodes einschränken kann.

Verwendung von Direct I/O mit dem O_DIRECT-Flag

Systemaufruf open() unterstützt die Flagge O_DIRECT, das den Cache des Betriebssystems umgehen soll, E/A-Vorgänge ausführt und direkt mit der Festplatte interagiert. Dies bedeutet in vielen Fällen, dass die vom Programm ausgegebenen Schreibbefehle direkt in Befehle übersetzt werden, die für die Arbeit mit der Festplatte bestimmt sind. Im Allgemeinen ist dieser Mechanismus jedoch kein Ersatz für die Funktionen fsync() oder fdatasync(). Tatsache ist, dass die Festplatte selbst dies kann Verzögerung oder Cache entsprechende Befehle zum Schreiben von Daten. Und noch schlimmer, in einigen Sonderfällen die E/A-Vorgänge, die bei Verwendung des Flags ausgeführt werden O_DIRECT, übertragen in traditionelle gepufferte Operationen. Der einfachste Weg, dieses Problem zu lösen, besteht darin, die Flagge zum Öffnen von Dateien zu verwenden O_DSYNC, was bedeutet, dass auf jeden Schreibvorgang ein Aufruf folgt fdatasync().

Es stellte sich heraus, dass das XFS-Dateisystem kürzlich einen „Schnellpfad“ für hinzugefügt hatte O_DIRECT|O_DSYNC-Datensätze. Wenn der Block mit überschrieben wird O_DIRECT|O_DSYNC, dann führt XFS, anstatt den Cache zu leeren, den FUA-Schreibbefehl aus, wenn das Gerät dies unterstützt. Ich habe dies mit dem Dienstprogramm überprüft blktrace auf einem Linux 5.4/Ubuntu 20.04-System. Dieser Ansatz sollte effizienter sein, da er die minimale Datenmenge auf die Festplatte schreibt und einen Vorgang anstelle von zwei (Schreiben und Leeren des Caches) verwendet. Ich habe einen Link dazu gefunden patch 2018-Kernel, der diesen Mechanismus implementiert. Es gibt einige Diskussionen darüber, diese Optimierung auf andere Dateisysteme anzuwenden, aber meines Wissens ist XFS bisher das einzige Dateisystem, das dies unterstützt.

sync_file_range()-Funktion

Linux hat einen Systemaufruf sync_file_range(), wodurch Sie nur einen Teil der Datei auf die Festplatte schreiben können, nicht die gesamte Datei. Dieser Aufruf initiiert einen asynchronen Flush und wartet nicht auf dessen Abschluss. Aber im Bezug auf sync_file_range() Dieser Befehl gilt als „sehr gefährlich“. Es wird nicht empfohlen, es zu verwenden. Merkmale und Gefahren sync_file_range() sehr gut beschrieben in Dies Material. Insbesondere scheint dieser Aufruf RocksDB zu verwenden, um zu steuern, wann der Kernel „schmutzige“ Daten auf die Festplatte schreibt. Gleichzeitig wird es aber auch dort eingesetzt, um eine stabile Datenspeicherung zu gewährleisten fdatasync(). In Code RocksDB hat einige interessante Kommentare zu diesem Thema. Es sieht zum Beispiel wie der Anruf aus sync_file_range() Bei Verwendung von ZFS werden die Daten nicht auf die Festplatte geleert. Die Erfahrung zeigt, dass selten verwendeter Code Fehler enthalten kann. Daher würde ich davon abraten, diesen Systemaufruf zu verwenden, es sei denn, dies ist unbedingt erforderlich.

Systemaufrufe zur Gewährleistung der Datenpersistenz

Ich bin zu dem Schluss gekommen, dass es drei Ansätze gibt, mit denen sich persistente I/O-Vorgänge durchführen lassen. Sie alle erfordern einen Funktionsaufruf fsync() für das Verzeichnis, in dem die Datei erstellt wurde. Dies sind die Ansätze:

  1. Aufrufen einer Funktion fdatasync() oder fsync() nach Funktion write() (besser zu verwenden fdatasync()).
  2. Arbeiten mit einem Dateideskriptor, der mit einer Flagge geöffnet wurde O_DSYNC oder O_SYNC (besser - mit einer Flagge O_DSYNC).
  3. Befehlsverwendung pwritev2() mit Fahne RWF_DSYNC oder RWF_SYNC (am besten mit einer Fahne RWF_DSYNC).

Leistungshinweise

Ich habe die Leistung der verschiedenen von mir untersuchten Mechanismen nicht sorgfältig gemessen. Die Unterschiede, die ich in der Geschwindigkeit ihrer Arbeit festgestellt habe, sind sehr gering. Das bedeutet, dass ich mich irren kann und dass das Gleiche unter anderen Bedingungen zu unterschiedlichen Ergebnissen führen kann. Zuerst werde ich darüber sprechen, was die Leistung stärker beeinflusst, und dann darüber, was die Leistung weniger beeinflusst.

  1. Das Überschreiben von Dateidaten 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, auch nach dem Systemaufruf fallocate(), aber das Ausmaß dieses Effekts kann variieren. Für eine optimale Leistung empfehle ich, anzurufen fallocate() um den benötigten Speicherplatz vorab zuzuweisen. Dann muss dieser Raum explizit mit Nullen gefüllt und aufgerufen werden fsync(). Dadurch werden die entsprechenden Blöcke im Dateisystem als „allocated“ statt als „unallocated“ markiert. Dies führt zu einer kleinen Leistungsverbesserung (ca. 2 %). Außerdem können einige Festplatten einen langsameren Zugriff auf den ersten Block aufweisen als andere. Dies bedeutet, dass das Füllen des Raums mit Nullen zu einer erheblichen Leistungsverbesserung (ca. 100 %) führen kann. Dies kann insbesondere bei Datenträgern passieren. AWS EBS (Dies sind inoffizielle Daten, ich konnte sie nicht bestätigen). Das Gleiche gilt für die Lagerung. Persistente GCP-Festplatte (und das sind bereits offizielle Informationen, bestätigt durch Tests). Andere Experten haben das Gleiche getan Beobachtungenbezogen auf verschiedene Festplatten.
  2. Je weniger Systemaufrufe, desto höher die Leistung (der Gewinn kann etwa 5 % betragen). Es sieht aus wie ein Anruf open() mit Fahne O_DSYNC oder anrufen pwritev2() mit Fahne RWF_SYNC schnellerer Anruf fdatasync(). Ich vermute, dass der Punkt hier darin liegt, dass bei diesem Ansatz die Tatsache eine Rolle spielt, dass weniger Systemaufrufe durchgeführt werden müssen, um die gleiche Aufgabe zu lösen (ein Aufruf statt zwei). Der Leistungsunterschied ist jedoch sehr gering, sodass Sie ihn problemlos ignorieren und etwas in der Anwendung verwenden können, das nicht zu einer Komplikation der Logik führt.

Wenn Sie sich für das Thema nachhaltige Datenspeicherung interessieren, finden Sie hier einige nützliche Materialien:

  • E/A-Zugriffsmethoden — ein Überblick über die Grundlagen von Eingabe-/Ausgabemechanismen.
  • Sicherstellen, dass die Daten die Festplatte erreichen - eine Geschichte darüber, was mit den Daten auf dem Weg von der Anwendung zur Festplatte passiert.
  • Wann sollten Sie das enthaltende Verzeichnis fsyncen? - die Antwort auf die Frage, wann man sich bewerben soll fsync() für Verzeichnisse. Kurz gesagt stellt sich heraus, dass Sie dies tun müssen, wenn Sie eine neue Datei erstellen. Der Grund für diese Empfehlung ist, dass es unter Linux viele Verweise auf dieselbe Datei geben kann.
  • SQL Server unter Linux: FUA-Interna – Hier finden Sie eine Beschreibung, wie die persistente Datenspeicherung in SQL Server auf der Linux-Plattform implementiert wird. Hier gibt es einige interessante Vergleiche zwischen Windows- und Linux-Systemaufrufen. Ich bin mir fast sicher, dass ich dank dieses Materials etwas über die FUA-Optimierung von XFS erfahren habe.

Haben Sie jemals Daten verloren, von denen Sie dachten, sie seien sicher auf der Festplatte gespeichert?

Langlebige Datenspeicherung und Linux-Datei-APIs

Langlebige Datenspeicherung und Linux-Datei-APIs

Source: habr.com