Wie kopiere ich eine Datei transaktional?

9

Ich möchte eine Datei von A nach B kopieren, die sich möglicherweise auf verschiedenen Dateisystemen befindet.

Es gibt einige zusätzliche Anforderungen:

  1. Die Kopie ist alles oder nichts, keine teilweise oder beschädigte Datei B bleibt beim Absturz an Ort und Stelle;
  2. Überschreiben Sie keine vorhandene Datei B;
  3. Konkurrieren Sie nicht mit einer gleichzeitigen Ausführung desselben Befehls, höchstens kann dies gelingen.

Ich denke das kommt näher:

cp A B.part && \
ln B B.part && \
rm B.part

Aber 3. wird verletzt, indem der cp nicht fehlschlägt, wenn B.part existiert (auch mit -n Flag). Anschließend könnte 1. fehlschlagen, wenn der andere Prozess den CP "gewinnt" und die verknüpfte Datei unvollständig ist. B.part könnte auch eine nicht verwandte Datei sein, aber ich bin froh, dass ich scheitern kann, ohne in diesem Fall andere versteckte Namen auszuprobieren.

Ich denke, Bash Noclobber hilft, funktioniert das voll und ganz? Gibt es eine Möglichkeit, ohne die Bash-Versionsanforderung auszukommen?

#!/usr/bin/env bash
set -o noclobber
cat A > B.part && \
ln B.part B && \
rm B.part

Follow-up, ich weiß, dass einige Dateisysteme dabei sowieso ausfallen werden (NFS). Gibt es eine Möglichkeit, solche Dateisysteme zu erkennen?

Einige andere verwandte, aber nicht ganz dieselben Fragen:

Annäherung der atomaren Bewegung über Dateisysteme hinweg?

Ist mv atomar auf meinem fs?

Gibt es eine Möglichkeit, Dateien und Verzeichnisse atomar von tempfs auf die ext4-Partition in eMMC zu verschieben?

https://rcrowley.org/2010/01/06/things-unix-can-do-atomically.html

Evan Benn
quelle
2
Sind Sie nur besorgt über die gleichzeitige Ausführung desselben Befehls (dh das Sperren in Ihrem Tool könnte ausreichen) oder über andere externe Eingriffe in die Dateien?
Michael Homer
3
"Transactional" könnte besser sein
Muru
1
@ MichaelHomer innerhalb des Tools ist gut genug, ich denke draußen würde die Dinge sehr schwer machen! Wenn es mit Dateisperren möglich ist ...
Evan Benn
1
@marcelm mvüberschreibt eine vorhandene Datei. B. mv -nbenachrichtigt nicht, dass ein Fehler aufgetreten ist. ln(1)( rename(2)) schlägt fehl, wenn B bereits vorhanden ist.
Evan Benn
1
@EvanBenn Guter Punkt! Ich hätte Ihre Anforderungen besser lesen sollen. (Ich brauche in der Regel atomare Updates eines vorhandenen Ziels, und ich antwortete in diesem Sinne)
marcelm

Antworten:

11

rsyncmacht diesen Job. Eine temporäre Datei wird O_EXCLstandardmäßig erstellt (nur deaktiviert, wenn Sie sie verwenden --inplace) und dann renamedüber der Zieldatei. Verwenden Sie --ignore-existingdiese Option, um B nicht zu überschreiben, falls vorhanden.

In der Praxis hatte ich auf ext4-, zfs- oder sogar NFS-Mounts nie Probleme damit.

Hermann
quelle
rsync macht das wahrscheinlich gut, aber die extrem komplizierte Manpage macht mir Angst. Optionen, die andere Optionen implizieren, nicht miteinander kompatibel sind usw.
Evan Benn
Rsync hilft bei Anforderung 3 nicht, soweit ich das beurteilen kann. Trotzdem ist es ein fantastisches Tool, und Sie sollten nicht vor ein bisschen Manpage-Lesen zurückschrecken. Sie können auch entweder github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md oder cheat.sh/rsync ausprobieren . (tldr und cheat sind zwei verschiedene Projekte, die bei dem von Ihnen angegebenen Problem helfen sollen, nämlich "Manpage ist TL; DR". Viele allgemeine Befehle werden unterstützt, und Sie werden die am häufigsten verwendeten Verwendungen sehen.
sitaram
@EvanBenn rsync ist ein erstaunliches Werkzeug und es lohnt sich zu lernen! Die Manpage ist kompliziert, weil sie so vielseitig ist. Lassen Sie sich nicht einschüchtern :)
Josh
@sitaram, # 3 könnte mit einer PID-Datei aufgelöst werden. Ein kleines Skript wie in der Antwort hier .
Robert Riedl
2
Dies ist die beste Antwort. Rsync ist der Industriestandard für atomare Dateiübertragungen und kann in verschiedenen Konfigurationen alle Ihre Anforderungen erfüllen.
wKavey
4

Keine Sorge, noclobberist eine Standardfunktion .

ilkkachu
quelle
Vielen Dank, versucht, diese prägnante Antwort zu akzeptieren. Irgendwelche Kommentare zu zwielichtigen Dateisystemen wie NFS?
Evan Benn
@EvanBenn, ich wollte hinzufügen, dass ich nicht sicher bin, ob NFS Sie hier irgendwie durcheinander bringen wird, aber ich habe es vergessen.
Ilkkachu
4

Sie haben nach NFS gefragt. Diese Art von Code wird wahrscheinlich unter NFS beschädigt, da die Prüfung auf noclobberzwei separate NFS-Vorgänge umfasst (prüfen, ob eine Datei vorhanden ist, neue Datei erstellen) und zwei Prozesse von zwei separaten NFS-Clients möglicherweise in einen Race-Zustand geraten, in dem beide erfolgreich sind ( beide verifizieren, dass es B.partnoch nicht existiert, und beide erstellen es erfolgreich, wodurch sie sich gegenseitig überschreiben.)

Es gibt nicht wirklich eine generische Überprüfung, ob das Dateisystem, in das Sie schreiben, so etwas wie noclobberatomar unterstützt oder nicht. Sie könnten den Dateisystemtyp überprüfen, ob es sich um NFS handelt, aber das wäre eine Heuristik und nicht unbedingt eine Garantie. Dateisysteme wie SMB / CIFS (Samba) leiden wahrscheinlich unter denselben Problemen. Dateisysteme, die über FUSE verfügbar gemacht werden, können sich möglicherweise korrekt verhalten oder nicht, dies hängt jedoch hauptsächlich von der Implementierung ab.


Ein möglicherweise besserer Ansatz besteht darin, die Kollision im B.partSchritt zu vermeiden , indem Sie einen eindeutigen Dateinamen verwenden (in Zusammenarbeit mit anderen Agenten), damit Sie sich nicht darauf verlassen müssen noclobber. Beispielsweise können Sie als Teil des Dateinamens Ihren Hostnamen, Ihre PID und einen Zeitstempel (+ möglicherweise eine Zufallszahl) angeben. Da auf einem Host zu einem bestimmten Zeitpunkt ein einzelner Prozess unter einer bestimmten PID ausgeführt werden sollte, sollte dies der Fall sein Einzigartigkeit garantieren.

Also eines von:

test -f B && continue  # skip already existing
unique=$(hostname).$$.$(date +%s).$RANDOM
cp A B.part."$unique"
# Maybe check for existance of B again, remove
# the temporary file and bail out in that case.
mv B.part."$unique" B
# mv (rename) should always succeed, overwrite a
# previously copied B if one exists.

Oder:

test -f B && continue  # skip already existing
unique=$(hostname).$$.$(date +%s).$RANDOM
cp A B.part."$unique"
if ln B.part."$unique" B ; then
    echo "Success creating B"
else
    echo "Failed creating B, already existed"
fi
# Both cases require cleanup.
rm B.part."$unique"

Wenn Sie also eine Race-Bedingung zwischen zwei Agenten haben, fahren beide mit der Operation fort, aber die letzte Operation ist atomar, sodass entweder B mit einer vollständigen Kopie von A existiert oder B nicht existiert.

Sie können die Größe des Rennens reduzieren, indem Sie nach der Kopie und vor der Operation mvoder erneut prüfen ln, aber es gibt immer noch eine kleine Rennbedingung. Unabhängig von der Rennbedingung sollte der Inhalt von B jedoch konsistent sein, vorausgesetzt, beide Prozesse versuchen, ihn aus A (oder einer Kopie aus einer gültigen Datei als Ursprung) zu erstellen.

Beachten Sie, dass in der ersten Situation mit mv, wenn ein Rennen existiert, der letzte Prozess derjenige ist, der gewinnt, da das Umbenennen (2) eine vorhandene Datei atomar ersetzt:

Wenn der neue Pfad bereits vorhanden ist, wird er atomar ersetzt, sodass es keinen Punkt gibt, an dem ein anderer Prozess, der versucht, auf den neuen Pfad zuzugreifen , ihn als fehlend ansieht . [...]

Wenn ein neuer Pfad vorhanden ist, der Vorgang jedoch aus irgendeinem Grund fehlschlägt, wird rename()garantiert, dass eine Instanz des neuen Pfads vorhanden bleibt .

Es ist also durchaus möglich, dass Prozesse, die B zu diesem Zeitpunkt verbrauchen, während dieses Prozesses unterschiedliche Versionen davon (unterschiedliche Inodes) sehen. Wenn die Autoren nur alle versuchen, denselben Inhalt zu kopieren, und die Leser einfach den Inhalt der Datei verbrauchen, ist dies möglicherweise in Ordnung. Wenn sie unterschiedliche Inodes für Dateien mit demselben Inhalt erhalten, sind sie trotzdem glücklich.

Der zweite Ansatz, bei dem ein Hardlink verwendet wird, sieht besser aus, aber ich erinnere mich, dass ich von vielen gleichzeitigen Clients Experimente mit Hardlinks in einer engen Schleife auf NFS durchgeführt und den Erfolg gezählt habe. Dort schien es immer noch einige Rennbedingungen zu geben, unter denen zwei Clients einen Hardlink herausgaben Operation zur gleichen Zeit, mit dem gleichen Ziel, schienen beide erfolgreich zu sein. (Es ist möglich, dass dieses Verhalten mit der jeweiligen NFS-Server-Implementierung YMMV zusammenhängt.) In jedem Fall ist dies wahrscheinlich die gleiche Art von Race-Bedingung, bei der Sie möglicherweise zwei separate Inodes für dieselbe Datei erhalten, wenn es schwer ist Parallelität zwischen Autoren, um diese Rennbedingungen auszulösen. Wenn Ihre Autoren konsistent sind (beide kopieren A nach B) und Ihre Leser nur den Inhalt konsumieren, könnte dies ausreichen.

Schließlich haben Sie das Sperren erwähnt. Leider fehlt das Sperren stark, zumindest in NFSv3 (nicht sicher über NFSv4, aber ich wette, es ist auch nicht gut). Wenn Sie das Sperren in Betracht ziehen, sollten Sie verschiedene Protokolle für das verteilte Sperren prüfen, möglicherweise außerhalb des Bandes mit dem tatsächliche Dateikopien, aber das ist sowohl störend als auch komplex und anfällig für Probleme wie Deadlocks. Daher würde ich sagen, dass es besser ist, dies zu vermeiden.


Weitere Informationen zum Thema Atomizität in NFS finden Sie im Maildir-Postfachformat , das erstellt wurde, um Sperren zu vermeiden und auch unter NFS zuverlässig zu funktionieren. Dies geschieht, indem überall eindeutige Dateinamen beibehalten werden (sodass Sie am Ende nicht einmal ein endgültiges B erhalten).

Vielleicht etwas interessanter für Ihren speziellen Fall, erweitert das Maildir ++ - Format Maildir, um Unterstützung für das Postfachkontingent hinzuzufügen, und zwar durch atomares Aktualisieren einer Datei mit einem festen Namen innerhalb des Postfachs (so dass dies möglicherweise näher an Ihrem B. liegt). Ich denke, Maildir ++ versucht es Anhängen, was unter NFS nicht wirklich sicher ist, aber es gibt einen Neuberechnungsansatz, der ein ähnliches Verfahren verwendet und als atomarer Ersatz gültig ist.

Hoffentlich sind all diese Hinweise nützlich!

filbranden
quelle
2

Sie können dafür ein Programm schreiben.

Verwenden Sie open(O_CREAT|O_RDWD)diese Option , um die Zieldatei zu öffnen, alle Bytes und Metadaten zu lesen und zu überprüfen, ob die Zieldatei vollständig ist. Wenn nicht, gibt es zwei Möglichkeiten:

  1. Unvollständiges Schreiben

  2. Ein anderer Prozess führt dasselbe Programm aus.

Versuchen Sie, eine geöffnete Dateibeschreibungssperre für die Zieldatei zu erhalten.

Ein Fehler bedeutet, dass gleichzeitig ein Prozess stattfindet. Der aktuelle Prozess sollte vorhanden sein.

Erfolg bedeutet, dass der letzte Schreibvorgang abgestürzt ist. Sie sollten von vorne beginnen oder versuchen, ihn durch Schreiben in die Datei zu beheben.

Beachten Sie auch, dass Sie fsync()nach dem Schreiben in die Zieldatei besser sind, bevor Sie die Datei schließen und die Sperre aufheben. Andernfalls werden möglicherweise noch nicht auf der Festplatte befindliche Daten gelesen.

https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html

Dies ist wichtig, damit Sie zwischen einem gleichzeitig ausgeführten Programm und einem zuletzt abgestürzten Vorgang unterscheiden können.

炸鱼 薯条 德里克
quelle
Vielen Dank für die Info, ich bin daran interessiert, dies selbst umzusetzen und werde es versuchen. Ich bin überrascht, dass es nicht bereits als Teil einiger Coreutils / ähnlicher Pakete existiert!
Evan Benn
Dieser Ansatz kann die nicht teilweise oder beschädigte Datei B nicht erfüllen, die bei einem Absturz erforderlich ist. Es ist wirklich am besten, den Standardansatz zu verwenden, die Datei in einen temporären Namen zu kopieren und dann an ihren Platz zu verschieben: Die Verschiebung kann atomar sein, was beim Kopieren nicht möglich ist.
Reinierpost
@reinierpost Wenn Absturz, aber Daten nicht vollständig kopiert werden, bleiben teilweise kopierte Daten, egal was passiert. Aber mein Ansatz wird dies erkennen und beheben. Das Verschieben einer Datei kann nicht atomar sein. Daten, die auf den physischen Sektor der Festplatte geschrieben werden, sind nicht atomar, aber Software (z. B. Betriebssystem-Dateisystemtreiber, dieser Ansatz) kann sie beheben (wenn rw) oder einen konsistenten Status melden (wenn ro). , wie im Kommentarbereich der Frage erwähnt. Auch die Frage ist über das Kopieren, nicht das Bewegen.
26 薯条 德里克
Ich habe auch O_TMPFILE gesehen, was wahrscheinlich helfen würde. (und wenn nicht auf dem FS verfügbar, sollte ein Fehler verursachen)
Evan Benn
@Evan Haben Sie das Dokument gelesen oder haben Sie jemals darüber nachgedacht, warum O_TMPFILE auf die Unterstützung des Dateisystems angewiesen ist?
26 薯条 德里克
0

Sie erhalten das richtige Ergebnis, wenn Sie cpzusammen mit mv. Dies ersetzt entweder "B" durch eine neue Kopie von "A" oder lässt "B" wie zuvor.

cp A B.tmp && mv B.tmp B

Update zur Anpassung an vorhandene B:

cp A B.tmp && if [ ! -e B ]; then mv B.tmp B; else rm B.tmp; fi

Dies ist nicht 100% atomar, aber es kommt nahe. Es gibt eine Rennbedingung, bei der zwei dieser Dinge ausgeführt werden, beide gleichzeitig in den ifTest eintreten , beide sehen, dass Bdies nicht vorhanden ist, und dann beide den Test ausführen mv.

Kaan
quelle
mv B.tmp B überschreibt ein bereits vorhandenes B. cp A B.tmp überschreibt ein bereits vorhandenes B.tmp, beide Fehler.
Evan Benn
mv B.tmp Bwird erst ausgeführt, wenn es cp A B.tmpzuerst ausgeführt wird und einen Erfolgsergebniscode zurückgibt. Wie ist das ein Fehler? Ich bin auch damit einverstanden, dass cp A B.tmpein vorhandenes überschrieben B.tmpwird, was Sie tun möchten. Das &&garantiert, dass der 2. Befehl genau dann ausgeführt wird, wenn der erste normal ausgeführt wird.
Kaan
In der Frage wird Erfolg so definiert, dass bereits vorhandene Dateien B nicht überschrieben werden. Die Verwendung von B.tmp ist ein Mechanismus, darf jedoch auch keine bereits vorhandenen Dateien überschreiben.
Evan Benn
Ich habe meine Antwort aktualisiert. Wenn Sie eine 100% ige Atomizität benötigen, wenn Dateien vorhanden sein können oder nicht, und mehrere Threads, benötigen Sie letztendlich irgendwo eine einzige exklusive Sperre (erstellen Sie eine spezielle Datei oder verwenden Sie eine Datenbank oder ...), der jeder als Teil der folgt Kopier- / Verschiebevorgang.
Kaan
Dieses Update überschreibt immer noch B.tmp und hat eine Racebedingung zwischen dem Test und dem MV. Ja, es geht darum, die Dinge richtig zu machen, hoffentlich nicht ungefähr gut genug. Andere Antworten zeigen, warum Sperren und Datenbanken nicht benötigt werden.
Evan Benn