ディスクとファイルディスクリプタ
一言でいうと
dfに余裕があるのに書き込めないときは、inodeが尽きたか、削除されたファイルを誰かが開いたままにしています。どちらもdf -hでは見えません。
なぜ必要なのか
ディスク関連の報告で最もよく出る2つが、これです。
inodeの枯渇。ファイル1つ1つがinodeを1つずつ使います。小さなファイルが数百万個あると、容量は余っていても、inodeが先に尽きます。df -iで見ないと見えません。
削除されたのに開かれたままのファイル。ログファイルをrmしたのに、プロセスがまだそのファイルを開いていると、Linuxはリンクだけを消して、実際のブロックは返しません。dfは相変わらず満杯だと言い、duはファイルがないと言います。この不一致が、決定的な手がかりです。
# du 와 df 가 다르면 이걸 본다
ls -l /proc/*/fd 2>/dev/null | grep deleted
解決は、プロセスにファイルを開き直させることです(ログならlogrotateのcopytruncateか、デーモンへのSIGHUP)。プロセスを再起動してもかまいません。
どう動くのか
ファイルディスクリプターのリークは、じわじわと死にます。
ls /proc/<PID>/fd | wc -l # 지금 몇 개 열었나
cat /proc/<PID>/limits | grep 'open files' # 한도
この数字が時間とともに単調に増加していれば、閉じないコードがあります。上限に達した瞬間、Too many open filesでリクエストがすべて失敗します。上限を上げるのは時間を稼ぐことで、直すことではありません。
よくある勘違い
ulimit -nが無制限なら安全だという勘違い。コンテナのデフォルトのRLIMIT_NOFILEは事実上無制限(10億規模)ですが、古いデーモンの中には、起動時にその数字の分だけ接続テーブルを確保しようとして、メモリを使い切って死ぬものがあります。無制限が常によいとは限りません。
ディスクが遅いかを判定する2つの数字
iostat -x 1の多くの列のうち、実際に使うのは2つです。
Device r/s w/s rkB/s wkB/s r_await w_await aqu-sz %util
nvme0n1 120 380 4800 15200 0.31 0.52 1.24 38.2
await(r_await・w_await): リクエスト1つがキューで待った時間まで含めた応答時間(ms)です。NVMeは1ms未満、SATA SSDは数ms、HDDは10ms台が正常です。この値が普段の何倍にも跳ねたら、ディスクがボトルネックです。aqu-sz: 平均キュー長です。1を超えると、リクエストが列を作っているという意味です。
%utilを信用しないでください。この値は「リクエストが1つでも進行中だった時間の割合」なので、並列処理ができるNVMeでは、100%でも余裕が残っています。回転ディスクの時代の指標です。
どのプロセスがディスクを使っているか
# 실시간으로 I/O 상위 프로세스
iotop -oPa # -o: 실제로 I/O 하는 것만, -a: 누적
# iotop 이 없을 때 (컨테이너에서 흔하다)
cat /proc/<PID>/io # read_bytes, write_bytes 를 두 번 읽어 차이를 낸다
/proc/<PID>/ioのread_bytesは実際のディスクから読んだ量で、rcharは読み取りのシステムコールで渡した量です。2つの差が大きければ、ページキャッシュがよく効いているという意味で、良いサインです。逆に2つが近ければ、キャッシュが効かず、毎回ディスクを叩いています。
コンテナでI/Oを制限する
CPU・メモリと違い、KubernetesにはI/Oの制限フィールドがありません。cgroup v2にはio.maxがありますが、Podのスペックとしては公開されていません。そのため、実務の対応は違います。
- うるさい隣人を物理的に分離します。ログコレクターやバックアップのように、I/Oをまとめて使うワークロードは、別のノードや別のディスクに送ります。
- アプリケーション側で調整します。バックアップスクリプトに
ionice -c3(idleクラス)をかけたり、rsync --bwlimitのように、ツールが提供する制限を使います。 - ローカルディスクの代わりに、ネットワークストレージのIOPSグレードを購入します。クラウドなら、これが最も確実です。
ioniceは、CFQ・BFQスケジューラーでしか効きません。最近のNVMeのデフォルトであるnone(マルチキュー)では何の効果もないので、cat /sys/block/nvme0n1/queue/schedulerで先に確認します。
実務で本当に大切なこと
ログローテーションの設定で、createとcopytruncateの違いが、ここに関わります。
create: 元のファイルをrenameして、新しいファイルを作ります。プロセスは引き続き古いファイル(今は名前が変わったもの)に書くので、デーモンにSIGHUPを送って開き直させる必要があります。copytruncate: 内容をコピーしたあと、元のファイルを0バイトに切り詰めます。プロセスはそのまま書き続ければよいのですが、コピーと切り詰めのあいだに書かれたログは失われます。
どちらもタダではありません。アプリケーションがSIGHUPを処理できるならcreate、できないならcopytruncateを使い、失われる可能性があることを承知しておきます。