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 — 何が違うのか
なぜ、このような乖離が起きるのでしょうか。
原因を理解するには、df と du がそれぞれ何を見ているかを知る必要があります。
| コマンド | 何を見ているか | 削除済みファイル |
|---|---|---|
df | カーネルが管理するブロック使用量 | 含む(FD が残っていれば) |
du | ディレクトリツリーを辿って合算 | 含まない(辿れないため) |
特に df が削除済みファイルを含む理由は、Linux のファイル削除の仕組みにあります。
rm コマンドを実行すると、ディレクトリエントリの参照は削除されますが、データブロックは即座に解放されません。
データブロックが解放されるのは、そのファイルを開いているプロセスが 0 になったとき(参照カウント = 0)です。
つまり、ファイルを開いたまま rm すると次のような状態になります。
- ディレクトリエントリが消える →
duからは見えなくなる - データブロックはプロセスが FD(ファイルディスクリプタ:プロセスが開いているファイルを管理する番号)を保持している間は解放されない →
dfには残り続ける
この仕組みにより、現場では以下のような状況で乖離が発生します。
- ログローテーション後にアプリを再起動していない
tail -fやlessでログファイルを開いたまま削除した
3. 調査手順(実機ログ)
それでは実際に実機でこの動作を再現しながら、調査の流れを確認していきましょう。
Step 1:ベースラインを確認
まず比較の基準として df と du の初期値を記録します。今回は 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
df と du の両方に約 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 作成後 | 増加量 | |
|---|---|---|---|
df | 51G | 81G | +30G |
du | 50G | 80G | +30G |
増加量が一致しており、この時点では乖離は発生していません。
Step 3:ファイルを開き続けるプロセスを起動してから削除
tail -f でファイルを開いたまま(FD を保持したまま)rm を実行します。これが実際の現場でよく起きる「ログローテーション後にアプリが古いファイルを掴んだまま」の状況を再現しています。
末尾の & はコマンドをバックグラウンドで実行するシェルの記法です。tail -f はファイルを監視し続けるコマンドのため、& なしで実行するとターミナルが占有されます。バックグラウンドで動かすことでプロセスを生かしたまま次のコマンドを入力できます。
$ tail -f /nfs_share/testfile &
[1] 8470
$ rm /nfs_share/testfile
Step 4:乖離を確認
rm 後の df と du を確認します。
$ 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 合計 |
|---|---|---|
| ベースライン | 51G | 50G |
| testfile 作成後 | 81G | 80G |
| rm 後(FD あり) | 81G | 50G ← 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 の乖離 → 「見えないのに場所を取っているものがある」サインです
対応フロー
df と du の乖離を発見したときの対応手順を整理します。
df が高い・du と乖離
↓
lsof +L1 で削除済みファイルを掴んでいるプロセスを特定
↓
サービス再起動できるか?
├─ YES → systemctl restart <サービス名>
└─ NO → > /proc/<PID>/fd/<FD> でファイルを空にする
現場でよく起きるパターン
- ログローテーション後にアプリを
systemctl reload/restartしていない tail -fやlessでログを開いたままrmしてしまった








