renameの原子的な置換、名前の切替と保存の完了を分けて考える

桃色のアーチを淡い緑の板が通り、隣に紫の板、離れたくぼみに白い厚い板が置かれた抽象的な構図。

設定ファイルの更新中に、別の処理が途中までの内容を読んでしまう。これを避ける方法の一つが、別のファイルを完成させてから、renameで名前を切り替えることです。ただし、名前の置換が一度に見えることと、停電後にも変更が残ることは別の条件です。Linuxのrenameとfsyncを、読み手の視点から整理します。

簡単にまとめると?

  • renameによる既存の名前の置換は原子的に行われ、その名前が一時的に消える間を作りません。別ファイルに完成させた内容へ、名前からの参照を切り替える用途に使えます。
  • 置換前に開いていたファイル記述子は、新しいファイルへ自動で移りません。同じ名前を開き直す読み手と、既存の参照を使い続ける読み手を分けます。
  • 原子的な名前の切替だけで、永続化まで確認したことにはなりません。ファイルとディレクトリの同期、失敗の扱い、ファイルシステムの条件を別に確認します。

途中の内容を見せる更新と、完成後に切り替える更新

例として、読み手がsettingsという名前から設定を読み、書き手が内容をOLDからNEWへ更新する場面を考えます。書き手がsettingsそのものを空にしてから書き直せば、途中で開いた読み手が空の内容や未完成の内容を見る余地があります。書き手の処理が数行で終わることと、他の処理から一回の変更に見えることは同じではありません。

別の方法は、同じファイルシステム上の一時ファイルへNEWを用意し、その内容を検査してから、renameでsettingsを置き換えることです。Linuxのrenameでは、置換先の名前が既にある場合、その置換は原子的に行われます。他のプロセスが置換先へアクセスするときに、削除してから作り直すような「名前が存在しない間」を設けません。

ここで原子的とは、操作を細かく分けた途中状態を、対象とする名前の置換として見せないという意味です。ファイルの作成・内容の書込み・複数の設定の更新・保存装置への同期まで、全工程が一つの取引になるという意味ではありません。

名前は変わっても、開いていた参照は変わらない

ファイル名と、プロセスが開いたファイルへの参照を分けると、切替後の動作が理解しやすくなります。読み手Aが更新前にsettingsを開いていたとします。別のファイルをsettingsへrenameしても、Aが持っていた参照が新しいファイルへ付け替わるわけではありません。

一方、読み手Bが置換後にsettingsを新しく開けば、置換後の対象を参照します。したがって、名前の置換に成功した直後でも、AとBが異なる版を読む状況はあり得ます。これは名前の切替が失敗した証拠ではなく、読み手が参照を取得した時点の違いです。

名前settingsは旧ファイルから新ファイルへ切り替わる。切替前に開いた読み手Aは旧ファイルを参照し続け、切替後に開く読み手Bは新ファイルを参照する。保存装置への同期確認は別工程。
名前の対応と開いた参照を分けた独自モデル。実機の障害試験ではない。

本稿のモデルでは、旧ファイルの内容をOLD、新ファイルをNEWと固定し、Aの参照を保持したまま名前の対応だけを変更しています。Aの読取りはOLD、名前から新たに参照を取得するBはNEWとなります。旧ファイルを別の書き手が変更し続ける場合まで、Aの内容が固定されると保証するモデルではありません。

常駐する処理に新しい設定を使わせたいなら、名前の切替に加えて、いつ開き直すか、いつ再読込みするかを決める必要があります。既存の参照を使う処理がすべて自動で最新版へ移る、と考えると、変更が反映されない原因を見誤ります。

fsyncで確認するものは、名前の見え方とは異なる

アプリケーションから読める変更が、既に永続的な保存先へ届いたとは限りません。fsyncは、指定したファイルの変更されたデータと関連するメタデータを同期し、装置が完了を報告するまで待つための呼出しです。名前の対応を切り替えるrenameとは、確かめていることが違います。

さらに、ファイルをfsyncしただけで、そのファイルを含むディレクトリのエントリまで保存されたと必ず言えるわけではありません。Linuxのマニュアルは、このためにはディレクトリのファイル記述子に対するfsyncも必要だと説明しています。ファイルの中身と、そのファイルをどの名前で探せるかには、別の保存対象があります。

設計を点検する際は、「新しい内容を完成させた」「対象名を切り替えた」「必要な同期の完了を確認した」を別々の欄にすると、どこまで済んだかを追いやすくなります。同期呼出しがエラーを返したなら、直前に読取りが成功したことだけを根拠に、保存完了と扱わないことも重要です。

これは、どの装置でも同じ手順だけで完全な耐障害性を保証できる、という説明ではありません。使うファイルシステム、マウント条件、装置が返す完了の意味、ディレクトリをまたぐ操作、エラー時の復旧方針を含めて確認します。本稿では電源断やクラッシュを発生させる試験を行っていません。

別のファイルシステムへ移す場合は、同じ操作にならない

renameは、異なるマウントされたファイルシステム間ではEXDEVで失敗します。そのときコピーしてから元を消す処理へ切り替えたとしても、それをrenameと同じ原子的な名前の置換とは呼べません。コピー途中に何が見えるか、失敗時にどちらが残るかを、別の操作として設計する必要があります。

また、NFSでは、サーバー側でrenameが行われた後の障害などによって、失敗応答だけから「名前の切替は実行されなかった」と判断できない場合があります。結果が不明な操作を単純に繰り返す前に、現在の名前と内容を照合する考え方が必要です。ローカルの単純な例を、通信を挟む保存先へそのまま広げないようにします。

更新方式を比べるための三つの質問

第一に、他の読み手がどの名前から対象を取得するか。第二に、既に開いた参照をいつ手放すか。第三に、どの時点を保存完了と扱い、失敗をどう記録するか。この三つを分けると、「更新できた」という一言を、具体的な条件へ置き換えられます。

原子的な切替は、読み手へ途中の名前の状態を見せないために役立ちます。永続化の確認は、障害後に何を回復できるかを考えるために必要です。両者を同じ保証として扱わず、必要な性質を一つずつ組み合わせて更新方式を選びます。

出典・参考資料