Comment copier un fichier de manière transactionnelle?

9

Je souhaite copier un fichier de A vers B, qui peut se trouver sur différents systèmes de fichiers.

Il existe des exigences supplémentaires:

  1. La copie est tout ou rien, aucun fichier B partiel ou corrompu laissé en place lors d'un crash;
  2. N'écrasez pas un fichier B existant;
  3. Ne rivalisez pas avec une exécution simultanée de la même commande, tout au plus peut réussir.

Je pense que cela se rapproche:

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

Mais 3. est violé par le cp n'échouant pas si B.part existe (même avec le drapeau -n). Par la suite, 1. pourrait échouer si l'autre processus «gagne» le cp et que le fichier lié en place est incomplet. B.part pourrait également être un fichier non lié, mais je suis heureux d'échouer sans essayer d'autres noms cachés dans ce cas.

Je pense que bash noclobber aide, cela fonctionne-t-il pleinement? Existe-t-il un moyen de se passer de la version bash requise?

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

Suivi, je sais que certains systèmes de fichiers échoueront de toute façon (NFS). Existe-t-il un moyen de détecter de tels systèmes de fichiers?

Quelques autres questions liées mais pas tout à fait les mêmes:

Approximation du mouvement atomique à travers les systèmes de fichiers?

Le mv est-il atomique sur mon fs?

existe-t-il un moyen de déplacer atomiquement le fichier et le répertoire de tempfs vers la partition ext4 sur eMMC

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

Evan Benn
la source
2
Êtes-vous uniquement préoccupé par l'exécution simultanée de la même commande (c'est-à-dire que le verrouillage dans votre outil pourrait suffire), ou par d'autres interférences externes avec les fichiers?
Michael Homer
3
"Transactionnel" pourrait être mieux
muru
1
@MichaelHomer dans l'outil est assez bon, je pense que l'extérieur rendrait les choses très difficiles! Si c'est possible avec des verrous de fichiers ...
Evan Benn
1
@marcelm mvécrasera un fichier existant B. mv -nne notifiera pas qu'il a échoué. ln(1)( rename(2)) échouera si B existe déjà.
Evan Benn
1
@EvanBenn Bon point! J'aurais dû mieux lire vos exigences. (J'ai tendance à avoir besoin de mises à jour atomiques d'une cible existante, et je répondais en gardant cela à l'esprit)
marcelm

Réponses:

11

rsyncfait ce travail. Un fichier temporaire est O_EXCLcréé par défaut (désactivé uniquement si vous utilisez --inplace), puis renamedsur le fichier cible. Utilisez --ignore-existingpour ne pas écraser B s'il existe.

En pratique, je n'ai jamais rencontré de problème avec cela sur des supports ext4, zfs ou même NFS.

Hermann
la source
rsync le fait probablement très bien, mais la page de manuel extrêmement compliquée me fait peur. options impliquant d'autres options, incompatibles entre elles, etc.
Evan Benn
Rsync n'aide pas avec l'exigence n ° 3, pour autant que je sache. Pourtant, c'est un outil fantastique, et vous ne devriez pas hésiter à lire un peu la page de manuel. Vous pouvez également essayer github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md ou cheat.sh/rsync . (tldr et cheat sont deux projets différents qui visent à résoudre le problème que vous avez indiqué, à savoir, "la page de manuel est TL; DR"; de nombreuses commandes courantes sont prises en charge et vous verrez les utilisations les plus courantes indiquées.
sitaram
@EvanBenn rsync est un outil incroyable et mérite d'être étudié! Sa page de manuel est compliquée car elle est tellement polyvalente. Ne soyez pas intimidé :)
Josh
@sitaram, # 3 pourrait être résolu avec un fichier pid. Un petit script comme dans la réponse ici .
Robert Riedl
2
C'est la meilleure réponse. Rsync est le standard de référence pour les transferts de fichiers atomiques et, dans diverses configurations, peut répondre à toutes vos exigences.
wKavey
4

Ne vous inquiétez pas, noclobberc'est une fonctionnalité standard .

ilkkachu
la source
Merci, tenté d'accepter cette réponse succincte. Un commentaire sur les systèmes de fichiers douteux comme NFS?
Evan Benn
@EvanBenn, je voulais ajouter que je ne sais pas si NFS va vous gâcher ici d'une manière ou d'une autre, mais j'ai oublié.
ilkkachu
4

Vous avez posé des questions sur NFS. Ce type de code risque de casser sous NFS, car la vérification denoclobber implique deux opérations NFS distinctes (vérifier si le fichier existe, créer un nouveau fichier) et deux processus de deux clients NFS distincts peuvent entrer dans une condition de concurrence critique où les deux réussissent ( les deux vérifient qu'il B.partn'existe pas encore, puis les deux procèdent à sa création, ils se remplacent donc.)

Il n'y a pas vraiment de vérification générique pour savoir si le système de fichiers sur lequel vous écrivez prendra en charge quelque chose comme noclobber atomiquement ou non. Vous pouvez vérifier le type de système de fichiers, que ce soit NFS, mais ce serait une heuristique et pas nécessairement une garantie. Les systèmes de fichiers comme SMB / CIFS (Samba) sont susceptibles de souffrir des mêmes problèmes. Les systèmes de fichiers exposés via FUSE peuvent ou non se comporter correctement, mais cela dépend principalement de l'implémentation.


Une approche peut-être meilleure consiste à éviter la collision à l' B.partétape, en utilisant un nom de fichier unique (grâce à la coopération avec d'autres agents) afin que vous n'ayez pas à dépendre denoclobber . Par exemple, vous pouvez inclure, dans le cadre du nom de fichier, votre nom d'hôte, votre PID et un horodatage (+ éventuellement un nombre aléatoire). garantir l'unicité.

Donc, soit l'un des:

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.

Ou:

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"

Donc, si vous avez une condition de concurrence entre deux agents, ils procéderont tous les deux à l'opération, mais la dernière opération sera atomique, donc soit B existe avec une copie complète de A, soit B n'existe pas.

Vous pouvez réduire la taille de la course en vérifiant à nouveau après la copie et avant le mvouln opération , mais il y a encore une petite condition de course. Mais, quelle que soit la condition de concurrence, le contenu de B doit être cohérent, en supposant que les deux processus tentent de le créer à partir de A (ou d'une copie d'un fichier valide comme origine.)

Notez que dans la première situation avec mv, lorsqu'une course existe, le dernier processus est celui qui gagne, car rename (2) remplacera atomiquement un fichier existant:

Si newpath existe déjà, il sera atomiquement remplacé, de sorte qu'il n'y ait aucun point auquel un autre processus tentant d'accéder à newpath le trouvera manquant. [...]

Si newpath existe mais que l'opération échoue pour une raison quelconque, rename()garantit de laisser une instance de newpath en place.

Il est donc tout à fait possible que les processus consommant B à l'époque puissent en voir différentes versions (différents inodes) au cours de ce processus. Si les rédacteurs essaient tous de copier le même contenu et que les lecteurs consomment simplement le contenu du fichier, cela pourrait être bien, s'ils obtiennent des inodes différents pour des fichiers avec le même contenu, ils seront tout de même satisfaits.

La deuxième approche utilisant un lien dur semble meilleure, mais je me souviens avoir fait des expériences avec des liens durs dans une boucle étroite sur NFS à partir de nombreux clients simultanés et compter le succès et il semblait toujours y avoir des conditions de concurrence là-bas, où il semblait que deux clients avaient émis un lien dur opération en même temps, avec la même destination, les deux semblaient réussir. (Il est possible que ce comportement soit lié à l'implémentation particulière du serveur NFS, YMMV.) Dans tous les cas, il s'agit probablement du même type de condition de concurrence, où vous pourriez finir par obtenir deux inodes distincts pour le même fichier dans les cas où il y a du lourd concurrence entre les écrivains pour déclencher ces conditions de concurrence. Si vos rédacteurs sont cohérents (copiant tous les deux de A à B) et que vos lecteurs n'en consomment que le contenu, cela pourrait suffire.

Enfin, vous avez mentionné le verrouillage. Malheureusement, le verrouillage fait gravement défaut, au moins dans NFSv3 (je ne suis pas sûr de NFSv4, mais je parierais que ce n'est pas bon non plus.) Si vous envisagez de verrouiller, vous devriez examiner différents protocoles pour le verrouillage distribué, éventuellement hors bande avec le les copies de fichiers réelles, mais qui sont à la fois perturbatrices, complexes et sujettes à des problèmes tels que les blocages, donc je dirais qu'il vaut mieux être évité.


Pour plus d'informations sur le sujet de l'atomicité sur NFS, vous voudrez peut-être lire sur le format de boîte aux lettres Maildir , qui a été créé pour éviter les verrous et fonctionner de manière fiable même sur NFS. Il le fait en gardant des noms de fichiers uniques partout (de sorte que vous n'obtenez même pas un B final à la fin.)

Peut-être un peu plus intéressant dans votre cas particulier, le format Maildir ++ étend Maildir pour ajouter la prise en charge du quota de boîte aux lettres et le fait en mettant à jour atomiquement un fichier avec un nom fixe à l'intérieur de la boîte aux lettres (de sorte que cela pourrait être plus proche de votre B.) Je pense que Maildir ++ essaie à ajouter, ce qui n'est pas vraiment sûr sur NFS, mais il existe une approche de recalcul qui utilise une procédure similaire à celle-ci et elle est valide en tant que remplacement atomique.

Espérons que tous ces pointeurs vous seront utiles!

filbranden
la source
2

Vous pouvez écrire un programme pour cela.

Utilisez open(O_CREAT|O_RDWD)pour ouvrir le fichier cible, lire tous les octets et les métadonnées pour vérifier si le fichier cible est complet, sinon, il y a deux possibilités,

  1. Écriture incomplète

  2. Un autre processus exécute le même programme.

Essayez d'acquérir un verrou de description de fichier ouvert sur le fichier cible.

L'échec signifie qu'il y a un processus simultané, le processus actuel devrait exister.

Le succès signifie que la dernière écriture s'est écrasée, vous devez recommencer ou essayer de le réparer en écrivant dans le fichier.

Notez également que vous feriez mieux fsync()après avoir écrit dans le fichier cible avant de fermer le fichier et de libérer le verrou, ou tout autre processus pourrait lire des données pas encore sur le disque.

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

Ceci est important pour vous aider à faire la distinction entre un programme s'exécutant simultanément et une opération qui s'est finalement arrêtée.

炸鱼 薯条 德里克
la source
Merci pour l'info, je suis intéressé à implémenter cela moi-même et je vais essayer. Je suis surpris qu'il n'existe pas déjà dans le cadre d'un paquet coreutils / similaire!
Evan Benn
Cette approche ne peut pas répondre au fichier B non partiel ou corrompu laissé en place en cas de panne . Il est vraiment préférable d'utiliser l'approche standard de copier le fichier vers un nom temporaire, puis de le déplacer en place: le déplacement peut être atomique, ce qui ne peut pas être le cas.
reinierpost
@reinierpost En cas de plantage, mais les données ne sont pas entièrement copiées, les données partiellement copiées seront conservées quoi qu'il arrive. Mais mon approche va détecter cela et le corriger. Le déplacement d'un fichier ne peut pas être atomique, les données écrites sur le secteur physique croisé sur le disque ne seront pas atomiques, mais un logiciel (par exemple, le pilote du système de fichiers du système d'exploitation, cette approche) peut le corriger (si rw) ou signaler un état cohérent (si ro) , comme indiqué dans la section des commentaires de la question. La question concerne également la copie, pas le déplacement.
炸鱼 薯条 德里克
J'ai également vu O_TMPFILE, ce qui pourrait probablement aider. (et s'il n'est pas disponible sur le FS, devrait provoquer une erreur)
Evan Benn
@Evan avez-vous lu le document ou avez-vous déjà pensé pourquoi O_TMPFILE s'appuierait sur la prise en charge du système de fichiers?
炸鱼 薯条 德里克
0

Vous obtiendrez le résultat correct en faisant un cpensemble avec mv. Cela remplacera soit «B» par une nouvelle copie de «A», soit laissera «B» tel qu'il était auparavant.

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

mise à jour pour accueillir l'existant B:

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

Ce n'est pas 100% atomique, mais ça se rapproche. Il y a une condition de concurrence où deux de ces choses fonctionnent, les deux entrent dans le iftest en même temps, les deux voient que cela Bn'existe pas, puis exécutent tous les deux le mv.

kaan
la source
mv B.tmp B écrasera un B. préexistant B. cp A B.tmp écrasera un B.tmp préexistant, les deux échecs.
Evan Benn
mv B.tmp Bne s'exécutera que s'il cp A B.tmps'exécute et renvoie un code de résultat de réussite. comment est-ce un échec? aussi, je suis d'accord que cp A B.tmpcela remplacerait un existant B.tmpqui est ce que vous voulez faire. Les &&garanties que la 2e commande s'exécutera si et seulement si la première se termine normalement.
kaan
Dans la question, le succès est défini comme n'écrasant pas le fichier préexistant B. L'utilisation de B.tmp est un mécanisme, mais ne doit pas non plus écraser un fichier préexistant.
Evan Benn
J'ai mis à jour ma réponse. En fin de compte, si vous avez besoin d'une atomicité à 100% lorsque des fichiers peuvent exister ou non, et de plusieurs threads, vous avez besoin d'un seul verrou exclusif quelque part (créez un fichier spécial, ou utilisez une base de données, ou ...) que tout le monde suit dans le cadre de la copier / déplacer le processus.
kaan
Cette mise à jour écrase toujours B.tmp et a une condition de concurrence entre le test et le mv. Oui, le but est de faire les choses correctement, pas assez bien, espérons-le. D'autres réponses montrent pourquoi les verrous et les bases de données ne sont pas nécessaires.
Evan Benn