名前、inode、そしてリンク
一言でいうと
Linux でファイル名は実体ではなく、実体を指すただの参照にすぎません。今日学ぶことのすべては、この一文から導かれます。
なぜ必要なのか
運用中のサーバーで、ログがディスクを使い切りました。rm で大きなログファイルを削除したのに、df の空き容量は1バイトも増えません。誰もが一度は経験し、一度は慌てます。
ここで「rm が効かなかった」と結論づけると、その夜は長くなります。実際に起きたのは次のことです。rm はファイルを消すコマンドではなく、ディレクトリから名前をひとつ外す(unlink)コマンドです。データブロックが解放されるには、2つの条件が同時に成り立つ必要があります。
- その実体を指す名前がひとつも残っておらず
- その実体を開いているプロセスもひとつもないとき
ログを書いていたアプリケーションがまだそのファイルを開いていると、2つ目の条件が崩れます。名前は消えてもデータは生きており、du はパスをたどって数えるため、名前のないそのファイルを永遠に見つけられません。df と du が食い違う瞬間が、この状況のサインです。
どう動くのか
3つを区別する必要があります。
| 概念 | 何か | 名前を持っているか |
|---|---|---|
| inode | ファイルの実体。サイズ・権限・所有者・時刻・リンク数・データブロックの位置 | 持っていない |
| ディレクトリエントリ | (名前, inode 番号) の組 | 名前はここにだけある |
| ファイルディスクリプタ | プロセスが開いたファイルを指す番号 | 持っていない |
ディレクトリは結局、この組のリストにすぎません。だから次のことがすべて自然に説明できます。
- ハードリンク: 同じ inode に名前をもうひとつ付けたもの。
ls -liで見ると inode 番号が同じで、リンク数は2です。元の名前を消しても別の名前が残っているので、データはそのままです。ただし inode 番号はファイルシステムの中でしか一意ではないため、ハードリンクは同じファイルシステムの中でしか作れず、ディレクトリには張れません。 - シンボリックリンク: パス文字列を持つ別のファイル。inode が異なり、元が消えると壊れます。そのかわり、ファイルシステムをまたげます。
- 名前の変更(mv): 同じファイルシステムの中では、データをまったく移動しません。ディレクトリエントリを書き換えるだけです。そのため、1GB のファイルを移しても一瞬で終わります。
現場での姿
1つ目は、削除したログの復旧です。 プロセスがまだ開いていれば、/proc/PID/fd の下にそのファイルへのシンボリックリンクが生きています。ls -l /proc/1234/fd | grep deleted で探し、cp /proc/1234/fd/7 /backup/recovered.log を実行すれば、内容をそっくり救えます。ここで最も重要なのは順序です。プロセスを再起動した瞬間に、復旧のチャンスも一緒に消えます。 この状況で最初にすべきことは、再起動ではなくコピーです。
2つ目は、世代バックアップの容量爆発です。 rsync の --link-dest は、変更のないファイルをハードリンクでつなぎ、世代バックアップを安く作ります。ところが、そのバックアップディレクトリをハードリンクを知らないツールでコピーすると、容量が数倍に跳ね上がります。rsync -aH のようにリンクを保存するオプションを明示する必要があり、-H は -a に含まれません。
3つ目は、inode の枯渇です。 ext4 はフォーマット時に inode の数を固定し、あとから増やせません。セッションファイルやメールキューが小さなファイルを数百万個作ると、ブロックが残っていても inode が先に尽きて No space left on device になります。そのため、容量アラームは df -h と df -i の両方に設定する必要があります。
ログがディスクを食い尽くさない構造
先ほどの事故を経験したあとにすべきことは、そのファイルを復旧して終わりではありません。同じことが二度と起きないようにすることが本題で、ここには名前と実体が分離しているという性質がそのまま使われます。
ログのローテーションには2つの方式があり、先ほどの事故はそのうち一方を誤って使ったときに起きます。
移動してシグナルを送る。 ファイル名を変更し(同じファイルシステムなので一瞬で終わります)、アプリケーションに「ログファイルを開き直せ」というシグナルを送ります。アプリケーションが新しいファイルを開くと、古いファイルへの最後の参照が消え、領域が回収されます。シグナルを送らない場合や、アプリケーションがそのシグナルを処理しない場合は古いファイルに書き続けることになり、それが先ほど見た「消したのに減らない」状況です。
コピーして空にする。 内容を別のファイルにコピーしたあと、元のファイルを長さ0に切り詰めます。ファイルディスクリプタがそのまま維持されるので、アプリケーションには手を触れなくて済みます。そのかわり、コピーと切り詰めの間に書かれたログが失われることがあり、大きなファイルではその瞬間にディスク使用量が一時的に2倍になります。
どちらが正しいかはアプリケーションが決めます。 シグナルを処理するプログラムなら前の方式がよく、処理しないなら後の方式しかありません。ローテーション設定を使うときにこの判断をせず、デフォルトのまま置いておくことが、事故の出発点です。
そして最近のコンテナ環境では、アプリケーションがそもそもファイルに書かない方式のほうが一般的です。標準出力にだけ出力し、ローテーションと保存はランタイムと収集ツールが担当する構造です。こうするとローテーションのシグナルの問題は丸ごと消えますが、ランタイムのローテーション設定を確認しないと、ノードのディスクがログで埋まる同じ事故が、階層を変えて再び起きます。どの構造でも、誰が消すことになっているかを知っておくことが核心です。
次のラボですること
/root/work の下に作業スペースを作り、ハードリンクとシンボリックリンクを自分で張って、inode 番号とリンク数がどう変わるかを目で確かめます。最後に、ログディレクトリをまるごとアーカイブにまとめます。