ファイルシステム — 名前と実体を分離した設計
一言でいうと
Unixファイルシステムは、ファイルの実体(inode)と名前(ディレクトリエントリ)を分離しました。その決定1つが、ハードリンク、開いているファイルの削除、アトミックな置換といった動作のすべてを説明します。
なぜ必要なのか
ファイルには2種類の情報があります。内容と、メタデータ(サイズ、権限、所有者、更新時刻、データがどのブロックにあるか)です。そこに名前もあります。この3つを1つの塊にまとめると単純ですが、同じファイルを2つの名前で呼んだり、名前を変えたりすることが高価になります。
Unixの選択は、メタデータと内容はinodeに置き、名前はディレクトリに置くというものでした。ディレクトリは特別なファイルで、その内容は「名前 → inode番号」のペアの一覧です。
どう動くのか
inodeには、ファイルサイズ、権限、タイムスタンプ、リンク数、そしてデータブロックを指すポインターが入ります。ポインターの構造が面白いところです。前のほうの数個はデータブロックを直接指し、その次はポインターのブロックを指す単一間接、その次は二重、三重間接です。小さなファイルは直接ポインターだけで済むので速く、大きなファイルも表現できます。階層が深くなるほど、アクセスに必要なブロック読み込みが増えるという代償を払います。
この構造から、自然に導かれる結果があります。
ハードリンクは、同じinodeを指す名前をもう1つ作ることです。元のファイルとコピーの区別はなく、2つは完全に対等です。inodeのリンク数が1つ増えるだけです。
ファイルの削除は、実は名前を消すことです(そのため、システムコール名がunlinkです)。リンク数が0になり、そのファイルを開いているプロセスもなくなったときに、初めてブロックが回収されます。そのため、ログファイルを削除したのにディスクの空き容量が増えない状況が起こります。プロセスがまだそのファイルを開いているからで、該当のプロセスを再起動するか、ファイルディスクリプターを閉じると、容量が戻ります。
名前の変更(rename)は、同じファイルシステムの中ではディレクトリエントリだけを書き換えるので、アトミックです。設定ファイルを安全に置き換える標準的な方法が、ここから生まれます。一時ファイルに新しい内容を書き、fsyncでディスクに降ろしてから、renameで差し替えます。読む側は常に古い内容か新しい内容のどちらかを見て、半分だけ書かれたファイルは決して見ません。
ジャーナリングは、クラッシュに備える仕組みです。メタデータの変更を実際の位置に書く前にジャーナルへ先に記録しておけば、途中で電源が落ちても、再起動時にジャーナルを再生して一貫性を復旧できます。ext4のデフォルトモードは、メタデータだけをジャーナリングするorderedです。データまでジャーナリングすれば安全ですが、すべての書き込みが2回起こります。
現場での姿
dfは空きがあると言っているのに書き込みが失敗することがあります。原因は2つのうちの1つです。inodeが枯渇したか(df -iで確認)、削除したファイルをまだ誰かが開いていて容量が回収されていないかです。後者は、lsof +L1でリンク数が0の開いているファイルを見つけ出せます。ログローテーションの設定を誤って古いファイルを削除したのに、アプリケーションがそのディスクリプターに書き込み続けている状況が典型的です。
書き込みは本当にディスクに届いたのか
先ほど、設定ファイルを安全に置き換える手順にfsyncが入っていました。この1段階がなぜ必要なのかは、書き込みが通る層を知ると理解できます。
プログラムがwriteを呼ぶと、データはカーネルのページキャッシュに入り、その瞬間に呼び出しは成功として戻ります。まだディスクには何も行っていません。カーネルが後で適当なときに書き出しますが、その間に電源が落ちると、そのデータはなかったことになります。fsyncは「今すぐ書き出して、終わるまで待て」という要求です。
ここで、人がよく見落とすことがもう1つあります。ファイルの内容をfsyncしても、そのファイルの名前が安全になるわけではありません。新しいファイルを作って書いてfsyncした後で電源が落ちると、内容は残っているのにディレクトリエントリがなく、そのファイルにたどり着く方法がなくなることがあります。そのため、新しく作ったファイルやrenameの後には、そのディレクトリもあわせてfsyncする必要があります。データベースとログシステムがしているのは、まさにこれです。
そして層は、カーネルで終わりではありません。ディスク自体にも書き込みキャッシュがあり、デバイスが「書いた」と答えても、まだ揮発性のバッファーにあることがあります。ちゃんとした記憶装置はキャッシュフラッシュの命令に対応しており、ファイルシステムがそれを送りますが、安価なデバイスの中には、その命令を受け取っても何もせずに成功と答えるものがあります。そのようなデバイスの上では、どんなソフトウェアもクラッシュ安全性を保証できません。
実務の結論は単純です。失ってはいけないデータにはfsyncを使い、その代償として遅くなることを受け入れます。逆に、いつでも作り直せるキャッシュや一時ファイルには使いません。2種類を区別せずにすべてfsyncすると性能が崩れ、すべて省略すると、ある日電源が落ちたときに何が消えたのかもわからなくなります。
続くラボですること
ここに書かれたことをすべて、手で作ってみます。inodeで同じファイルかどうかを見分け、名前を外したファイルをそのまま読み出し、削除したまま容量をつかんでいるプロセスを見つけ出すスクリプトを作ります。設定ファイルを差し替えるスクリプトも作りますが、採点ツールがそのファイルを開いたまま、みなさんの差し替えを実行させて、読む側が半分だけ書かれたファイルを見るかどうかを確認します。