Linux のディスクフル調査:df と du の乖離が起きる理由と対処法

df -h/ が 100% になっているのに、du -ch /* で合計してもその数字に届かない——そんな不思議な状況に遭遇したことはありませんか?

原因は「削除済みなのにプロセスが開き続けているファイル」です。この記事では、乖離が起きる仕組みと lsof +L1 を使った特定・解消の手順を実機ログつきで解説します。


1. 症状

アラートメールでディスク使用率 100% の通知が届きました。df -h でディスクの状態を確認したところ、100% になっていることが確認できました。

# ディスクの実際の使用量を確認(OSが管理するブロック使用量)
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        50G   50G     0 100% /

次に du -ch /* で各ファイル・ディレクトリの容量を確認しましたが、確認できた合計サイズは 30GB しかありませんでした。

# ディレクトリツリーを辿って使用量を集計(見えているファイルの合計)
$ du -ch /*
4.0K    /bin
512M    /home
1.2G    /usr
800M    /var
...
30G     total

df が示す 50G と 20G の乖離 が生じています。

合計だけ確認したい場合は du -ch /* | tail -1 で total 行のみ取り出せます。ただし原因調査の段階では全体出力を見ておく方がよいです。


2. df と du — 何が違うのか

なぜ、このような乖離が起きるのでしょうか。
原因を理解するには、dfdu がそれぞれ何を見ているかを知る必要があります。

コマンド何を見ているか削除済みファイル
dfカーネルが管理するブロック使用量含む(FD が残っていれば)
duディレクトリツリーを辿って合算含まない(辿れないため)

特に df が削除済みファイルを含む理由は、Linux のファイル削除の仕組みにあります。

rm コマンドを実行すると、ディレクトリエントリの参照は削除されますが、データブロックは即座に解放されません。
データブロックが解放されるのは、そのファイルを開いているプロセスが 0 になったとき(参照カウント = 0)です。

つまり、ファイルを開いたまま rm すると次のような状態になります。

  • ディレクトリエントリが消える → du からは見えなくなる
  • データブロックはプロセスが FD(ファイルディスクリプタ:プロセスが開いているファイルを管理する番号)を保持している間は解放されない → df には残り続ける

この仕組みにより、現場では以下のような状況で乖離が発生します。

  • ログローテーション後にアプリを再起動していない
  • tail -fless でログファイルを開いたまま削除した

3. 調査手順(実機ログ)

それでは実際に実機でこの動作を再現しながら、調査の流れを確認していきましょう。

Step 1:ベースラインを確認

まず比較の基準として dfdu の初期値を記録します。今回は 50GB のダミーデータをあらかじめ用意しているため、乖離が起きたときの差分が視覚的にわかりやすくなっています。

$ df -h /nfs_share
ファイルシス                サイズ  使用  残り 使用% マウント位置
/dev/mapper/vg_nfs-lv_share   100G   51G   50G   51% /nfs_share

$ du -ch /nfs_share
0       /nfs_share/datapump
0       /nfs_share/rman
50G     /nfs_share
50G     合計

df が 51G、du が 50G と約 1G の差がありますが、これは XFS ファイルシステムのメタデータ(ジャーナル・AG 構造体など)によるものです。ユーザーデータの乖離ではなく正常な状態です。

Step 2:30GB のテストファイルを作成

dd コマンドで 30GB のファイルを作成します。

$ dd if=/dev/zero of=/nfs_share/testfile bs=1G count=30
30+0 レコード入力
30+0 レコード出力
32212254720 bytes (32 GB, 30 GiB) copied, 40.1229 s, 803 MB/s

dfdu の両方に約 30GB 増加していることを確認します。

$ df -h /nfs_share
ファイルシス                サイズ  使用  残り 使用% マウント位置
/dev/mapper/vg_nfs-lv_share   100G   81G   20G   81% /nfs_share

$ du -ch /nfs_share
0       /nfs_share/datapump
0       /nfs_share/rman
80G     /nfs_share
80G     合計
ベースラインtestfile 作成後増加量
df51G81G+30G
du50G80G+30G

増加量が一致しており、この時点では乖離は発生していません。

Step 3:ファイルを開き続けるプロセスを起動してから削除

tail -f でファイルを開いたまま(FD を保持したまま)rm を実行します。これが実際の現場でよく起きる「ログローテーション後にアプリが古いファイルを掴んだまま」の状況を再現しています。

末尾の & はコマンドをバックグラウンドで実行するシェルの記法です。tail -f はファイルを監視し続けるコマンドのため、& なしで実行するとターミナルが占有されます。バックグラウンドで動かすことでプロセスを生かしたまま次のコマンドを入力できます。

$ tail -f /nfs_share/testfile &
[1] 8470

$ rm /nfs_share/testfile

Step 4:乖離を確認

rm 後の dfdu を確認します。

$ df -h /nfs_share
ファイルシス                サイズ  使用  残り 使用% マウント位置
/dev/mapper/vg_nfs-lv_share   100G   81G   20G   81% /nfs_share

$ du -ch /nfs_share
0       /nfs_share/datapump
0       /nfs_share/rman
50G     /nfs_share
50G     合計

du はベースラインの 50G に戻りましたが、df は 81G のままです。30G の乖離が発生しています。

タイミングdf 使用量du 合計
ベースライン51G50G
testfile 作成後81G80G
rm 後(FD あり)81G50G ← 30G の乖離

Step 5:lsof +L1 で犯人を特定

乖離の原因となっているプロセスを lsof +L1 で特定します。

通常、ファイルが存在している状態では NLINK(ハードリンク数)は 1 以上です。rm を実行するとディレクトリエントリが削除されて NLINK が 0 になりますが、プロセスがファイルを開き続けている場合は inode がまだ残っています。+L1 は「NLINK が 1 未満(= 0)のファイルを開いているプロセスだけ」に絞り込むオプションで、これにより削除済みファイルを掴んでいる犯人プロセスを効率よく特定できます。

$ lsof +L1
COMMAND    PID USER   FD   TYPE DEVICE    SIZE/OFF NLINK NODE NAME
firewalld  814 root    8u   REG    0,1        4096     0    5 /memfd:libffi (deleted)
tail      8470 root    3r   REG  252,2 32212254720     0  134 /nfs_share/testfile (deleted)

tail(PID 8470)が /nfs_share/testfile を 30GB のまま保持していることが確認できました。NLINK=0 が rm 済みの証拠です。

別の確認方法(PID を直接指定して確認)

$ ls -l /proc/$(pgrep tail)/fd
lrwx------ 1 root root 64  ... 3 -> /nfs_share/testfile (deleted)

Step 6:プロセスを終了して空き容量が戻ることを確認

犯人プロセスを特定できたので kill で終了します。FD が解放されることでデータブロックが解放され、df の使用量がベースラインに戻るはずです。

$ kill 8470
[1]+  Terminated              tail -f /nfs_share/testfile

$ df -h /nfs_share
ファイルシス                サイズ  使用  残り 使用% マウント位置
/dev/mapper/vg_nfs-lv_share   100G   51G   50G   51% /nfs_share

$ du -ch /nfs_share
0       /nfs_share/datapump
0       /nfs_share/rman
50G     /nfs_share
50G     合計

$ lsof +L1
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NLINK NODE NAME
firewalld 814 root    8u   REG    0,1     4096     0    5 /memfd:libffi (deleted)

df の使用量がベースラインの 51G に戻り、lsof +L1 からも tail のエントリが消えたことを確認できました。


4. 再起動できない場合の応急処置

サービスを止められない場合は /proc 経由でファイルを空にする方法があります。

/proc はカーネルが管理する仮想ファイルシステムです。/proc/<PID>/fd/ 配下には、そのプロセスが開いているファイルディスクリプタへのシンボリックリンクが並んでいます。lsof +L1 で確認した PID と FD 番号を使ってファイルを空にすることで、プロセスを終了させずにブロックを解放できます。

各操作の違いを整理します。

操作プロセスFDデータブロック
rm のみ生きている保持したまま解放されない
> /proc/<PID>/fd/<FD>生きている保持したまま解放される
kill終了閉じる解放される

> /proc/<PID>/fd/<FD>rm ではなく、「何も出力しない(空)」をファイルに書き込むリダイレクト操作です。ファイルサイズを 0 バイトに切り詰めることでデータブロックを解放しますが、プロセスは FD を保持したまま動き続けます。

$ > /proc/8470/fd/3

注意: この操作後にプロセスがファイルに書き込もうとするとエラーが発生する可能性があります。本番環境ではサービス影響を確認してから実施してください。


5. よくある Q&A

Q: du では空きがあるのに書き込みできない——df が 100% だと本当に書き込めないのか?

はい、書き込めません。OS はブロックの確保を df の実際のブロック使用状況に基づいて判断します。du 上は空きがあっても、df が 100% であれば touch や書き込みは「No space left on device」になります。

Q: lsof | grep deleted との違いは?

lsof | grep deleted でも同じプロセスを見つけられます。+L1 との違いは絞り込みの方法で、+L1 はカーネルレベルでフィルタするため grep より高速です。大量のプロセスがいる環境では +L1 の方が出力がすっきりします。


6. まとめ

df と du の役割

  • df → ディスクの真の使用量(書き込めるかどうかの判断はこちら)
  • du → 見えているファイルの合計(どのディレクトリが何GB を占めているかの判断はこちら)
  • df と du の乖離 → 「見えないのに場所を取っているものがある」サインです

対応フロー

dfdu の乖離を発見したときの対応手順を整理します。

df が高い・du と乖離
    ↓
lsof +L1 で削除済みファイルを掴んでいるプロセスを特定
    ↓
サービス再起動できるか?
    ├─ YES → systemctl restart <サービス名>
    └─ NO  → > /proc/<PID>/fd/<FD> でファイルを空にする

現場でよく起きるパターン

  • ログローテーション後にアプリを systemctl reload/restart していない
  • tail -fless でログを開いたまま rm してしまった