python 3x and linux/macOS)に「レコードマネージャ」クラスを実装しようとしています。このクラスは比較的簡単で簡単です。私が望む唯一の「難しい」ことは、アクセスできることです複数のプロセスで同じファイル(結果が保存される場所)。
これは概念的には非常に簡単に見えました。保存するときに、ファイルの排他ロックを取得します。情報を更新し、新しい情報を保存して、ファイルの排他ロックを解除します。簡単です。
fcntl.lockf(file, fcntl.LOCK_EX)
を使用して排他ロックを取得しています。問題は、インターネットを見て、さまざまなWebサイトをたくさん見つけて、これが信頼できない、動作しないと言っていることです。 Windows、NFSでのサポートが不安定で、macOSとLinuxの間で状況が変わる可能性があることを示しています。
私はコードがWindowsでは機能しないことを受け入れましたが、macOS(単一のマシン)とLinux(NFSを備えた複数のサーバー)で機能することを望んでいました。
問題は、私がこれを機能させることができないように見えることです。しばらくデバッグした後、macOSでテストに合格した後、LinuxでNFSを試したところ、失敗しました(ubuntu 16.04)。問題は、複数のプロセスによって保存された情報間の不整合です。一部のプロセスには変更が欠落しているため、ロックと保存の手順で問題が発生しました。
何かがあると確信しています私は間違っています。これはオンラインで読んだ問題に関連しているのではないかと思います。では、NFSを介してmacOSとLinuxで動作する同じファイルへの複数のアクセスを処理する適切な方法は何ですか?
編集
これは、新しい情報をディスクに書き込む一般的な方法のようになります。
sf = open(self._save_file_path, 'rb+')
try:
fcntl.lockf(sf, fcntl.LOCK_EX) # acquire an exclusive lock - only one writer
self._raw_update(sf) #updates the records from file (other processes may have modified it)
self._saved_records[name] = new_info
self._raw_save() #does not check for locks (but does *not* release the lock on self._save_file_path)
finally:
sf.flush()
os.fsync(sf.fileno()) #forcing the OS to write to disk
sf.close() #release the lock and close
これは、ディスクから情報を読み取るだけの典型的な方法ですが、次のようになります。
sf = open(self._save_file_path, 'rb')
try:
fcntl.lockf(sf, fcntl.LOCK_SH) # acquire shared lock - multiple writers
self._raw_update(sf) #updates the records from file (other processes may have modified it)
return self._saved_records
finally:
sf.close() #release the lock and close
また、_raw_saveは次のようになります。
def _raw_save(self):
#write to temp file first to avoid accidental corruption of information.
#os.replace is guaranteed to be an atomic operation in POSIX
with open('temp_file', 'wb') as p:
p.write(self._saved_records)
os.replace('temp_file', self._save_file_path) #pretty sure this does not release the lock
エラーメッセージ
私は100の異なるプロセスを作成する単体テストを作成しました。50は読み取り、50は同じファイルに書き込みます。各プロセスは、ファイルへの順次アクセスを回避するために、ランダムに待機します。
問題は、一部のレコードが保持されないことです。最後に3〜4個のランダムなレコードが欠落しているので、50ではなく46〜47個のレコードしかありません。
編集2
上記のコードを変更し、ファイル自体ではなく、別のロックファイルでロックを取得します。これにより、ファイルを閉じるとロックが解除され(@jannebで提案されているように)、問題がコードがMacで正しく機能するようになります。ただし、NFSを使用するLinuxでは同じコードが失敗します。
ファイルロックとos.replace()の組み合わせがどのように意味をなすかわかりません。ファイルが置き換えられると(つまり、ディレクトリエントリが置き換えられます)、既存のすべてのファイルロック(おそらく、ロックが成功するのを待っているファイルロックを含みます。ここでは意味がわかりません)とファイル記述子は、新しいファイルではなく古いファイル。これが、テストでいくつかのレコードを失う原因となる競合状態の背後にある理由だと思います。
os.replace()は、リーダーが部分的な更新を読み取らないようにするための優れた手法です。ただし、複数のアップデーターがある場合、堅牢に機能しません(一部のアップデートを失っても問題ない場合)。
別の問題は、fcntlが本当に本当に愚かなAPIであることです。特に、ロックはファイル記述子ではなくプロセスにバインドされます。つまり、たとえばファイルを指しているファイル記述子のclose()は、ロックを解放します。
1つの方法は、「ロックファイル」を使用することです。 link()の原子性を利用します。 http://man7.org/linux/man-pages/man2/open.2.html から:
ロックファイルを使用してアトミックファイルロックを実行し、O_EXCLのNFSサポートへの依存を回避する必要があるポータブルプログラムは、同じファイルシステム上に一意のファイルを作成し(たとえば、ホスト名とPIDを組み込む)、link(2)を使用してロックファイルへのリンク。 link(2)が0を返す場合、ロックは成功しています。それ以外の場合は、一意のファイルでstat(2)を使用して、リンク数が2に増加したかどうかを確認します。この場合、ロックも成功します。
Okで少し古いデータを読み取る場合、このlink()ダンスを使用できるのは、ファイルの更新時に使用する一時ファイルのみであり、その後os.replace()で「メイン」ファイルを使用します。読み取りに使用します(読み取りはロックレスにすることができます)。そうでない場合は、「メイン」ファイルのlink()トリックを実行し、共有/排他ロックを忘れる必要があります。すべてのロックは排他的です。
補遺:ロックファイルを使用する際に注意する必要があるのは、プロセスが何らかの理由で停止し、ロックファイルが残っている場合の対処です。これを無人で実行する場合は、ある種のタイムアウトとロックファイルの削除を組み込むことができます(たとえば、stat()タイムスタンプを確認します)。
ランダムに名前が付けられたハードリンクを使用し、それらのファイルのロックカウントをロックファイルとして使用することは一般的な戦略であり(例 this )、lockd
を使用するよりも議論の余地がありますが、制限の詳細についてはNFSを介したあらゆる種類のロックのこれを読む: http://0pointer.de/blog/projects/locking.html
また、これは、NFS経由でMbox
ファイルを使用するMTAソフトウェアの標準的な長年の問題であることがわかります。おそらく最良の答えは、Maildir
の代わりにMbox
を使用することでしたが、postfixなどのソースコードの例を探すと、ベストプラクティスに近づきます。そして、彼らが単にその問題を解決しないのであれば、それもあなたの答えかもしれません。
NFSはファイル共有に最適です。それは「伝達」媒体として吸います。
NFS-for-data-transmissionの道を何度も行ってきました。いずれの場合も、ソリューションにはNFSからの移行が含まれていました。
信頼できるロックを取得することは問題の一部です。もう1つの部分は、サーバー上のファイルの更新であり、クライアントが特定の時点(ロックを取得する前など)にそのデータを受信することを期待しています。
NFSは、データ転送ソリューションとして設計されていません。キャッシュとタイミングが関係しています。ファイルコンテンツのページングとファイルメタデータ(atime属性など)は言うまでもありません。また、クライアントO/Sはローカルで状態を追跡します(ファイルの最後に書き込むときにクライアントのデータを追加する「どこ」など)。
分散した同期ストアの場合は、それを行うツールを検討することをお勧めします。 Cassandraなどの汎用データベースです。
ユースケースを正しく読んでいれば、単純なサーバーベースのソリューションを使用することもできます。サーバーにTCP=接続をリッスンさせ、接続からメッセージを読み取ってから、それぞれをファイルに書き込み、サーバー自体内で書き込みをシリアル化します。独自のプロトコルを使用すると、さらに複雑になります(知るためにメッセージが開始および停止する場所)ですが、それ以外の場合は、かなり単純です。