Come copiare un file in modo transazionale?


9

Voglio copiare un file da A a B, che potrebbe trovarsi su diversi filesystem.

Ci sono alcuni requisiti aggiuntivi:

  1. La copia è tutto o niente, nessun file parziale o corrotto B è rimasto in posizione in caso di arresto anomalo;
  2. Non sovrascrivere un file esistente B;
  3. Non competere con un'esecuzione simultanea dello stesso comando, al massimo si può avere successo.

Penso che questo si avvicini:

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

Ma 3. viene violato dal cp che non fallisce se esiste B.part (anche con -n flag). Successivamente 1. potrebbe non riuscire se l'altro processo "vince" il cp e il file collegato in posizione è incompleto. B.part potrebbe anche essere un file non correlato, ma sono felice di fallire senza provare altri nomi nascosti in quel caso.

Penso che bash noclobber aiuti, funziona completamente? C'è un modo per ottenere senza il requisito della versione bash?

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

Seguito, so che alcuni file system falliranno comunque (NFS). C'è un modo per rilevare tali filesystem?

Alcune altre domande correlate ma non esattamente le stesse:

Spostamento atomico approssimativo tra i file system?

MV Atomic è sul mio FS?

c'è un modo per spostare atomicamente file e directory da tempfs alla partizione ext4 su eMMC

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


2
Sei preoccupato solo per l'esecuzione simultanea dello stesso comando (cioè potrebbe essere sufficiente il blocco all'interno del tuo strumento) o anche per altre interferenze esterne con i file?
Michael Homer,

3
"Transazionale" potrebbe essere migliore
muru

1
@MichaelHomer all'interno dello strumento è abbastanza buono, penso che fuori renderebbe le cose molto difficili! Se è possibile con i blocchi dei file, però ...
Evan Benn,

1
@marcelm mvsovrascriverà un file esistente B. mv -nnon avviserà che non è riuscito. ln(1)( rename(2)) fallirà se B esiste già.
Evan Benn,

1
@EvanBenn Ottimo punto! Avrei dovuto leggere meglio le tue esigenze. (Tendo a necessitare di aggiornamenti atomici di un obiettivo esistente, e stavo rispondendo con questo in mente)
marcelm

Risposte:


11

rsyncfa questo lavoro. Un file temporaneo viene O_EXCLcreato per impostazione predefinita (disabilitato solo se si utilizza --inplace) e quindi renamedsul file di destinazione. Utilizzare --ignore-existingper non sovrascrivere B se esiste.

In pratica, non ho mai avuto problemi con questo su montaggi ext4, zfs o persino NFS.


rsync probabilmente lo fa bene, ma la pagina man estremamente complicata mi fa paura. opzioni che implicano altre opzioni, essendo incompatibili tra loro ecc.
Evan Benn,

Rsync non aiuta con il requisito n. 3, per quanto ne so. Tuttavia, è uno strumento fantastico e non dovresti rifuggire da un po 'di lettura delle pagine man. Puoi anche provare github.com/tldr-pages/tldr/blob/master/pages/common/rsync.md o cheat.sh/rsync . (tldr e cheat sono due diversi progetti che mirano ad aiutare con il problema che hai affermato, vale a dire, "man page is TL; DR"; sono supportati molti comandi comuni e vedrai gli usi più comuni mostrati.
sitaram

@EvanBenn rsync è uno strumento straordinario e vale la pena imparare! La sua pagina man è complicata perché è così versatile. Non essere intimidito :)
Josh,

@sitaram, # 3 potrebbe essere risolto con un file pid. Una piccola sceneggiatura come nella risposta qui .
Robert Riedl,

2
Questa è la risposta migliore Rsync è lo standard del settore per i trasferimenti di file atomici e in varie configurazioni può soddisfare tutte le vostre esigenze.
wKavey,


4

Hai chiesto di NFS. È probabile che questo tipo di codice si interrompa in NFS, poiché il controllo per noclobbercoinvolge due operazioni NFS separate (verifica se il file esiste, crea un nuovo file) e due processi da due client NFS separati possono entrare in una condizione di competizione in cui entrambi hanno esito positivo ( entrambi verificano che B.partnon esista ancora, quindi entrambi procedono a crearlo correttamente, di conseguenza si sovrascrivono a vicenda.)

Non c'è davvero da fare un controllo generico per verificare se il filesystem su cui stai scrivendo supporterà qualcosa di simile noclobberatomicamente o no. È possibile verificare il tipo di filesystem, sia esso NFS, ma sarebbe euristico e non necessariamente una garanzia. I filesystem come SMB / CIFS (Samba) potrebbero soffrire degli stessi problemi. I filesystem esposti attraverso FUSE possono o meno comportarsi correttamente, ma ciò dipende principalmente dall'implementazione.


Un approccio forse migliore è quello di evitare la collisione nel B.partpassaggio, utilizzando un nome file univoco (attraverso la cooperazione con altri agenti) in modo da non dover dipendere noclobber. Ad esempio, potresti includere, come parte del nome file, il tuo nome host, PID e un timestamp (+ possibilmente un numero casuale.) Dal momento che dovrebbe esserci un singolo processo in esecuzione sotto un PID specifico in un host in qualsiasi momento, questo dovrebbe garantire unicità.

Quindi uno dei due:

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.

O:

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"

Quindi se hai una condizione di competizione tra due agenti, entrambi procederanno con l'operazione, ma l'ultima operazione sarà atomica, quindi B esiste con una copia completa di A o B non esiste.

Puoi ridurre le dimensioni della gara controllando di nuovo dopo la copia e prima dell'operazione mvo ln, ma c'è ancora una piccola condizione di gara. Ma, indipendentemente dalle condizioni della razza, il contenuto di B dovrebbe essere coerente, supponendo che entrambi i processi stiano cercando di crearlo da A (o una copia da un file valido come origine).

Si noti che nella prima situazione con mv, quando esiste una gara, l'ultimo processo è quello che vince, poiché rename (2) sostituirà atomicamente un file esistente:

Se newpath esiste già, verrà sostituito atomicamente, in modo che non vi sia alcun punto in cui un altro processo che tenta di accedere a newpath lo trovi mancante. [...]

Se newpath esiste ma l'operazione non riesce per qualche motivo, rename()garantisce di lasciare un'istanza di newpath in atto.

Pertanto, è possibile che i processi che consumano B al momento possano vedere versioni diverse di esso (inode diversi) durante questo processo. Se gli autori stanno solo provando a copiare lo stesso contenuto e i lettori stanno semplicemente consumando il contenuto del file, ciò potrebbe andare bene, se ottengono inode diversi per file con lo stesso contenuto, saranno felici lo stesso.

Il secondo approccio usando un hard link sembra migliore, ma ricordo di aver fatto esperimenti con hardlink in un circuito stretto su NFS da molti client concorrenti e contare il successo e sembra che ci siano ancora delle condizioni di gara lì, dove sembra che se due client emettano un hardlink operazione allo stesso tempo, con la stessa destinazione, entrambi sembravano avere successo. (È possibile che questo comportamento fosse correlato alla particolare implementazione del server NFS, YMMV.) In ogni caso, è probabilmente lo stesso tipo di condizione di competizione, in cui potresti finire per ottenere due inode separati per lo stesso file nei casi in cui è pesante concorrenza tra scrittori per innescare queste condizioni di gara. Se i tuoi autori sono coerenti (entrambi copiano da A a B) e i tuoi lettori ne consumano solo i contenuti, ciò potrebbe essere sufficiente.

Infine, hai menzionato il blocco. Sfortunatamente il blocco è gravemente carente, almeno in NFSv3 (non sono sicuro di NFSv4, ma scommetto che non va bene neanche.) Se stai considerando il blocco, dovresti esaminare diversi protocolli per il blocco distribuito, possibilmente fuori banda con il copie effettive dei file, ma questo è sia dirompente, complesso e soggetto a problemi come deadlock, quindi direi che è meglio evitare.


Per ulteriori informazioni sul tema dell'atomicità su NFS, potresti voler leggere sul formato della cassetta postale di Maildir , che è stato creato per evitare blocchi e funzionare in modo affidabile anche su NFS. Lo fa mantenendo nomi di file univoci ovunque (quindi alla fine non ottieni nemmeno una B finale).

Forse un po 'più interessante per il tuo caso particolare, il formato Maildir ++ estende Maildir per aggiungere il supporto per la quota della cassetta postale e lo fa aggiornando atomicamente un file con un nome fisso all'interno della cassetta postale (quindi potrebbe essere più vicino al tuo B.) Penso che Maildir ++ ci provi aggiungere, che non è davvero sicuro su NFS, ma esiste un approccio di ricalcolo che utilizza una procedura simile a questa ed è valido come sostituto atomico.

Spero che tutti questi suggerimenti siano utili!


2

Puoi scrivere un programma per questo.

Utilizzare open(O_CREAT|O_RDWD)per aprire il file di destinazione, leggere tutti i byte e i metadati per verificare se il file di destinazione è completo, in caso contrario, ci sono due possibilità,

  1. Scrittura incompleta

  2. Altro processo sta eseguendo lo stesso programma.

Prova ad acquisire un blocco descrizione file aperto sul file di destinazione.

Fallimento significa che c'è un processo simultaneo, il processo corrente dovrebbe esistere.

Successo significa che l'ultima scrittura è andata in crash, dovresti ricominciare o provare a risolverlo scrivendo nel file.

Si noti inoltre che sarebbe meglio fsync()dopo aver scritto nel file di destinazione prima di chiudere il file e rilasciare il blocco, altrimenti un altro processo potrebbe leggere dati non ancora su disco.

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

Questo è importante per aiutarti a distinguere tra un programma in esecuzione simultaneamente e un'operazione che ha subito un arresto anomalo.


Grazie per le informazioni, sono interessato a implementarlo da solo e ci proverò. Sono sorpreso che non esista già come parte di alcuni pacchetti coreutils / simili!
Evan Benn,

Questo approccio non è in grado di soddisfare il file B parziale o corrotto lasciato sul posto in caso di crash . È davvero meglio usare l'approccio standard di copiare il file su un nome temporaneo, quindi spostarlo in posizione: lo spostamento può essere atomico, mentre la copia non può essere.
reinierpost,

@reinierpost In caso di arresto anomalo, ma i dati non vengono copiati completamente, i dati parzialmente copiati verranno lasciati, qualunque cosa accada. Ma il mio approccio lo rileverà e lo risolverà. Lo spostamento di un file non può essere atomico, tutti i dati scritti sul disco tra settori diversi non saranno atomici, ma il software (ad es. Driver del filesystem OS, questo approccio) può risolverlo (se rw) o segnalare uno stato coerente (se ro) , come menzionato nella sezione commenti della domanda. Anche la domanda riguarda la copia, non lo spostamento.
炸鱼 薯条 德里克

Ho anche visto O_TMPFILE, che probabilmente aiuterebbe. (e se non disponibile su FS, dovrebbe causare un errore)
Evan Benn,

@Evan hai letto il documento o hai mai pensato al motivo per cui O_TMPFILE si sarebbe affidato al supporto del filesystem?
炸鱼 薯条 德里克

0

Otterrai il risultato corretto facendo un cpinsieme a mv. Questo sostituirà "B" con una nuova copia di "A" o lascerà "B" come prima.

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

aggiornamento per accogliere esistente B:

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

Questo non è atomico al 100%, ma si avvicina. C'è una condizione di gara in cui due di queste cose sono in esecuzione, entrambe entrano nel iftest contemporaneamente, entrambe vedono che Bnon esiste, quindi entrambe eseguono il mv.


mv B.tmp B sovrascriverà un B. cp preesistente Un B.tmp sovrascriverà un B.tmp preesistente, entrambi i guasti.
Evan Benn,

mv B.tmp Bnon verrà eseguito a meno che non venga eseguito per la cp A B.tmpprima volta e restituisca un codice risultato di successo. come è un fallimento? inoltre, sono d'accordo che cp A B.tmpsovrascriverebbe un esistente B.tmpche è quello che vuoi fare. Le &&garanzie che il 2 ° comando eseguito se e solo se il primo viene completata normalmente.
Kaan,

Nella domanda il successo è definito come non sovrascrivere il file preesistente B. L'uso di B.tmp è un meccanismo, ma non deve sovrascrivere alcun file preesistente.
Evan Benn,

Ho aggiornato la mia risposta. In definitiva, se è necessaria un'atomicità completa al 100% quando i file possono esistere o meno e più thread, è necessario un singolo blocco esclusivo da qualche parte (creare un file speciale o utilizzare un database o ...) che tutti seguono come parte del processo di copia / spostamento.
Kaan,

Questo aggiornamento sovrascrive ancora B.tmp e presenta una condizione di competizione tra il test e il mv. Sì, il punto è fare le cose correttamente non all'incirca forse abbastanza bene, si spera. Altre risposte mostrano perché non sono necessari blocchi e database.
Evan Benn,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.