Schreiben von Programmen zur Bewältigung von E / A-Fehlern, die unter Linux zu Schreibverlust führen

138

TL; DR: Wenn der Linux-Kernel einen gepufferten E / A-Schreibvorgang verliert , kann die Anwendung dies herausfinden?

Ich weiß, dass Sie fsync()die Datei (und das übergeordnete Verzeichnis) für die Haltbarkeit benötigen . Die Frage ist, ob der Kernel fehlerhafte Puffer verliert, deren Schreibvorgang aufgrund eines E / A-Fehlers aussteht. Wie kann die Anwendung dies erkennen und wiederherstellen oder abbrechen?

Denken Sie an Datenbankanwendungen usw., bei denen die Reihenfolge der Schreibvorgänge und die Schreibdauer von entscheidender Bedeutung sein können.

Verlorene schreibt? Wie?

Die Linux - Kernel-Block Schicht kann unter bestimmten Umständen verlieren I / O - Anfragen gepuffert , die erfolgreich durch eingereicht wurde write(), pwrite()wie usw., mit einem Fehler:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Siehe end_buffer_write_sync(...)und end_buffer_async_write(...)infs/buffer.c ).

Auf neueren Kerneln enthält der Fehler stattdessen "Lost Async Page Write" , wie:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Da die Anwendung write()bereits fehlerfrei zurückgegeben wurde, scheint es keine Möglichkeit zu geben, einen Fehler an die Anwendung zurückzumelden.

Sie erkennen?

Ich bin mit den Kernelquellen nicht so vertraut, aber ich denke, dass sie AS_EIOauf den Puffer gesetzt sind, der nicht ausgeschrieben werden konnte, wenn ein asynchroner Schreibvorgang ausgeführt wird:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Es ist mir jedoch unklar, ob oder wie die Anwendung dies herausfinden kann, wenn sie später in fsync()der Datei bestätigt, dass sie sich auf der Festplatte befindet.

Es sieht aus wie wait_on_page_writeback_range(...)inmm/filemap.c Macht, do_sync_mapping_range(...)infs/sync.c der wiederum von genannt wird sys_sync_file_range(...). Es wird zurückgegeben, -EIOwenn ein oder mehrere Puffer nicht geschrieben werden konnten.

Wenn sich dies, wie ich vermute, auf fsync()das Ergebnis auswirkt, wenn die App in Panik gerät und ausfällt, wenn ein E / A-Fehler auftritt fsync()und weiß, wie sie ihre Arbeit beim Neustart erneut ausführen kann, sollte dies eine ausreichende Sicherheit sein?

Vermutlich kann die App nicht erkennen, welche Byte-Offsets in einer Datei den verlorenen Seiten entsprechen, sodass sie diese neu schreiben kann, wenn sie weiß, wie, aber wenn die App alle anstehenden Arbeiten seit dem letzten erfolgreichen fsync()der Datei wiederholt und diese neu schreibt Alle schmutzigen Kernel-Puffer, die verlorenen Schreibvorgängen für die Datei entsprechen, sollten alle E / A-Fehlerflags auf den verlorenen Seiten löschen und den nächsten fsync()Abschluss ermöglichen - richtig?

Gibt es dann noch andere harmlose Umstände, fsync()unter -EIOdenen die Rettung und Wiederholung von Arbeiten zu drastisch wäre?

Warum?

Natürlich sollten solche Fehler nicht auftreten. In diesem Fall ist der Fehler auf eine unglückliche Interaktion zwischen den dm-multipathStandardeinstellungen des Treibers und dem vom SAN verwendeten Erfassungscode zurückzuführen, der den Fehler beim Zuweisen von Thin Provisioning-Speicher meldet. Dies ist jedoch nicht der einzige Umstand, unter dem sie auftreten können. Ich habe auch Berichte darüber von beispielsweise Thin Provisioning LVM gesehen, wie sie von libvirt, Docker und anderen verwendet werden. Eine kritische Anwendung wie eine Datenbank sollte versuchen, mit solchen Fehlern umzugehen, anstatt blind weiterzumachen, als ob alles in Ordnung wäre.

Wenn der Kernel der Meinung ist, dass es in Ordnung ist, Schreibvorgänge zu verlieren, ohne an einer Kernel-Panik zu sterben, müssen Anwendungen einen Weg finden, um damit umzugehen.

Die praktische Auswirkung ist, dass ich einen Fall gefunden habe, in dem ein Multipath-Problem mit einem SAN zu verlorenen Schreibvorgängen führte, die zu einer Beschädigung der Datenbank führten, weil das DBMS nicht wusste, dass seine Schreibvorgänge fehlgeschlagen waren. Kein Spaß.

Craig Ringer
quelle
1
Ich befürchte, dies würde zusätzliche Felder in der SystemFileTable erfordern, um diese Fehlerbedingungen zu speichern und sich daran zu erinnern. Und eine Möglichkeit für den Userspace-Prozess, sie bei nachfolgenden Anrufen zu empfangen oder zu überprüfen. (Geben fsync () und close () diese Art von historischen Informationen zurück?)
joop
@joop Danke. Ich habe gerade eine Antwort mit dem gepostet, was meiner Meinung nach vor sich geht. Es macht etwas aus, einen Sanity-Check durchzuführen, da Sie anscheinend mehr darüber wissen, was los ist, als die Leute, die offensichtliche Varianten von "write ()" veröffentlicht haben, schließen () oder fsync ( ) für Haltbarkeit "ohne die Frage zu lesen?
Craig Ringer
Übrigens: Ich denke, Sie sollten sich wirklich mit den Kernelquellen befassen. Die Journalled-Dateisysteme würden wahrscheinlich unter den gleichen Problemen leiden. Ganz zu schweigen von der Handhabung der Swap-Partitionen. Da diese im Kernelraum leben, wird der Umgang mit diesen Bedingungen wahrscheinlich etwas starrer sein. writev (), das im Userspace sichtbar ist, scheint ebenfalls ein Ort zu sein, an dem man suchen kann. [bei Craig: Ja, weil ich Ihren Namen kenne und ich weiß, dass Sie kein kompletter Idiot sind; -]
Joop
1
Ich stimme zu, ich war nicht so fair. Leider ist Ihre Antwort nicht sehr zufriedenstellend, ich meine, es gibt keine einfache Lösung (überraschend?).
Jean-Baptiste Yunès
1
@ Jean-BaptisteYunès Richtig. Für das DBMS, mit dem ich arbeite, ist "Absturz und Wiederholung" akzeptabel. Für die meisten Apps ist dies keine Option, und sie müssen möglicherweise die schreckliche Leistung synchroner E / A tolerieren oder nur schlecht definiertes Verhalten und Beschädigung bei E / A-Fehlern akzeptieren.
Craig Ringer

Antworten:

91

fsync() kehrt zurück -EIO wenn der Kernel einen Schreibvorgang verloren hat

(Hinweis: Der frühe Teil verweist auf ältere Kernel; unten aktualisiert, um moderne Kernel widerzuspiegeln.)

Es sieht so aus, als würde das Ausschreiben des asynchronen Puffers bei end_buffer_async_write(...)Fehlern ein -EIOFlag auf der Seite für den fehlerhaften fehlerhaften Puffer für die Datei setzen :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

Dies wird dann erkannt von wait_on_page_writeback_range(...)aufgerufen von do_sync_mapping_range(...)aufgerufen von sys_sync_file_range(...)aufgerufen sys_sync_file_range2(...), um den Aufruf der C-Bibliothek zu implementieren fsync().

Aber nur einmal!

Dieser Kommentar zu sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

schlägt vor, dass bei fsync()Rückgabe -EIOoder (undokumentiert in der Manpage) der Fehlerstatus so nachträglich gelöscht -ENOSPCwirdfsync() Meldung den Erfolg meldet, obwohl die Seiten nie geschrieben wurden.

Sicher genug wait_on_page_writeback_range(...) löscht die Fehlerbits, wenn sie getestet werden :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Wenn die Anwendung erwartet, dass sie es erneut versuchen kann, fsync()bis sie erfolgreich ist, und darauf vertraut, dass sich die Daten auf der Festplatte befinden, ist dies furchtbar falsch.

Ich bin mir ziemlich sicher, dass dies die Quelle der Datenbeschädigung ist, die ich im DBMS gefunden habe. Es wird wiederholtfsync() und glaubt, dass alles gut wird, wenn es erfolgreich ist.

Ist das erlaubt?

In den POSIX / SuS-Dokumenten wirdfsync() dies nicht so oder so angegeben:

Wenn die Funktion fsync () fehlschlägt, wird nicht garantiert, dass ausstehende E / A-Vorgänge abgeschlossen wurden.

Linux-Manpage für fsync() sagt einfach nichts darüber aus, was bei einem Fehler passiert.

So scheint es, dass die Bedeutung von fsync() Fehlern "Keine Ahnung, was mit Ihren Schreibvorgängen passiert ist, ob es funktioniert hat oder nicht, versuchen Sie es noch einmal, um sicherzugehen".

Neuere Kernel

Auf 4.9 end_buffer_async_writeSätze -EIOauf der Seite, nur über mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Auf der Synchronisierungsseite denke ich, dass es ähnlich ist, obwohl die Struktur jetzt ziemlich komplex zu folgen ist. filemap_check_errorsin mm/filemap.cjetzt tut:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

das hat fast den gleichen Effekt. Fehlerprüfungen scheinen alle durchlaufen zu sein, filemap_check_errorswas ein Test-and-Clear durchführt:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Ich verwende es btrfsauf meinem Laptop, aber wenn ich einen ext4Loopback zum Testen erstelle /mnt/tmpund eine Perf-Sonde darauf einrichte:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Ich finde folgenden Aufrufstapel in perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Ein Durchlesen deutet darauf hin, dass sich moderne Kernel genauso verhalten.

Dies scheint zu bedeuten , dass , wenn fsync()(oder vermutlich write()oder close()) zurückkehrt -EIO, wird die Datei in einem gewissen undefinierten Zustand zwischen, wenn Sie das letzte Mal erfolgreich fsync()d oder close()d er und seine zuletzt write()zehn Zustand.

Prüfung

Ich habe einen Testfall implementiert, um dieses Verhalten zu demonstrieren .

Implikationen

Ein DBMS kann dies durch Eingabe der Absturzwiederherstellung bewältigen. Wie um alles in der Welt soll eine normale Benutzeranwendung damit umgehen? Die fsync()Manpage gibt keine Warnung aus, dass es "fsync-wenn-du-fühlst-wie-es" bedeutet, und ich gehe davon aus, dass viele Apps mit diesem Verhalten nicht gut umgehen können.

Fehlerberichte

Weiterführende Literatur

lwn.net hat dies im Artikel "Verbesserte Fehlerbehandlung auf Blockebene" angesprochen .

Postgresql.org Mailinglisten-Thread .

Craig Ringer
quelle
3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 ist ein mögliches Rennen, da es auf {ausstehende und geplante E / A} wartet, nicht auf {noch nicht geplante E / A}. Dies dient offensichtlich dazu, zusätzliche Hin- und Rückfahrten zum Gerät zu vermeiden. (Ich nehme an, Benutzer schreibt () nicht zurück, bis E / A geplant ist, für mmap () ist dies anders)
joop
3
Ist es möglich, dass der Aufruf eines anderen Prozesses an fsync für eine andere Datei auf derselben Festplatte die Fehlerrückgabe erhält?
Random832
3
@ Random832 Sehr relevant für eine Multi-Processing-DB wie PostgreSQL, also gute Frage. Sieht wahrscheinlich so aus, aber ich kenne den Kernel-Code nicht gut genug, um ihn zu verstehen. Ihre Prozesse sollten besser zusammenarbeiten, wenn beide trotzdem dieselbe Datei geöffnet haben.
Craig Ringer
1
@DavidFoerster: Die Systemaufrufe geben Fehler mit negativen Fehlercodes zurück. errnoist vollständig ein Konstrukt der Userspace C-Bibliothek. Es ist üblich, die Rückgabewertunterschiede zwischen den Syscalls und der C-Bibliothek wie folgt zu ignorieren (wie Craig Ringer oben), da der Fehlerrückgabewert zuverlässig angibt, auf welche (Syscall- oder C-Bibliotheksfunktion) verwiesen wird: " -1mit errno==EIO"bezieht sich auf eine C-Bibliotheksfunktion, während" -EIO"sich auf einen Systemaufruf bezieht. Schließlich sind Linux-Manpages online die aktuellste Referenz für Linux-Manpages.
Nominelles Tier
2
@CraigRinger: Um Ihre letzte Frage zu beantworten: "Durch Verwendung von E / A auf niedriger Ebene und fsync()/ oder fdatasync()wenn die Transaktionsgröße eine vollständige Datei ist; durch Verwendung mmap()/ msync()wenn die Transaktionsgröße ein seitenausgerichteter Datensatz ist; und durch Verwendung von I auf niedriger Ebene / O fdatasync(),, und mehrere gleichzeitige Dateideskriptoren (ein Deskriptor und ein Thread pro Transaktion) für dieselbe Datei, andernfalls " . Die Linux-spezifischen Open File Description Locks ( fcntl(), F_OFD_) sind bei der letzten sehr nützlich.
Nominelles Tier
22

Da write () der Anwendung bereits fehlerfrei zurückgegeben wurde, scheint es keine Möglichkeit zu geben, einen Fehler an die Anwendung zurückzumelden.

Ich stimme nicht zu. writekann ohne Fehler zurückkehren, wenn der Schreibvorgang einfach in die Warteschlange gestellt wird. Der Fehler wird jedoch beim nächsten Vorgang gemeldet, bei dem das eigentliche Schreiben auf die Festplatte erforderlich ist, dh beim nächstenfsync , möglicherweise bei einem nachfolgenden Schreibvorgang, wenn das System beschließt, den Cache zu leeren und um am wenigsten beim letzten Schließen der Datei.

Aus diesem Grund ist es für die Anwendung wichtig, den Rückgabewert von close zu testen, um mögliche Schreibfehler zu erkennen.

Wenn Sie wirklich in der Lage sein müssen, eine clevere Fehlerverarbeitung durchzuführen, müssen Sie davon ausgehen, dass alles, was seit dem letzten Erfolg geschrieben wurde, fsync möglicherweise fehlgeschlagen ist und dass zumindest etwas fehlgeschlagen ist.

Serge Ballesta
quelle
4
Ja, ich denke das nagelt es. Dies wäre in der Tat deuten darauf hin , dass die Anwendung sollte alle seine Arbeit seit dem letzten bestätigten erfolgreichen wieder tun fsync()oder close()der Datei , wenn es sich um eine bekommt -EIOvon write(), fsync()oder close(). Das macht Spaß.
Craig Ringer
1

write(2) bietet weniger als Sie erwarten. Die Manpage ist sehr offen über die Semantik eines erfolgreichen write()Anrufs:

Eine erfolgreiche Rückgabe von write()garantiert nicht, dass Daten auf die Festplatte übertragen wurden. Tatsächlich garantiert es bei einigen fehlerhaften Implementierungen nicht einmal, dass Speicherplatz erfolgreich für die Daten reserviert wurde. Der einzige Weg, um sicher zu sein, besteht darin, fsync(2) anzurufen, nachdem Sie alle Ihre Daten geschrieben haben.

Wir können daraus schließen, dass ein Erfolg write()lediglich bedeutet, dass die Daten die Pufferfunktionen des Kernels erreicht haben. Wenn das Fortbestehen des Puffers fehlschlägt, gibt ein nachfolgender Zugriff auf den Dateideskriptor den Fehlercode zurück. Als letztes Mittel kann das sein close(). Die Manpage des closeSystemaufrufs (2) enthält den folgenden Satz:

Es ist durchaus möglich, dass Fehler bei einer vorherigen write(2) Operation zuerst bei final close() gemeldet werden .

Wenn Ihre Anwendung das Abschreiben von Daten beibehalten muss, muss sie fsync/ fsyncdataregelmäßig verwenden:

fsync()überträgt ("Flushes") alle modifizierten In-Core-Daten (dh modifizierte Puffer-Cache-Seiten für) der Datei, auf die der Dateideskriptor fd verweist, auf das Plattengerät (oder ein anderes permanentes Speichergerät), so dass alle geänderten Informationen abgerufen werden können auch nach dem Absturz des Systems oder dem Neustart. Dies umfasst das Durchschreiben oder Leeren eines Festplattencaches, falls vorhanden. Der Anruf wird blockiert, bis das Gerät meldet, dass die Übertragung abgeschlossen ist.

fzgregor
quelle
4
Ja, mir ist bewusst, dass dies fsync()erforderlich ist. Aber im konkreten Fall , in dem der Kernel die Seiten aufgrund eines E / A - Fehler verliert wird fsync()scheitern? Unter welchen Umständen kann es dann danach gelingen?
Craig Ringer
Ich kenne auch die Kernelquelle nicht. Nehmen wir eine fsync()Rendite -EIObei E / A-Problemen an (Wofür wäre es sonst gut?). Die Datenbank weiß also, dass ein Teil eines vorherigen Schreibvorgangs fehlgeschlagen ist, und kann in den Wiederherstellungsmodus wechseln. Willst du das nicht? Was ist die Motivation für Ihre letzte Frage? Möchten Sie wissen, welcher Schreibvorgang fehlgeschlagen ist, oder den Dateideskriptor zur weiteren Verwendung wiederherstellen?
fzgregor
Im Idealfall wird ein DBMS es vorziehen, keine Absturzwiederherstellung zu starten (alle Benutzer werden gestartet und vorübergehend unzugänglich oder zumindest schreibgeschützt), wenn dies möglicherweise vermieden werden kann. Aber selbst wenn der Kernel uns "Bytes 4096 bis 8191 von fd X" mitteilen könnte, wäre es schwierig herauszufinden, was dort (neu) geschrieben werden soll, ohne so ziemlich die Wiederherstellung nach einem Absturz durchzuführen. Also habe ich die wichtigste Frage erraten ist , ob unschuldigere Umstände gibt es , wo fsync()zurückkehren kann , -EIOwo es ist sicher zu wiederholen, und wenn es möglich ist, den Unterschied zu erkennen.
Craig Ringer
Sicher, Crash Recovery ist der letzte Ausweg. Aber wie Sie bereits sagten, werden diese Probleme voraussichtlich sehr, sehr selten sein. Daher sehe ich bei keinem ein Problem mit der Wiederherstellung -EIO. Wenn jeder Dateideskriptor jeweils nur von einem Thread verwendet wird, kann dieser Thread zum letzten zurückkehren fsync()und die write()Aufrufe wiederholen . Wenn diese write()s jedoch nur einen Teil eines Sektors schreiben, ist der unveränderte Teil möglicherweise immer noch beschädigt.
Fzgregor
1
Sie haben Recht, dass eine Wiederherstellung nach einem Absturz wahrscheinlich vernünftig ist. Bei teilweise beschädigten Sektoren speichert das DBMS (PostgreSQL) ein Bild der gesamten Seite, wenn es aus diesem Grund zum ersten Mal nach einem bestimmten Prüfpunkt berührt wird. Es sollte also in Ordnung sein :)
Craig Ringer
0

Verwenden Sie das O_SYNC-Flag, wenn Sie die Datei öffnen. Es stellt sicher, dass die Daten auf die Festplatte geschrieben werden.

Wenn dich das nicht befriedigt, wird es nichts geben.

hardmanwang
quelle
17
O_SYNCist ein Albtraum für Leistung. Dies bedeutet, dass die Anwendung während der Datenträger-E / A nichts anderes tun kann , es sei denn, sie erzeugt E / A-Threads. Sie können auch sagen, dass die gepufferte E / A-Schnittstelle unsicher ist und jeder AIO verwenden sollte. Sicherlich können stillschweigend verlorene Schreibvorgänge in gepufferten E / A nicht akzeptabel sein?
Craig Ringer
3
( O_DATASYNCist in dieser Hinsicht nur geringfügig besser)
Craig Ringer
@CraigRinger Sie sollten AIO verwenden, wenn Sie dieses Bedürfnis haben und irgendeine Art von Leistung benötigen. Oder verwenden Sie einfach ein DBMS. es erledigt alles für Sie.
Demi
10
@ Demi Die Anwendung hier ist eine Datenbank (postgresql). Ich bin sicher, Sie können sich vorstellen, dass das Umschreiben der gesamten Anwendung zur Verwendung von AIO anstelle von gepufferten E / A nicht praktikabel ist. Es sollte auch nicht notwendig sein.
Craig Ringer
-5

Überprüfen Sie den Rückgabewert von close. Das Schließen kann fehlschlagen, während gepufferte Schreibvorgänge erfolgreich zu sein scheinen.

Malcolm McLean
quelle
8
Nun, wir wollen kaum sein open()ing und close()ing die Datei alle paar Sekunden. Deshalb haben wir fsync()...
Craig Ringer