dfとduはなぜ違う答えを出すのか
一言でいうと
dfはファイルシステムに問い合わせ、duはディレクトリを歩き回って数えます。そのため、名前のないファイルは、dfには捕捉され、duには捕捉されません。
なぜ必要なのか
ログがディスクを食いつくしたので、大きなファイルをrmで消したのに、dfの空き容量が1バイトも増えません。誰もが一度は経験し、一度は慌てます。
ここで「rmが効かなかった」と結論づけると、その夜は長くなります。実際に起きたことは次のとおりです。rmはファイルを消すコマンドではなく、ディレクトリから名前を1つ外す(unlink)コマンドです。データブロックが返却されるには、2つの条件が同時に成り立つ必要があります。
- その実体を指す名前が1つも残っておらず
- その実体を開いているプロセスも1つもないとき
ログを書いていたアプリケーションがまだそのファイルを開いていれば、2つ目の条件が崩れます。名前は消えましたがデータは生きており、duはパスを歩いて数えるので、名前のないそのファイルを永遠に見つけられません。dfとduがずれる瞬間が、まさにこの状況のサインです。
どう動くのか
容量の問題を絞り込む順序は次のとおりです。
df -h # 블록 사용량
df -i # inode 사용량 (이게 100% 일 수도 있다)
du -x -h --max-depth=1 / | sort -h # 어느 디렉터리가 큰가 (-x: 다른 fs 로 안 넘어감)
lsof +L1 # 삭제됐지만 열려 있는 파일
ls -l /proc/<PID>/fd | grep deleted
find / -xdev -size +500M -printf '%s %p\n' | sort -rn | head
それぞれのツールが答える問いが違います。
| ツール | 問い | 見逃すもの |
|---|---|---|
df |
ファイルシステムにどれだけ残っているか | どこが食ったか |
df -i |
inodeはどれだけ残っているか | ブロックの使用量 |
du |
このパスの下がどれだけ大きいか | 名前のないファイル、別のファイルシステム |
lsof +L1 |
削除されたのに開かれているファイルがあるか | すでに閉じられたもの |
duには、落とし穴がさらに2つあります。
- ハードリンクは1回しか数えません。同じinodeを複数の名前が指していると、duは最初に出会った1つだけを計算します。世代バックアップのディレクトリのサイズを測るとき、この性質のために、値が予想より小さく出ます。
- スパースファイルは、実際の占有だけを数えます。
--apparent-sizeを指定すると、論理サイズが出ます。2つの値の違いが、そのままスパース領域です。
削除された開いているファイルの復旧
プロセスがまだ開いているなら、/proc/PID/fdの下に、そのファイルへのシンボリックリンクが生きています。
ls -l /proc/1234/fd | grep deleted
cp /proc/1234/fd/7 /backup/recovered.log
ここで順序がいちばん重要です。プロセスを再起動した瞬間に、最後の参照が消えてブロックが返却され、復旧の機会が完全になくなります。容量が急ぎの状況でも、最初の行動は再起動ではなくコピーであるべきです。
容量だけを急いで回収する必要があるなら、> /proc/1234/fd/7でファイルを空にできます。ログファイルなら、この方法が、プロセスを生かしたまま容量を返してくれます。
何がどれだけ占めているかを素早く絞り込む
容量の問題は時間との勝負です。全体をなめる代わりに、上から半分ずつ切り分けて降りていくと、たいてい3、4回で犯人が見つかります。
du -xh --max-depth=1 / 2>/dev/null | sort -rh | head
-xは、別のファイルシステムに移らないようにします。これを外すと、/proc、/sys、ネットワークマウントまで数えるので、時間がかかり、数字も間違います。--max-depth=1で1層ずつ降りながら、最も大きなディレクトリにcdすることを繰り返します。
大きなファイルが数個なのか、小さなファイルが数百万個なのかを、先に見分けます。処方が違います。
find /var -xdev -type f -size +100M -printf '%s\t%p\n' | sort -rn | head
find /var -xdev -type f | wc -l
前者が答えをくれれば、そのファイルを消すか移せば終わりです。後者の数字が数百万なら、inodeとディレクトリの探索が問題なので、消すこと自体に時間がかかります。
古いものから消します。rmに引数を渡しすぎるとArgument list too longが出るので、-deleteやxargsを使います。
find /var/log/app -xdev -type f -name '*.log' -mtime +14 -delete
find /var/cache/thumbs -xdev -type f -mtime +30 -print0 | xargs -0 -r rm -f
消す前に、何が使っているかを見ます。あるプロセスが書き続けているファイルを消すと、容量は戻らず、そのプロセスだけがおかしくなります(前のモジュールの「消したのに開かれているファイル」)。
lsof -nP +D /var/log/app 2>/dev/null | head
同じことが繰り返されないようにします。一度片づけて終わりにすると、必ずまたいっぱいになります。ログはlogrotateに任せ(copytruncateは、開いているfdを扱う安全装置です)、コンテナホストなら、docker system dfでイメージ・ボリューム・ビルドキャッシュを別々に見て、--filter until=で期間を決めて整理します。アラートは90%ではなく、増加の速度でかけるほうがよいです。1日に5%ずついっぱいになっているなら、2日後がいつなのかが分かり、その場合は明け方に起こされずにすみます。
現場での姿
inodeの枯渇です。セッションファイルやメールキューが小さなファイルを数百万個作ると、ブロックが残っていてもinodeが先に尽きます。df -iを見ないと、原因を見つけられません。そしてext4はフォーマット後にinodeを増やせないので、根本的な解決は、再フォーマットか、そのワークロードを別のファイルシステム(XFSは動的に割り当てます)に移すことです。
duが長くかかって、障害対応が遅れます。ファイルが数百万個あると、duは数分かかります。急ぐときは、du --max-depth=1で上から絞り込んでいくほうが速いです。
次のラボですること
小さなファイルを大量に作ってinodeの消費を観察し、スパースファイルの2種類のサイズを比べ、削除されたのに開かれているファイルを自分で作って、dfとduの不一致を再現します。