TT Lab
はじめる
学ぶ 学習パス コース

Linux障害対応

No space left on deviceの五つの顔

TT Labで続きを見る

一言でいうと

ENOSPCは「ブロックがない」という意味ではなく、「カーネルがこの書き込みを受け入れるリソースがない」という意味です。そのリソースは、ブロックのこともあれば、inodeのことも、予約分を除いた空きのこともあります。

なぜ必要なのか

df -h /varが空き35Gを示しているのに、touch /var/log/testがNo space left on deviceで失敗します。アプリケーションはログを書けず、デプロイは一時ファイルの作成で失敗し、DBはWALを書けません。

ここで「dfが間違っている」と結論づけても進展しません。dfは自分が知っていること(ブロック使用量)を正直に報告しただけで、失敗の原因がブロックではなかっただけです。

どう動くのか

可能性を確率の高い順に整理すると、5つあります。

0. パスの思い違い。/には空きがあるのに、/varが別のパーティションで、そちらがいっぱいになっている場合です。30秒で確認できます。findmnt -T /var/log/app.logで、そのパスがどのファイルシステムに属するかをまず確定します。/tmpがtmpfsなら、ディスクとは無関係にENOSPCが発生し、同時にその分のメモリも消費します。

1. inodeの枯渇。ext2/3/4はフォーマット時にinodeの個数が確定し、その後は増やせません。デフォルトのプロファイルはおよそ16KBにつき1つなので、100GBのファイルシステムで約655万個です。平均ファイルサイズがそれより小さいワークロード(セッションファイル、メールキュー、キャッシュ)では、ブロックよりinodeが先に尽きます。df -iのIUse%が100%なら確定です。XFSは動的に割り当てるため、事実上この問題はありません。

2. 削除されたが開いたままのファイル。前のコースで学んだ、あの仕組みです。rmはリンクを消すだけで、リンク数が0で、かつ開いているfdが0になって初めてブロックが返されます。duはパスをたどって数えるため、ディレクトリエントリがないそのファイルを数えられません。dfとduの乖離が決定的なサインです。原因の常連は、logrotateがローテーション後に再オープンのシグナルを送らなかった場合です。

3. 予約ブロック。ext系は、デフォルトで5%をroot専用として残します。目的は2つあります。満杯になっても管理者が整理する余地を残すこと、そしてアロケーターが連続ブロックを探す余地を与えて断片化を遅らせることです。症状に特徴があります。rootでは成功するのに、サービスアカウントでだけ失敗します。

4. マウントによる隠蔽。ファイルがあるディレクトリの上に別のファイルシステムをマウントすると、元のファイルは見えなくなりますが、下のファイルシステムの領域は占有されたままです。duは上側だけをたどるため、どのツールでも見えません。

5. inotify。watchの上限を超えると、inotify_add_watchがENOSPCを返します。ディスクとはまったく無関係なのに、メッセージは同じです。

現場での姿

df 20G / du 3.1G。17GBの差があれば、削除された開いたままのファイルを疑います。lsof -nP +L1のNLINK列が0の項目がそれで、SIZEが返されていないバイト数です。lsofがない最小イメージでは、/proc/*/fdを直接たどって(deleted)を探します。ラボで行う方法がまさにこれです。

応急処置の落とし穴。truncate -s 0 /proc/PID/fd/Nはファイルを空にして、領域をすぐに返します。ただし、ファイルがO_APPENDで開かれていないと、プロセスは古いオフセットに書き続けてスパースファイルになり、ls -lのサイズは大きいままに見えます。応急処置専用であり、正式な解決は再オープンのシグナル(kill -USR1など)か再起動です。

診断が障害を大きくする場合。すでにI/Oが飽和しているディスクでfind /を全体スキャンすると、状況はさらに悪化します。-xdevでファイルシステムの境界を固定し、範囲を絞って始めます。

dfとduが食い違うとき

ディスクがいっぱいだという通知を受けて入ってみると、duでいくら合計しても、その分の容量にならないことがあります。原因は次の3つのどれかです。

削除したがまだ開いているファイル。Unixでファイルを削除するのは、ディレクトリから名前を外すだけです。あるプロセスがそのファイルを開いている間は、ブロックは返されません。ログファイルをrmで削除したのに、プロセスがそのfdへ書き続けている状況が典型です。duは名前をたどって歩くため、この容量は見えず、dfはブロックを数えるため見えます。

lsof +L1                      # 링크 수가 0인, 즉 지워졌는데 열려 있는 파일
ls -l /proc/<pid>/fd | grep deleted

取り戻す方法はプロセスを再起動することで、それができないならfdを切り詰めて使います。: > /proc/<pid>/fd/3は、ファイルを削除せずにサイズだけを0にします。rmの代わりにtruncateやログローテーションのcopytruncateを使う理由がこれです。

inodeが先に尽きた場合。ブロックは残っているのに、ファイルを作れない状態です。No space left on deviceという同じメッセージが出るため、症状だけでは区別できません。小さなファイルが何百万個も溜まるキャッシュディレクトリ、メールキュー、セッションファイルが原因です。

df -i                                    # 사용률이 100%인 파일 시스템
find /var -xdev -type f | cut -d/ -f1-4 | sort | uniq -c | sort -rn | head

-xdevを外すと、別のマウントに移って見当違いの場所を数えてしまいます。inodeの数はファイルシステムを作るときに決まるため(ext4の場合)、後から増やせません。XFSは動的に割り当てますが、imaxpctという上限があります。

マウントに隠れたファイル。/dataにファイルをたくさん書いたあとで、その場所にディスクをマウントすると、元からあったファイルは見えなくなる一方で、ルートファイルシステムの領域は占有されたままです。マウントを外してはじめて見えるようになります。

mkdir /mnt/root && mount --bind / /mnt/root
du -shx /mnt/root/data

最後に予約ブロックを覚えておきます。ext4は、デフォルトで5%をroot用に残しています。一般ユーザーにはすでにいっぱいに見えますが、dfはまだ空きがあると言います。データ専用ディスクならtune2fs -m 1で減らしてもかまいませんが、ルートファイルシステムでは手を付けません。その余裕が、システムが自力で復旧するための最後の拠り所だからです。

次のラボですること

inodeの使用率とディレクトリの容量を直接読み取り、空のファイル500個がなぜ容量を消費するのかを確認します。その後、削除されたが開いたままのファイルを自分で作り、/procで保持しているfdを探し、そのfdのパスを使って内容を復旧します。最後に、システム全体からそのような幽霊ファイルを見つけ出すスクリプトを書きます。