Skip to content

필사 모드: ファイルディスクリプタと inode 完全ガイド: df と du が食い違うとき何が起きているのか

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

はじめに

運用をしていると、常識に反して見える状況に出くわします。ログを消したのに df の使用量がそのままです。容量が 30 パーセント残っているのに、ファイルを作れないと言われます。ファイル名を変えただけなのに、アプリケーションが古いファイルに書き続けます。ulimit -n を上げたのに、相変わらず「Too many open files」が出ます。

四つとも、同じ二つの概念を知らないと説明がつきません。inode とファイルディスクリプタです。そしてこの二つを知れば、四つとも当然の結果に見えてきます。

この記事はファイルシステムの理論書ではなく、運用者のためのリファレンスです。概念は必要な分だけ説明し、残りはすべて「この状況で何を確認し、何を実行するか」に使います。

前提は Linux カーネル 5.x 以上、ext4 と XFS です。ファイルシステムの種類によって動作が異なる部分は、その都度表記します。特に inode 関連の特性は ext4 と XFS で大きく異なり、この違いが運用中に実際に問題となる地点でもあります。

読む順序は概念から症状へ進みます。最初の二節で inode とディレクトリエントリの関係をつかみ、そのあとは現場で出くわす状況を一つずつ扱います。急いでいるなら、最後の診断順序の要約表から見て、必要な節へさかのぼる読み方でも構いません。


1. 名前と実体 — inode とは何か

Linux においてファイルは二つの部分に分かれます。

  • inode: ファイルの実体です。サイズ、権限、所有者、時刻、リンク数、そしてデータブロックの位置を持ちます。ファイル名はここには入っていません
  • ディレクトリエントリ: 名前と inode 番号を対にした項目です。ディレクトリとは結局、こうした対のリストです。

この分離が、Linux のファイル動作のほぼすべての特異点を説明します。

ls -li /var/log/messages
stat /var/log/messages

ls -istat は inode 番号とリンク数を見せてくれます。stat の出力では Links がリンク数です。

ファイルを消すというのは、正確には ディレクトリエントリを消すこと です。その結果リンク数が 1 減ります。リンク数が 0 になり、そのファイルを開いているプロセスもないとき に初めて、カーネルがデータブロックを返却します。この二つの条件が核心です。

ハードリンクとシンボリックリンクの違いもここから出てきます。

echo hello > original.txt
ln original.txt hardlink.txt
ln -s original.txt symlink.txt
ls -li original.txt hardlink.txt symlink.txt
  • ハードリンクは同じ inode を指すもう一つの名前です。inode 番号が同じで、リンク数が 2 になります。オリジナルを消してもハードリンク経由でアクセスできます。
  • シンボリックリンクはパス文字列を持つ別個のファイルです。inode 番号が異なり、オリジナルを消すと壊れます。

ハードリンクには制約があります。同じファイルシステムの中でのみ作成でき、ディレクトリには張れません。inode 番号がファイルシステムの中でのみ一意だからです。これは rsync の --link-dest を使った世代バックアップが、同じファイルシステムでのみ効率的である理由でもあります。

ここから派生する結果がもう一つあります。ファイルに対する書き込み権限と削除権限は別物だということです。ファイルを消す行為はディレクトリエントリを消すことなので、必要なのは そのファイルではなく、そのディレクトリに対する書き込み権限 です。読み取り専用のファイルであっても、ディレクトリに書ければ消せます。共有ディレクトリで他人のファイルが消える事故はここから生まれます。

この問題の標準的な解法が sticky ビットです。

ls -ld /tmp
sudo chmod 1777 /srv/shared

sticky ビットが立ったディレクトリでは、ファイルの所有者とディレクトリの所有者、そして root だけがファイルを消せます。/tmp はこの方式で保護されています。複数のユーザーが一緒に使うディレクトリを作るときは、このビットも合わせて検討してください。


2. df と du が違う理由

df はファイルシステムのスーパーブロックが管理する使用量を、du はパスを巡回して出会ったファイルのサイズを足し合わせます。二つが違う場合、理由はたいてい三つのうちのどれかです。

理由1 — 削除されたが開かれたままのファイル。もっともよくあります。ディレクトリエントリがないので du は見つけられませんが、プロセスが開いているのでブロックは返却されません。

sudo lsof -nP +L1
sudo lsof -nP +L1 | awk 'NR>1 {print $1, $2, $7, $9}' | sort -k3 -nr | head

+L1 はドキュメントによると、リンク数が 1 未満の開かれたファイル、すなわちすでに unlink されたファイルを列挙します。

理由2 — マウントに隠されたファイル。あるディレクトリにファイルを入れたあと、その上に別のファイルシステムをマウントすると、下のファイルは見えなくなりますが容量は占有し続けます。

sudo mkdir -p /mnt/check
sudo mount --bind / /mnt/check
sudo du -x -sh /mnt/check/var/log
sudo umount /mnt/check

バインドマウントでルートを改めてマウントすると、隠されたファイルが見えます。確認後は必ずアンマウントしてください。

理由3 — 予約ブロック。ext4 はデフォルトで一定の比率を root 専用に予約します。df の利用可能容量が、総量から使用量を引いた値より小さい理由です。

sudo tune2fs -l /dev/sda1 | grep -i 'reserved'

注意: 予約比率を減らす tune2fs -m コマンドがインターネットでよく紹介されますが、予約ブロックには断片化の防止と root の緊急作業スペースの確保という目的があります。データ専用ボリュームでないなら、むやみに 0 にしないでください。


3. 削除したのに空き容量が戻らないとき

もっとも頻繁に出くわす状況なので、手順を整理します。

ステップ1 — 確認

df -h /var
sudo du -x -sh /var
sudo lsof -nP +L1 | head -20

ステップ2 — どのプロセスかを特定

sudo lsof -nP +L1 | awk 'NR>1 {print $2}' | sort -u | while read -r P; do
  printf '%s\t%s\n' "$P" "$(ps -o comm= -p "$P")"
done

ステップ3 — 解放。順番に試します。

# 3-1. 再オープンのシグナル (もっとも安全)
sudo systemctl reload rsyslog
sudo kill -USR1 "$(cat /run/nginx.pid)"

# 3-2. ファイルディスクリプタを直接空にする (プロセスは維持)
sudo truncate -s 0 /proc/1234/fd/7

# 3-3. サービスの再起動 (最終手段)
sudo systemctl restart myapp

破壊的コマンドの警告: truncate -s 0 /proc/PID/fd/N は、そのディスクリプタが指すファイルの内容を即座に空にします。誤ったディスクリプタ番号を指定すると、生きている別のファイルを飛ばします。実行前に必ず対象のリンクを確認してください

sudo ls -l /proc/1234/fd/7

逆にこの性質を利用して 削除されたファイルを復旧 することもできます。プロセスがまだ開いているなら内容は生きています。

sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log

削除されたファイルのシンボリックリンクには、パスの後ろに削除されたことを示す表示が付きます。この状態で cp を使ってコピーすれば内容を取り戻せます。ログファイルを誤って消したときに、まず最初に試すべき方法です。


4. inode の枯渇 — 容量が残っていてもファイルを作れないとき

df -i
df -ih

IUse% が 100 パーセントなら、ブロックが残っていても新しいファイルを作れません。No space left on device エラーが出るのに df -h には余裕があるという場合はこれです。

犯人を探します。ファイル数が多いディレクトリを探す問題です。

sudo find /var -xdev -type f 2>/dev/null | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head -20

-xdev は別のファイルシステムへ入り込まないようにします。すでに負荷が高い状態なら、このスキャン自体が負担になるので、範囲を絞って実行してください。

典型的な原因は次のとおりです。

  • PHP のセッションファイルやアプリケーションのキャッシュが整理されずに溜まる
  • メールキューが滞留する
  • 一時ファイルを作って消さないバッチ
  • コンテナイメージのレイヤーに含まれる多数の小さいファイル

ext4 では inode の数がファイルシステムの作成時点で固定されます。あとから増やせないので、枯渇したらファイルを消すか、ファイルシステムを作り直すしかありません。作成時に密度を調整できます。

sudo mkfs.ext4 -i 8192 /dev/sdb1
sudo mkfs.ext4 -N 10000000 /dev/sdb1

-i は inode 一つあたりのバイト数 (小さいほど inode が多くなる)、-N は inode の数を直接指定するものです。

XFS は違います。inode を動的に割り当てるので、事実上、枯渇の問題はほとんどありません。ただし古いカーネルや特定の設定では inode が特定の領域にのみ割り当てられる制限がありうるので、大量の小さいファイルを扱うボリュームなら XFS を検討するほうが安全です。正確なオプションと制約は、使用中のディストリビューションの mkfs 関連の man ページで確認してください。

破壊的コマンドの警告: mkfs は対象デバイスのすべてのデータを消します。デバイス名を一文字書き間違えると、本番ボリュームが消えます。実行前に必ず確認してください。

lsblk -f
sudo blkid /dev/sdb1
findmnt /dev/sdb1

5. ファイルディスクリプタ — プロセスが何を開いているか

ファイルディスクリプタは、プロセスが開いたファイルを指す整数です。0、1、2 はそれぞれ標準入力、標準出力、標準エラーとして慣例が定まっています。

ls -l /proc/1234/fd
ls -l /proc/1234/fd | wc -l
sudo lsof -p 1234
sudo lsof -p 1234 | wc -l

/proc/PID/fd の各項目は、ドキュメントによると 実際のファイルを指すシンボリックリンク です。ソケットやパイプは、ファイルパスの代わりに種類と inode 番号の形で表示されます。

lsof の FD 列には、数字以外に次の値が現れます。

  • cwd: 現在の作業ディレクトリ
  • rtd: ルートディレクトリ
  • txt: 実行ファイルのテキスト
  • mem: メモリマップされたファイル

数字の後ろの文字はアクセスモードです。r は読み取り、w は書き込み、u は読み書きです。

有用な照会の組み合わせです。

sudo lsof -nP -iTCP -sTCP:LISTEN
sudo lsof -nP -i :5432
sudo lsof -u appuser
sudo lsof +D /var/lib/myapp
sudo lsof -c nginx -a -u www-data
sudo lsof -t -c nginx
  • -a は条件を AND で結合します。これがないと条件が OR で処理され、予想よりはるかに多くの結果が出ます。
  • -t は PID だけを出力するので、他のコマンドに渡すのに向いています。
  • +D は指定したディレクトリ配下の全体を再帰的に探索します。遅いので範囲を絞って使ってください。

6. 上限 — Too many open files を解決する

このエラーの原因は四つの層にまたがっています。一つの層だけ直しても解決しない場合が多いです

層1 — プロセスごとのソフト/ハード上限

ulimit -Sn
ulimit -Hn
cat /proc/1234/limits | grep 'open files'

/proc/PID/limits を見るのが確実です。シェルで ulimit を変えても、すでに起動しているプロセスには反映されないからです。

層2 — ログインセッションの上限/etc/security/limits.conf または /etc/security/limits.d/ 配下のファイルです。

appuser soft nofile 65535
appuser hard nofile 65535

この設定は PAM を経由するログインセッションにのみ適用されます。systemd が起動するサービスには適用されません。この事実を知らないと、「設定したのになぜ効かないのか」で長くさまようことになります。

層3 — systemd ユニットの設定

[Service]
LimitNOFILE=65535

サービスとして実行されるプロセスの上限はここで決まります。適用にはデーモンの再読み込みとサービスの再起動の両方が必要です。

sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'

層4 — システム全体の上限

cat /proc/sys/fs/file-nr
cat /proc/sys/fs/file-max
sysctl fs.file-max

file-nr は三つの値を見せます。割り当てられたディスクリプタ数、使われていない割り当て分、そして最大値です。最初の値が三番目に近づいていれば、システム全体の上限に達しています。現代のシステムでは file-max のデフォルト値が十分に大きいので、ここで引っかかることはまれです。

inotify の監視上限も同じ系列の問題を生みます。ファイル監視を多用する開発ツールやログ収集器でよく引っかかります。

cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances

値を永続的に変えるには sysctl の設定ファイルに入れます。カーネルパラメータの調整全般については Linux カーネルパラメータチューニングガイド を参照してください。


7. 診断順序の要約

症状別に、何から確認するかを整理します。

症状最初のコマンド次の確認
消したのに容量が減らないlsof +L1該当プロセスへの再オープンのシグナル
容量は残っているのにファイル作成が失敗df -i小さいファイルが多いディレクトリの探索
dfdu が大きく違うlsof +L1バインドマウントで隠されたファイルの確認
Too many open files/proc/PID/limitssystemd の LimitNOFILE の確認
名前を変えたのに古いファイルに書き続けるls -l /proc/PID/fd再オープンのシグナルまたは再起動
ログファイルを誤って削除ls -l /proc/PID/fdcp /proc/PID/fd/N で復旧
バックアップの容量が予想より大きいfind -links +1ハードリンク保存オプションの確認

最後の行をもう少し説明すると、ハードリンクの多いディレクトリをハードリンクを知らないツールでコピーすると、それぞれのリンクが別個のファイルとしてコピーされ、容量が数倍になります。

find /backup -type f -links +1 | head
du -sh --count-links /backup/daily.0
rsync -aH /backup/ /backup2/

rsync-H オプションがハードリンクを保存します。tar もデフォルトでハードリンクを認識しますが、アーカイブに両方のファイルが含まれていて初めて関係が保たれる点を覚えておいてください。

ファイルシステム自体の破損が疑われる場合は検査を回します。

sudo umount /dev/sdb1
sudo fsck -n /dev/sdb1
sudo xfs_repair -n /dev/sdb1

破壊的コマンドの警告: fsckxfs_repair はマウントされたファイルシステムに対して実行すると破損を引き起こすことがあります。必ずアンマウントしてから実行し、-n オプションでまず読み取り専用の点検を行ってください。-n は何も直さず、問題の報告だけを行います。


クイズ: 理解度を確認しましょう

クイズ1: 10GB のログファイルを消したのに df の使用量がそのままです。何が起きているのでしょうか

答え: ファイルを開いたプロセスが生きていて inode が解放されていません

解説: ファイルの削除はディレクトリエントリを消すだけです。リンク数が 0 になっても、開かれたディスクリプタが残っていればカーネルはブロックを返却しません。

sudo lsof -nP +L1 | head
sudo systemctl reload rsyslog

もっとも安全な解決は、プロセスに再オープンを知らせることです。それが不可能なら、ディスクリプタを直接空にする方法もありますが、対象の確認が必須です。

sudo ls -l /proc/1234/fd/7
sudo truncate -s 0 /proc/1234/fd/7
クイズ2: df -h では 40 パーセントの余裕があるのにファイル作成が失敗します。確認コマンドと対応は

答え: df -i で inode の使用率を確認します

解説: ブロックと inode は別個の資源です。小さいファイルが数百万個あれば、容量が残っていても inode が先に枯渇します。

df -i
sudo find /var -xdev -type f | awk -F/ '{print "/"$2"/"$3}' | sort | uniq -c | sort -nr | head

ext4 は inode の数が作成時点で固定されるので、あとから増やせません。即時の対応は不要な小さいファイルの整理であり、根本的な対応は XFS の採用の検討か、inode 密度を高めた再作成です。

クイズ3: systemd サービスに limits.conf で nofile を 65535 に設定したのに反映されません。なぜでしょうか

答え: limits.conf は PAM のログインセッションにのみ適用され、systemd サービスには適用されません

解説: サービスの上限はユニットファイルで定めます。

[Service]
LimitNOFILE=65535

適用後は実際の値を確認する必要があります。

sudo systemctl daemon-reload
sudo systemctl restart myapp
cat /proc/"$(systemctl show -p MainPID --value myapp)"/limits | grep 'open files'

ulimit -n をシェルで確認するのは、そのシェルの値であってサービスの値ではありません。

クイズ4: アプリケーションが書いていたログを誤って消しました。プロセスはまだ生きています。復旧は可能でしょうか

答え: 可能です。開かれたディスクリプタを通じて内容をコピーすればよいです

解説: プロセスがファイルを開いている限り、データブロックは生きています。

sudo ls -l /proc/1234/fd | grep deleted
sudo cp /proc/1234/fd/7 /backup/recovered.log

注意すべき点は、プロセスを再起動した瞬間に復旧の機会が消えるということです。だからこの状況での最初の行動は、再起動ではなくコピーであるべきです。

クイズ5: ハードリンクとシンボリックリンクの実務上の違いを一つだけ挙げるとしたら

答え: ハードリンクはオリジナルを消してもデータが維持され、シンボリックリンクは壊れます

解説: ハードリンクは同じ inode を指すもう一つの名前です。リンク数が 0 になって初めてデータが解放されるので、名前を一つ消しても別の名前でアクセスできます。

ls -li original.txt hardlink.txt symlink.txt
stat original.txt | grep Links

制約も合わせて覚えておいてください。ハードリンクは 同じファイルシステムの中でのみ 作成でき、ディレクトリには張れません。一方、シンボリックリンクはファイルシステムの境界を越え、ディレクトリも指せますが、対象が消えると壊れます。

クイズ6: 世代バックアップのディレクトリを別のサーバーにコピーしたら容量が五倍になりました。原因は

答え: ハードリンクを保存しなかったため、それぞれのリンクが別個のファイルとしてコピーされました

解説: --link-dest を使った世代バックアップは、変更されていないファイルをハードリンクで共有します。コピーツールがこの関係を知らなければ、すべての世代のすべてのファイルを実際にコピーします。

find /backup -type f -links +1 | head
rsync -aH /backup/ /backup2/

-H(--hard-links)がハードリンクを保存します。-a には含まれないので、必ず別途指定する必要があります。


おわりに

この記事の内容を一文に縮めるとこうなります。Linux においてファイル名は実体ではなく、実体を指す一つの参照にすぎません

この一文から残りが導かれます。名前を消しても別の参照が残っていればデータは生きています。だから消したファイルの容量が減らず、だから消したファイルを復旧でき、だから名前を変えてもプロセスは古いファイルに書き続けます。

運用チェックリストに移すと三行です。容量の問題では df -hdf -i を一緒に見ます。消したのに減らなければ lsof +L1 を見ます。上限の問題はシェルではなく /proc/PID/limits で確認します。この三行がこの記事の実戦的な要約です。


参考資料


関連記事

현재 단락 (1/197)

運用をしていると、常識に反して見える状況に出くわします。ログを消したのに `df` の使用量がそのままです。容量が 30 パーセント残っているのに、ファイルを作れないと言われます。ファイル名を変えただ...

작성 글자: 0원문 글자: 10,552작성 단락: 0/197