先に要点
- 空きメモリのしきい値でアラートを組んでも、メモリ不足による強制終了(OOM)は検知できません。強制終了の瞬間にメモリが解放されるので、その直後に測った値は正常に見えます。
- 実際に測りました。上限64MBのコンテナで2回OOMを起こした直後の使用量は約2.4MB、上限の3.8%です。しきい値を何パーセントに置いても引っかかりません。
- コンテナの
OOMKilledは「起きたかどうか」の真偽値で、何回起きたかは分かりません。2回起こしてもtrueのままでした。しかもコンテナは動き続け、終了コードは0です。 - 取りこぼさないのは累積カウンタです。同じ状況で
oom_killは 2 を示しました。監視で見るべきはここです。
サーバーの監視を組むとき、まず入れるのが空きメモリのしきい値アラートです。残りが一定を下回ったら通知する。素直な設計に見えます。
ところがこの設計では、いちばん知りたい「メモリ不足でプロセスが強制終了された」という事象を捉えられません。しかも捉えられないことに気づく機会がありません。アラートが鳴らないのは平穏の証拠に見えるからです。
この記事では、その理由を実際に測った数値で示し、代わりに何を見ればよいかをまとめます。
測った結果
上限64MBのコンテナを作り、その中で意図的にメモリを食い尽くしました。コンテナ単位の上限なので、動かしている母艦には影響しません。手順は後半に書きます。
| 見た場所 | 2回OOMを起こした後の値 |
|---|---|
| メモリ使用量 | 2,568,192 バイト(上限 67,108,864 の約3.8%) |
| コンテナの状態 | running(動き続けている) |
| コンテナの終了コード | 0 |
OOMKilled の値 |
true(1回目のときも true) |
累積カウンタ oom_kill |
2 |
| 強制終了されたプロセスの終了コード | 137 |
上から3つを見てください。使用量は上限の4%未満、コンテナは正常稼働、終了コードは0。この3つだけを監視していると、何も起きていないように見えます。
なぜ空きメモリでは見えないのか
理由は単純で、OOMは「メモリを解放する処理」だからです。
メモリが足りなくなったとき、カーネルはプロセスを1つ選んで強制終了します。終了すればそのプロセスが確保していたメモリは解放されます。つまり問題が起きた直後は、むしろメモリに余裕がある状態になります。
監視のしくみは、たいてい一定の間隔で現在値を読みます。読んだ時点ではもう解放が終わっているので、記録に残るのは正常な値だけです。
この見落としは「アラートが鳴らない」という形で現れます。鳴らないことは正常と区別がつきません。設計が間違っているほど平穏に見えるので、自分から疑いにいかない限り、何年でも気づかないまま運用できてしまいます。
同じことは負荷の急上昇にも言えます。1分間隔で読んでいるなら、30秒で終わった急上昇は存在しなかったことになります。現在値の監視は、読んだ瞬間の話しかしていません。
真偽値は「何回か」を教えてくれない
コンテナには、メモリ不足で終了させられたかどうかを示す値があります。これは実際に機能しました。子プロセスだけが強制終了され、コンテナ自体は動き続けている場合でも true になりました。
ただし限界があります。2回起こしても値は true のままです。
- いつ起きたのか分からない
- 何回起きたのか分からない
- 前回見たときの
trueと、新しいtrueを区別できない
監視で使うなら、この3つが致命的になります。毎日確認して true が出続けるとき、それが「今日また起きた」のか「先月の1件が残っている」のかを判断できません。区別できない通知は、数回で読まれなくなります。
そしてもう1つ。上の表のとおり、この状況でコンテナの終了コードは0でした。子プロセスの側は 137 で終わっていますが、それを起動したシェルは正常に次へ進みます。終了コードだけを見る監視では、成功として記録されます。
累積カウンタなら取りこぼさない
同じ状況で、カーネルが持っている累積カウンタを読むと oom_kill は 2 でした。回数がそのまま入っています。
累積カウンタが優れているのは、読む間隔に依存しない点です。
| 現在値(使用量・空き) | 累積カウンタ(発生回数) | |
|---|---|---|
| 読んだ瞬間の状態 | 分かる | 分からない |
| 読んでいない間の出来事 | 消える | 残る |
| 監視の間隔 | 短いほど良い(負荷とのトレードオフ) | 長くても取りこぼさない |
| 判定の仕方 | しきい値と比較 | 前回の値との差 |
つまり1日1回読んでも、その24時間に起きた回数は正しく分かります。前回の値を保存しておき、増えていたら通知する。それだけで済みます。
ディスク使用率のような現在値は、これができません。読んでいない間に満杯になって復旧していたら、その事実は永久に残りません。性質がまったく違う指標を、同じ「メトリクス」という言葉で扱わないことが大事です。
自分の環境で安全に確かめる
実際に手を動かすと理解が早いので、手順を書いておきます。上限を小さく決めたコンテナの中だけで起こすので、母艦のメモリには影響しません。
# 上限64MBのコンテナで、子プロセスにメモリを食わせる
docker run -d --name oomtest -m 64m --memory-swap 64m alpine:3.21 \
sh -c 'tail /dev/zero; echo "子が死んだ exit=$?"; sleep 600'
十数秒待ってから、3か所を見比べます。
# 1. コンテナの状態。動き続けていて終了コードは0
docker inspect --format 'Status={{.State.Status}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' oomtest
# 2. 累積カウンタ。oom_kill に回数が入っている
docker exec oomtest cat /sys/fs/cgroup/memory.events
# 3. 今の使用量。上限に対してごくわずかしか使っていない
docker exec oomtest sh -c 'echo "$(cat /sys/fs/cgroup/memory.current) / $(cat /sys/fs/cgroup/memory.max)"'
後片付けは次のとおりです。失うのはこのコンテナだけで、ほかのコンテナやイメージには触れません。
docker rm -f oomtest
なお --memory-swap を上限と同じ値にしているのは、退避先を無くして確実に強制終了させるためです。これを省くと、環境によっては退避が効いて終了まで至りません。
「使用率」と「残量」は別の指標
退避領域(スワップ)の監視にも、似た形の勘違いがあります。使用率が高いこと自体は問題ではありません。使われているのは、使うべきものが退避されているだけかもしれません。
危険なのは残量が尽きることです。残りが無い状態で急な確保が発生すると、退避で吸収できずにそのまま強制終了へ進みます。
- 使用率が90%でも、残量が十分なら問題ない場合がある
- 使用率が30%でも、全体が小さければ残量はすぐ尽きる
見るべきは割合ではなく絶対量の残りです。割合で監視していると、容量を増やしたときに同じ割合へ戻って「改善していない」と誤解することもあります。
監視を組むときの整理
現在値か、累積値か
指標を足すとき、まずどちらの性質かを決めます。累積値なら間隔を長くしても取りこぼしません。現在値なら、読んでいない間の出来事は見えないと割り切ります。
真偽値は回数にできないか
「起きたか」より「何回起きたか」の方がほぼ常に有用です。真偽値しか無いなら、読んだ時刻とセットで記録して、自分で回数に変えます。
鳴らないことを確かめる
アラートは、鳴ることより鳴らないことの方が多い仕組みです。導入時に一度、意図的に条件を満たして本当に鳴るかを確かめておきます。
3番目が実務ではいちばん効きます。検知の仕組みは、検知できることを一度も確かめないまま何年も置かれがちです。この記事の再現手順は、そのまま「本当に鳴るか」の試験に使えます。
よくある質問
ログを見れば分かるのではないですか
分かります。カーネルのログには強制終了の記録が残るので、そこを監視するのは有効です。ただしログは保持期間があり、古いものから消えます。累積カウンタは再起動するまで消えないので、確認の頻度が低くても成り立ちます。両方あるなら、まずカウンタで気づき、詳細をログで追うのが早いです。
コンテナを使っていない場合はどうなりますか
同じです。サーバー全体でも、カーネルは同じ仕組みで強制終了を行い、同じように回数を数えています。見る場所が変わるだけで、現在値では捉えられず累積値なら捉えられるという関係は変わりません。
しきい値のアラートは無駄ということですか
無駄ではありません。じわじわ増えて埋まっていく形の問題には有効です。向いていないのは、短時間で発生して自動的に解消する事象です。強制終了、瞬間的な負荷、接続の一時的な失敗などが該当します。この2種類を別の仕組みで見るのが本来の形です。
通知が多すぎて読まれない状態になりませんか
なります。だからこそ回数で見ることが効きます。「増えたときだけ通知する」なら、静かなときは何も届きません。真偽値をそのまま通知すると、状態が続いている間ずっと鳴り続けるので、まず読まれなくなります。
まとめ
空きメモリのしきい値では、メモリ不足による強制終了は検知できません。
- 強制終了はメモリを解放する処理なので、直後に測ると正常に見える
- 実測では、2回起こした直後の使用量が上限の3.8%だった
- 真偽値は「起きたか」しか分からない。2回起こしても値は同じ
- コンテナは動き続け、終了コードは0のまま
- 累積カウンタなら回数が残る。読む間隔が長くても取りこぼさない
監視に指標を足すときは、それが現在値なのか累積値なのかを最初に確かめてください。この区別を意識するだけで、「鳴らないから平穏だ」という最も危険な誤解を避けられます。