SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

dfはfullなのにduが少ない原因と解決方法

dfはfullなのにduの合計が合わない原因を解説します。削除済みファイルを保持するプロセスの特定方法、再起動なしでの解放、inode不足や隠れたマウント、予約ブロックも確認できます。

df が full を示し、du の合計が一致しない理由

df はディスクが full だと報告しますが、du では使用領域を特定できません。これは、削除済みのファイルをプロセスが開いたまま保持しているためです。ファイルを削除すると、ディレクトリからその名前が削除されます。データブロックが解放されるのは、その inode を指す最後のオープンファイルディスクリプターが閉じられたときだけです。du はファイル名をたどるため、その領域をカウントしません。df はファイルシステムに割り当て済みブロック数を問い合わせるため、名前がなくなったファイルもカウントします。

このガイドでは、すでにインストールされているツールを使い、通常の Ubuntu VPS でこの状態を再現します。/proc でファイルを保持しているプロセスを特定し、再起動せずに領域を解放します。同じ症状を引き起こす他の原因として、空き inode がない inode テーブル、マウントポイントの下に隠れているファイル、root 用に予約されたブロックも確認します。

各コマンドを実行し、自分の環境の出力を確認してください。値はディスクによって異なります。ガイドに表示された数値ではなく、自分のマシンで実行前と実行後の値を比較してください。

df が数えるものと du が数えるもの

df(disk free)は、マウントされた各ファイルシステムに対して、そのファイルシステム自身の使用状況を問い合わせます。存在するブロック数、割り当て済みのブロック数、空きブロック数を確認します。ディレクトリを開くことはありません。結果には、どのディレクトリエントリからも参照されていないファイルに属するブロックを含め、割り当て済みのすべてのブロックが含まれます。

du(disk usage)は逆の動作をします。指定したパスから開始し、ディレクトリを読み取り、見つかったすべてのエントリの属性を取得して、ブロック数を合計します。名前のないファイルは認識できません。読み取り権限のないディレクトリも同様です。そのため、通常のユーザーで取得した合計は root で取得した合計より小さくなります。比較結果から判断する前に、dusudo で実行してください。

2 つを比較するときは、毎回 2 つのオプションが重要です。

  • -x は、du を 1 つのファイルシステム内に限定します。これを指定しないと、du // 以下にマウントされたすべてのファイルシステムへ入り込み、df / が測定していない合計を出力します。
  • -s は、ディレクトリごとに 1 行ではなく、引数ごとに 1 行の要約を出力します。

これで、確認したいファイルシステムに対して並べて実行するコマンドがそろいます。

df -h /
sudo du -xhs / 2>/dev/null

df はすぐに結果を返します。du は、途中ですべてのファイルの属性を取得するため、大容量のファイルシステムでは数分かかります。2 つの合計が大きく異なり、du を root として -x 付きで実行した場合、失われている容量は名前のない何かに割り当てられています。

意図的に不一致を再現する

テスト用の VPS で実行してください。以下はすべて bash と coreutils だけを使用するため、何もインストールされません。

/var/tmpを保持しているファイルシステムの開始時点の状態を記録します。

cd /var/tmp
df -h .
df --output=used -B1 .

2 番目のコマンドは、丸めずに使用済みバイト数を表示します。そのため、最後の確認を正確に行えます。

次に、ファイルを作成します。サイズにはマシン自身が報告する空き容量を使用するため、使用するディスクの容量に合わせてデモを実行できます。

free=$(df --output=avail -B1 . | tail -n 1)
fallocate -l $((free / 10)) ghost.bin
ls -l ghost.bin
df -h .

$(...)はコマンド置換です。シェルはその中のコマンドを実行し、出力をfreeの値にします。この構文に不慣れな場合は、bash のコマンド置換で詳しく説明しています。fallocateは実際のブロックをデータを書き込まずに確保するため、すぐに完了します。対応していないファイルシステムではコマンドが失敗し、head -c $((free / 10)) /dev/zero > ghost.binがバイトを書き出して同じ処理を行います。

このdf -h .を記録した値と比較します。使用済みの列は増え、使用可能な列は減っています。

次に、別のプロセスでファイルを開いたままにしてから、ファイルを削除します。

sleep infinity < ghost.bin &
holder=$!
rm ghost.bin
ls -l ghost.bin
df -h .
sudo du -xhs . 2>/dev/null

この処理の要点はリダイレクトです。sleep infinity < ghost.bin &は標準入力をそのファイルにしたバックグラウンドプロセスを開始します。シェルはファイルを開き、そのファイルディスクリプターをsleepに渡して、開いたままにします。$!には、そのバックグラウンドジョブのプロセス ID が格納されます。rmは、ファイルディスクリプターが開いたままの状態で名前を削除します。

出力を確認します。lsはファイルを見つけられません。名前がなくなったためです。duは開始時点に近い値へ戻っています。名前をたどって調べるためです。dfは変化していません。ブロックが引き続き割り当てられているためです。これで、ファイルシステムとディレクトリツリーの状態が一致しなくなりました。その差分が、今削除したファイルです。

削除済みファイルを保持しているプロセスを特定する

開いているすべてのファイルディスクリプターは、/proc/<pid>/fd/ に、それが参照するファイルへのシンボリックリンクとして表示されます。ファイルのリンクが解除されると、カーネルはそのリンク先に削除済みの印を付けます。したがって、保持元を特定するには、リンク先にその印が付いたリンクを探します。

sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

-lname はシンボリックリンクの名前ではなくリンク先に一致し、%p はディスクリプターのパスを表示し、%l はリンク先を表示します。表示されるパスの2番目の要素がプロセス ID です。sudo を付けて実行してください。付けない場合、自分のプロセスの /proc/<pid>/fd しか読み取れません。find が走査している間に終了したプロセスから出るエラー出力は、標準エラー出力のリダイレクトで破棄します。

稼働中のサーバーでは、常に複数の削除済みファイルが保持されており、その大半は小さく、問題ありません。サイズで並べ替えると、重要なものだけを上位に残せます。

sudo bash -c 'for fd in /proc/[0-9]*/fd/*; do
  target=$(readlink "$fd" 2>/dev/null) || continue
  case "$target" in
    *"(deleted)") echo "$(stat -Lc %s "$fd" 2>/dev/null) $fd $target" ;;
  esac
done' | sort -rn | head

stat -L はリンクをたどって inode 自体を参照するため、%s は名前を失ったファイルのサイズを報告します。その数値で並べ替えると、最大のファイルが先頭になります。

次に、該当するディスクリプターの背後にあるプロセスを特定します。リストの先頭にあるパスには必要な2つの数値が含まれているため、まず変数に格納します。PID と N は、自分の環境で表示された値に置き換えてください。

pid=PID
n=N
ps -o pid,user,etime,args -p "$pid"
sudo stat -L "/proc/$pid/fd/$n"

ps はプログラム名と稼働時間を表示します。stat -L は、削除済み inode のサイズと割り当て済みブロック数を表示します。これらを合わせると、重要な問いに答えられます。どのサービスがこのファイルを保持しているのか、という問いです。

すでに lsof がある場合、sudo lsof +L1 は、リンク数が0まで減少した開いているファイルを一覧表示し、サイズを1つの表にまとめて表示します。最小構成の Ubuntu イメージには含まれていません。また、空き容量のないファイルシステムにパッケージをインストールすると、インストール自体が失敗することがあります。そのため、/proc による走査が常に利用できる方法です。

再起動せずに空き容量を戻す

再起動で解決することはありますが、最初に行うべき手段ではありません。サービスが停止し、証拠も失われるためです。より影響の小さい方法は、試す順に4つあります。

まだデータが必要なら、最初に別の場所へコピーします。ディスクリプタのパスを読み取ると、現在の inode を読み取れます。

sudo cp /proc/<pid>/fd/<n> /root/recovered.log

削除済みファイルを簡単に取り戻せる唯一のケースです。そのため、rm -rf で削除したファイルを復旧する では、プロセスがまだファイルを開いたまま保持しているかどうかを最初に確認します。最後のディスクリプタが閉じられると、この方法は使えなくなります。

2つ目は、ディスクリプタ経由でファイルを空にする方法です。/proc のパスは同じ inode を指すため、そこを切り詰めると、プロセスを実行したままブロックを解放できます。

sudo truncate -s 0 "/proc/$pid/fd/$n"
df -h /

書き込み側がファイルを append モードで開いている場合、この方法は問題なく動作します。その場合、すべての書き込みがファイルの現在の末尾に追加されるためです。append モードで開かれていない場合、プロセスは以前の書き込みオフセットを保持します。そのため、次の書き込みはファイル内のかなり後方に行われ、先頭に hole のあるファイルが再作成されます。hole にはブロックが割り当てられないため、ブロックは空きのままです。df には、解放された空き容量がそのまま表示されます。変化するのはサイズだけです。プロセスが書き込んだ後にもう一度 sudo stat -L "/proc/$pid/fd/$n" を実行すると、以前のサイズと、対応しなくなったブロック数が表示されます。サイズも0から始めたい場合は、プロセスを再起動します。

3つ目は、サービスにログを再オープンさせる方法です。ログファイルが削除された状態で動作し続けるデーモンは、この問題の典型的な実例です。多くのデーモンはシグナルを受け取るとログファイルを再オープンします。nginx は SIGUSR1、rsyslog は SIGHUP を使用します。手元のデーモンについては、推測せずにドキュメントを確認してください。デーモンに誤ったシグナルを送ると、停止する可能性があります。

sudo systemctl kill -s USR1 nginx

4つ目は、unit を再起動する方法です。sudo systemctl restart <unit> により、古いプロセスが保持していたすべてのディスクリプタが閉じられるため、ブロックは確実に戻ります。上の例では、保持しているのは自分で起動した sleep なので、これを終了するだけで十分です。

kill $holder
df -h .
df --output=used -B1 .
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

使用済みバイト数を、ファイルを作成する前に記録した値と比較します。両者は再び一致し、find にはディスクリプタが表示されなくなります。問題を見つけたのと同じコマンドで確認する習慣が重要です。

この値の変化を確認するには、df を何度も手動で実行するよりも、簡単な方法があります。一定間隔でコマンドを繰り返す watch は、出力をその場で再表示します。そのため、watch df -h / では空き容量が戻るにつれて使用済みの列が変化する様子を確認できます。

合計値が一致しているのにディスクが満杯の場合

dfとroot du -xの値が一致している場合、削除済みファイルは関係していません。残る原因は種類が異なり、それぞれに確認方法があります。

inode が枯渇しているのであって、ブロックが枯渇しているのではありません

inode には 1 つのファイルのメタデータが格納されます。ext4 はファイルシステムの作成時に固定数の inode を作成するため、空きブロックが残っていても inode が枯渇することがあります。その場合、df -h に空きが表示されていても、新しいファイルの作成に失敗します。

df -h /
df -i /

最初のコマンドはブロック数を数え、2 番目のコマンドは inode 数を数えます。それぞれの use 列を比較します。ブロック使用率が低く、inode 使用率が上限に達している場合、問題は非常に多くの小さなファイルです。

df は同じ呼び出しで -i--output を拒否します。そのため、生のカウントを読み取ったり別のコマンドに渡したりする場合は、inode フィールドを名前で選択し、-i を付けません。

df --output=itotal,iused,iavail,ipcent /

これらの列には df -i が出力するものと同じ使用量の情報が、個別に処理できる形式で含まれています。

バイト数ではなくエントリ数を数えて、ファイルの場所を特定します。

sudo du --inodes -x -d 1 / 2>/dev/null | sort -rn | head

最上位になったディレクトリで同じコマンドを 1 階層下に対して繰り返し、ファイルを作成しているツリーに到達するまで調べます。使用している du--inodes に対応していない場合は、sudo find /var -xdev -type f | wc -l でサブツリーを時間のかかる方法で数えます。

対処方法は、それらのファイルを削除または移動することです。既存の ext4 ファイルシステムに inode を追加することはできません。inode 数は mkfs 時点で固定されるため、増やすにはファイルシステムを作り直し、バックアップから復元する必要があります。XFS は必要に応じて inode を割り当てるため、同じような固定上限には達しません。コンテナを実行するマシンでは、イメージレイヤーに多数の小さなファイルが含まれるため、通常より早く両方の上限に達します。そのマシンでは、VPS の Docker のディスク使用量を削減することが具体的な対処方法です。ファイルシステム全体を一般的に検索するより、はるかに多くの容量を回収できます。

マウントポイントの下に隠れた領域

ディレクトリには、そこへ何もマウントされていない段階でファイルを保存できます。そのディレクトリにファイルシステムをマウントしても、下にあるファイルはそのまま残ります。領域は確保されたままで、dfにも計上されますが、名前ではアクセスできなくなります。マウントによって覆われるため、duからは確認できません。

余分なディスクを必要としないtmpfsで確認します。この操作にはマウント権限のあるマシンが必要です。そのため、KVM VPSで実行できます。

sudo mkdir -p /srv/covered
sudo cp /etc/services /srv/covered/
ls /srv/covered
sudo mount -t tmpfs tmpfs /srv/covered
ls /srv/covered
sudo umount /srv/covered
ls /srv/covered

中央のlsには、空のディレクトリが表示されます。コピー先がなくなったわけではありません。ファイルはrootファイルシステム上に残っており、アンマウントすると再び表示されます。誰かがボリュームをそのパスにマウントする前に、サービスが1か月間そのパスへログを書き込んでいた状況を想定してください。

稼働中のサーバーで実体を確認するには、rootファイルシステムを別の場所へ2回目にマウントします。bind mountを使うと、その内部にマウントされたファイルシステムを除外して、1つのファイルシステムを確認できます。

sudo mkdir -p /mnt/rootcheck
sudo mount --bind / /mnt/rootcheck
sudo du -xhs /mnt/rootcheck/* 2>/dev/null | sort -h
sudo umount /mnt/rootcheck

この一覧に表示され、通常のパスには表示されないものは、マウントポイントの下に隠れています。完了したらbind mountをアンマウントしてください。そうしないと、後で-xを指定しないduが同じファイルを2回計上します。

root 用に予約されたブロック

ext4 は、root ユーザー用にブロックの一部を確保します。これにより、ディスクが満杯になっても root がログインしてマシンを修復できます。一般ユーザーとして実行されるプロセスは、df にまだ少し空きがある状態でも、先にこの制限に達します。デフォルト値を前提にせず、使用中のファイルシステムで設定を確認してください。

dev=$(df --output=source / | tail -n 1)
sudo tune2fs -l "$dev" | grep -i 'block count'

このコマンドは、総ブロック数と予約ブロック数を同じ単位で表示するため、両者の比率を直接求められます。df は、一般ユーザーが引き続き使用できる空き容量を available 列に表示します。そのため、used と available の合計は size より小さくなります。この差が予約領域です。

sudo tune2fs -m <percent> "$dev" で変更できます。変更は直ちに適用され、再マウントは必要ありません。独立したデータファイルシステムでは、予約領域を減らしても問題ありません。root ファイルシステムでは、root が書き込めるだけの空きを残してください。空き容量が完全になくなった root ファイルシステムは、修復がはるかに困難になります。tune2fs は ext2、ext3、ext4 で使用できます。XFS には同等の設定がありません。

単独で du を使うと誤解しやすい場合

duには、合計値が誤って見える原因となる習慣が4つあります。

  • ハードリンク: duは、複数の名前が同じ inode を指していても、その inode を1回だけ数えます。そのため、ハードリンクが多いツリーでは、ファイルの合計より少ない値が表示されます。
  • スパースファイル: duは実際に割り当てられたブロック数を表示します。一方、ls -lは見かけ上のサイズを表示します。もう一方の値を確認するには --apparent-size を追加します。
  • 権限: 一般ユーザーとして実行すると、duは読み取れない項目をスキップするため、使用量を少なく報告します。表示されるエラーを /dev/null にリダイレクトして、確認を止めてしまうケースがあります。
  • ファイルシステムの境界: -xを指定しない場合、du // 以下にマウントされたすべてのファイルシステムを数えます。そのため、合計が df / の報告値を超えることがあります。

dfにも、知っておくべき動作が1つあります。ファイルシステムごとに個別の値を報告するため、書き込みが失敗している対象の正確なパスに対して実行してください。別の /boot は、カーネルパッケージの蓄積に応じて独自のスケジュールで容量を消費します。Ubuntu で古いカーネルを削除する作業は、/ の空き容量を増やす作業とは異なります。

実際のインシデントでの対応手順

  1. 反射的に / で実行するのではなく、書き込みに失敗したファイルシステム上で df -h <path>df -i <path> を実行します。
  2. sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h を実行し、最も大きいディレクトリを順に下位へ調べます。
  3. du で、df が使用済みとして報告した容量を説明できない場合は、/proc で削除済みのまま開かれているファイルを検索します。
  4. 2 つの結果が一致する場合は、ファイルシステムを別の場所に bind mount し、マウントポイント配下に残っているファイルを探します。
  5. 上限に達しているのが inode 使用量であれば、バイト数ではなくファイル数を数えます。

各手順では、コマンドを実行して出力を確認できます。これが、推測ではなく原因を特定して解決するための違いです。

FAQ

df ではディスクが満杯と表示されるのに、du では大幅に少ないのはなぜですか?

通常の原因は、プロセスが開いたままのファイルを削除したことです。ファイルを削除するとディレクトリエントリがなくなるため、du はたどる名前を失い、そのファイルをカウントしません。inode とそのブロックは、最後のファイルディスクリプタが閉じるまで割り当てられたままです。一方、df は割り当て済みのブロックをカウントします。/proc/<pid>/fd で、リンク先が deleted とマークされたシンボリックリンクを検索すると、対象ファイルと、それを保持しているプロセスの両方を特定できます。比較結果を信頼する前に、du を root として実行し、-x も指定したことを確認してください。一般ユーザーでは、読み取り権限のないディレクトリが通知なくスキップされます。

lsof を使わずに、開いたままの削除済みファイルを見つけるにはどうすればよいですか?

カーネルが保持しているオープンファイルディスクリプタの記録を使用します。sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null は、名前のないファイルを指すすべてのディスクリプタを一覧表示します。出力されるパスにはプロセス ID が含まれます。対象のディスクリプタパスに対して sudo stat -Lc %s を実行するとサイズを確認できるため、サイズ順に並べて重要なものを選べます。パッケージを追加する必要がありません。空き容量のないファイルシステムでは、パッケージのインストール自体が失敗する可能性があるためです。

プロセスを停止せずに容量を解放できますか?

場合によっては可能です。sudo truncate -s 0 /proc/<pid>/fd/<n> はディスクリプタを通じて同じ inode に到達し、プロセスを実行したままブロックを解放します。プロセスがファイルを append モードで開いていた場合に、最も安全に機能します。この場合、書き込みは常に現在の末尾に行われるためです。append モードでなかった場合、書き込みオフセットは元の位置に残ります。次の書き込みで先頭に hole のあるファイルが再作成されるため、表示サイズは元に戻りますが、hole の下にあるブロックは空きのままです。unit を再起動するか、ドキュメントで指定されたシグナルを送ってログを再オープンさせる方法なら、sparse file を残さずに解決できます。

df では空き容量があるのに、書き込みに失敗します。ほかに何が考えられますか?

同じパスに対して df -i を実行し、inode を確認してください。空きブロックがあっても空き inode がないファイルシステムでは、新しいファイルを作成できません。書き込みが root 以外のユーザーで実行されており、ext4 ファイルシステムに予約ブロックしか残っていない可能性も確認してください。デバイスに対して sudo tune2fs -l を実行すると確認できます。書き込み先のファイルシステムを確認してください。別の /boot/var/ とは独立して容量を消費するためです。

du の合計が df より大きくなるのはなぜですか?

du-x を指定しないと、指定したパス以下にマウントされたすべてのファイルシステムを横断します。そのため複数のファイルシステムを合計しますが、df が示すのは 1 つのファイルシステムです。Bind mount があると、同じファイルが出現する各パスで 1 回ずつカウントされるため、さらに差が大きくなります。-x を追加して du を 1 つのファイルシステムに限定し、df にも同じパスを指定してください。これにより、両方のコマンドが同じ対象を説明することになります。