web-dev-qa-db-ja.com

ファイルをトランザクションでコピーする方法は?

AからBにファイルをコピーしたいのですが、ファイルシステムが異なる場合があります。

追加の要件がいくつかあります。

  1. コピーはオールオアナッシングであり、クラッシュ時にファイルBの一部または破損は残りません。
  2. 既存のファイルBを上書きしないでください。
  3. 同じコマンドの同時実行と競合しないでください。成功するのは1つだけです。

私はこれが近づくと思います:

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

しかし3.は、B.partが存在する場合でも(-nフラグを使用しても)失敗しないcpに違反しています。その後、他のプロセスがcpに「勝ち」、リンクされたファイルが不完全な場合、1。が失敗する可能性があります。 B.partも無関係のファイルである可能性がありますが、その場合は他の隠しファイル名を試さなくても失敗します。

私はbash noclobberが役立つと思いますが、これは完全に機能しますか? bashバージョンの要件なしで取得する方法はありますか?

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

フォローアップ、とにかくこれでいくつかのファイルシステムが失敗することを知っています(NFS)。そのようなファイルシステムを検出する方法はありますか?

他のいくつかの関連するがまったく同じではない質問:

ファイルシステム間でのアトミック移動の概算?

mvは私のfsにアトミックですか?

eMMC上のtempfsからext4パーティションにファイルとディレクトリをアトミックに移動する方法はありますか

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

9
Evan Benn

rsyncがこの仕事をします。一時ファイルはO_EXCLはデフォルトで作成されます(--inplace)、次にrenamedをターゲットファイルに上書きします。使用する --ignore-existingが存在する場合、Bを上書きしません。

実際には、ext4、zfs、さらにはNFSマウントでこの問題が発生することはありません。

11
Hermann

心配しないでください noclobberは標準機能です

4
ilkkachu

あなたはNFSについて尋ねました。 noclobberのチェックには2つの別々のNFS操作(ファイルが存在するかどうかの確認、新しいファイルの作成)が含まれ、2つの別々のNFSクライアントからの2つのプロセスが競合状態になる可能性があるため、この種のコードはNFSで壊れる可能性があります。両方が成功した場合(両方が_B.part_がまだ存在しないことを確認してから、両方が正常に作成を続行し、その結果、お互いが上書きされます。)

書き込んでいるファイルシステムがnoclobberのようなものをアトミックにサポートするかどうかについて、一般的なチェックを実際に行う必要はありません。 NFSであるかどうかにかかわらず、ファイルシステムのタイプを確認できますが、これはヒューリスティックであり、必ずしも保証されるものではありません。 SMB/CIFS(Samba)などのファイルシステムでも同じ問題が発生する可能性があります。 Fuseを介して公開するファイルシステムは正しく動作する場合と動作しない場合がありますが、それは主に実装に依存します。


おそらくより良いアプローチは、noclobberに依存する必要がないように(他のエージェントとの連携により)一意のファイル名を使用することにより、_B.part_ステップでの衝突を回避することです。たとえば、ファイル名の一部として、ホスト名、PID、およびタイムスタンプ(おそらくランダムな番号)を含めることができます。ホストの特定のPIDで実行される単一のプロセスが常に存在するため、これは一意性を保証します。

したがって、次のいずれかになります。

_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.
_

または:

_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"
_

したがって、2つのエージェント間に競合状態がある場合、エージェントは両方とも操作を続行しますが、最後の操作はアトミックであるため、BがAの完全なコピーとともに存在するか、Bが存在しません。

コピー後、mvまたはln操作の前にもう一度チェックすることで、競合のサイズを縮小できますが、まだ小さな競合状態があります。ただし、競合状態に関係なく、両方のプロセスがA(またはOriginとして有効なファイルからのコピー)から作成しようとしていることを前提として、Bの内容は一貫している必要があります。

rename(2) が既存のファイルをアトミックに置き換えるため、mvの最初の状況では、競合が存在する場合、最後のプロセスが勝者となります。

newpathがすでに存在する場合は、アトミックに置き換えられるため、newpathにアクセスしようとする別のプロセスがアクセスするポイントはありません。それが欠けているのを見つけてください。 [...]

newpathが存在するが、何らかの理由で操作が失敗した場合、rename()は、newpathのインスタンスを場所。

したがって、その時点でBを消費しているプロセスでは、このプロセス中に異なるバージョンのiノード(異なるiノード)が表示される可能性があります。ライターがすべて同じコンテンツをコピーしようとしていて、リーダーが単にファイルのコンテンツを消費しているだけの場合、問題はないかもしれません。同じコンテンツのファイルに対して異なるiノードを取得しても、同じように満足します。

ハードリンクを使用した2番目のアプローチは見栄えが良いが、多くの同時クライアントからNFSのタイトループでハードリンクを使って実験を行い、成功を数えたが、まだいくつかのレースがあったようだそこでは、2つのクライアントが同じ宛先で同時にハードリンク操作を発行した場合に、両方が成功したように見えました。 (この動作は、特定のNFSサーバー実装であるYMMVに関連していた可能性があります。)いずれにせよ、それはおそらく同じ種類の競合状態であり、重いファイルがある場合、同じファイルに対して2つの別々のiノードを取得することになります。これらの競合状態をトリガーするライター間の同時実行性。ライターが一貫していて(どちらもAからBにコピーする)、リーダーがコンテンツのみを消費している場合は、それで十分かもしれません。

最後に、ロックについて説明しました。残念ながら、少なくともNFSv3ではロックが大幅に不足しています(NFSv4については不明ですが、それも良くないと思います)。ロックを検討している場合は、分散ロックの異なるプロトコルを調べてください。実際のファイルのコピーですが、それは混乱を招き、複雑であり、デッドロックなどの問題が発生しやすいので、回避することをお勧めします。


NFSのアトミック性に関する背景をさらに理解するには、ロックを回避し、NFSでも確実に機能するように作成された Maildirメールボックス形式 を参照することをお勧めします。それはどこにでもユニークなファイル名を保持することによってそうします(それであなたは最後に最後のBを得ることさえしません。)

おそらくあなたの特定のケースに少し興味があるかもしれません Maildir ++ format Maildirを拡張してメールボックスクォータのサポートを追加しますB.)私はMaildir ++が追記しようとしていると思いますが、これはNFSでは実際には安全ではありませんが、これと同様の手順を使用する再計算アプローチがあり、アトミックな置き換えとして有効です。

うまくいけば、これらすべてのポインタが役立つでしょう!

4
filbranden

このためのプログラムを書くことができます。

open(O_CREAT|O_RDWD)を使用してターゲットファイルを開き、すべてのバイトとメタデータを読み取って、ターゲットファイルが完全なものかどうかを確認します。そうでない場合は、2つの可能性があります。

  1. 不完全な書き込み

  2. 他のプロセスが同じプログラムを実行しています。

ターゲットファイルの開いているファイル説明ロックを取得してみてください。

失敗は、並行プロセスが存在することを意味し、現在のプロセスが存在する必要があります。

成功とは、最後の書き込みがクラッシュしたことを意味します。最初からやり直すか、ファイルに書き込んで修正してください。

また、ターゲットファイルに書き込んだ後、ファイルを閉じてロックを解放する前にfsync()を使用することをお勧めします。そうしないと、他のプロセスがまだディスク上にないデータを読み取る可能性があります。

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

これは、同時に実行されているプログラムと最後にクラッシュした操作を区別するのに役立ちます。

cpmvと組み合わせて実行すると、正しい結果が得られます。これにより、「B」が「A」の新しいコピーに置き換えられるか、「B」が以前と同じままになります。

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

既存のBに対応するための更新:

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

これは100%アトミックではありませんが、近づきます。これらの2つが実行されている競合状態があり、両方が同時にifテストに入り、両方がBが存在しないことを確認してから、両方がmvを実行します。

0
kaan