概要
プロジェクトを進めていると、OOMまたはOOMKilledに遭遇することがあります。特にKubernetes環境では、「PodがOOMKilledによって再起動された」という状況にしばしば遭遇します。
OOMKilledは単にサーバーが再起動されたように見えることがありますが、原因を把握しないまま放置すると、サービス負荷が高い状況で繰り返し発生し、致命的な障害につながる可能性があります。
この文書は、プロジェクトを進める中で遭遇したOOMKilledとは何か、なぜ発生するのか、そしてどのように分析・解決すべきかを調査・検討した内容をまとめたものです。
OOMとOOMKilledの違い
OOMであれOOMKilledであれ、どちらもメモリ割り当てに関連して発生する問題です。しかし、両者には発生主体と動作方式に違いがあります。
OOM (Out Of Memory Error)はJVM内部で発生します。Heapメモリがいっぱいになり、これ以上オブジェクトを割り当てられなくなると、JVMがjava.lang.OutOfMemoryErrorを発生させ、プロセスが終了します。つまり、JVM自身が限界を認識してエラーを投げるのです。
OOMKilledはJVMの外部、つまりKubernetesのようなContainer環境で発生します。Containerに設定されたメモリLimitを超えた瞬間に、Kubernetesが該当プロセスを強制終了します。JVMがエラーを認識する前に外部から先にkillするため、OOMとは異なり個別のエラーログが残らず、原因の把握がより困難です。
|
区分 |
OOM |
OOMKilled |
|---|---|---|
|
発生主体 |
JVM内部 |
Kubernetes (cgroup) |
|
原因 |
Heapメモリの枯渇 |
Container Limitの超過 |
|
エラーログ |
OutOfMemoryErrorのログが残る |
ログなしで強制終了 |
|
Exit Code |
1 |
137 |
|
対象メモリ |
Heap領域 |
Heap + Non-Heap + Off-Heap全体 |
OOMKilledが発生すると、Kubernetesの再起動ポリシー(restartPolicy)に従ってPodが自動的に再起動されるため、単にトラフィックが少ない時間帯に発生したのであれば、大きな問題にならないこともあります。しかし、サービス負荷が高い状況でOOMKilledが発生すると、事態は深刻になる可能性があります。Podの状況によっては、再起動中にサービスが停止することもあり、再起動直後にトラフィックが再び集中するとメモリが急速に消費され、連続的なOOMKilledが繰り返される可能性があります。最終的にはCPUとメモリの負荷が増大し、サービス全体に致命的な障害をもたらす可能性があります。
メモリの分類
OOMKilledに関連してメモリについて言及する際は、Heapメモリの割り当てだけでなく、Container、JVMなど、より詳細なメモリについて理解する必要があります。
Containerメモリ
Kubernetes環境で注意すべき点は、メモリ設定の範囲です。Containerに設定するメモリLimitはJVMだけを対象とするものではなく、Container全体を対象とします。
つまりContainerメモリ > JVMメモリという関係を必ず認識しておく必要があります。
Container 메모리
│
├── JVM 프로세스 메모리
│ ├── Heap
│ ├── Non-Heap
│ └── Off-Heap (Direct Buffer)
│
├── OS 및 시스템 라이브러리
└── 사이드카 Container (Istio sidecar 등) 등 기타…
JVMメモリ
JVMメモリは大きく3つの領域に分けられます。
Heap領域
Javaアプリケーションで生成されるオブジェクトが保存される領域です。GC(Garbage Collection)の対象となる領域でもあります。
|
指標 |
説明 |
|---|---|
|
Heap Used |
現在実際に使用されているメモリ |
|
Heap Committed |
JVMがOSからあらかじめ確保しておいたメモリ。GC後もすぐには返却しない |
|
Heap Max |
JVMが使用可能な最大Heapサイズ |
3つの指標の関係は常にUsed ≤ Committed ≤ Maxであり、CommittedはFull GC時に整理されます。
Non-Heap領域
JVM自体の運用のための領域で、GCの対象ではありません。
|
領域 |
説明 |
|---|---|
|
Metaspace |
クラスメタデータ、Static変数の保存 |
Metaspaceは-XX:MaxMetaspaceSizeを設定しない場合、理論上無制限に増加する可能性があります。
Off-Heap領域(Direct Buffer)
HeapでもNon-Heapでもない領域で、JVMがOSに直接要求して割り当てるメモリです。NIOベースのファイル処理やネットワークI/Oで主に使用されます。
GCの対象ではないため、明示的に解放されなければ蓄積する可能性があり、-XX:MaxDirectMemorySizeで上限を設定しない場合は無制限に増加する可能性があります。
ContainerメモリとJVMメモリの関係
JVMのHeapサイズがContainer Limitより小さく設定されていれば問題ないと考えがちです。しかし、HeapだけでなくNon-Heap、Off-HeapもすべてContainer Limit内で一緒に管理されるため、3つの領域を合算して慎重に計算する必要があります。特にシステムの特性によっては、ファイル処理が多いサービスはOff-Heapを、クラスローディングが多いサービスはNon-Heapを多く使用する可能性があります。そのため、単純にHeapサイズだけを見るのではなく、自身のシステムの特性を十分に把握したうえでメモリを設計する必要があります。
基本的にNon-Heapが200MB、Off-Heapが100MB、JVM以外のその他のメモリが50MBを使用するシステムで、Container Limitを1Gi、-XX:MaxRAMPercentage=80に設定すると、以下のような状況が発生します。
Heap Max (80%) : 819MB ← JVM이 사용 가능하다고 인식하는 Heap 한도
Non-Heap : 200MB
Off-Heap : 100MB
JVM 외 기타 : 50MB
─────────────────────────
Heap 외 합계 : 350MB
실질적으로 Heap에 허용되는 메모리 : 1,024MB - 350MB = 674MB
JVMはHeapを819MBまで使用できると認識し、Full GCを実行せずにメモリを割り当て続けます。しかし、KubernetesはHeap + Non-Heap + Off-Heap + その他をすべて合算してContainer Limitと比較するため、Heap使用量が674MBを超えた瞬間にContainer Limit 1Giを超過し、JVMが認識する前にKubernetesがプロセスを強制終了します。これがOOMKilledです。
JVM設定
以下では、OOMKilledに関連するJVMオプションについて簡単に定義して説明します。
MaxRAMPercentage
-
Container Limitに対するHeap Maxサイズの割合を設定します。小さくしすぎるとメモリを無駄にする可能性があり、高く設定しすぎるとNon-HeapやOff-Heapの消費量も合算され、Container Limitを超過する可能性があります。
MaxMetaspaceSize
-
Non-Heap領域のうち、Metaspaceの最大サイズを制限します。設定しない場合、Metaspaceは理論上無制限に増加する可能性があります。現在の使用量に余裕を持たせて上限を設定することを推奨します。
MaxDirectMemorySize
-
Off-Heap(Direct Buffer)の最大サイズを制限します。NIOベースのファイル処理やネットワークI/Oを多く使用するサービスで設定しない場合、Direct Bufferが無制限に増加してContainer Limitを超過する可能性があります。このオプションを設定すると、上限超過時にOutOfMemoryError: Direct buffer memoryというログが残るため、原因の把握が容易になるという副次的なメリットもあります。
OOMKilledサーバー分析ケース
以下は、OOMKilledが発生したサービスのメモリ状況を入力としてAIで分析した結果を要約した内容です。最終的にJVMオプションを修正して適用した後、OOMKilledの問題は解決しました。
元のNotionリンク: https://app.notion.com/p/vizend/Pod-OOMKilled-38b35bc54c13806699e8e3ae329bf38f
メモリ状況の分析
1. Containerリソース設定
|
項目 |
値 |
|---|---|
|
Memory Limit |
1Gi (1,073 MB) |
|
Memory Request |
256Mi |
|
QoS Class |
Burstable |
2. JVMメモリ状況(現在時点)
|
領域 |
値 |
MB換算 |
|---|---|---|
|
Heap Used |
376,595,248 bytes |
~359 MB |
|
Heap Committed |
512,229,376 bytes |
~488 MB |
|
Heap Max |
859,308,032 bytes |
~819 MB |
|
Non-Heap Used |
210,018,744 bytes |
~200 MB |
|
└ Metaspace |
127,576,696 bytes |
~122 MB |
3. 主要な問題の分析
Container Limit : 1,073 MB (1Gi)
────────────────────────────────────────
Heap Max (JVM 설정) : 819 MB
Non-Heap Used (현재) : 200 MB
────────────────────────────────────────
합산 최대 사용 가능 : 1,019 MB ← 한계에 매우 근접
나머지 여유 : ~54 MB ← 위험 수준
JVMのHeap Max + Non-Heapだけで、Container Limitの約95%を占めています。
ここにJVM内部のオーバーヘッド(Thread Stack、GCメタデータなど)が加わると、1Giを超過してOOMKilledが発生します。
4. GC方式の確認
ActuatorのレスポンスにPS Eden Space、PS Old Gen、PS Survivor Spaceが表示されています。
G1GCの使用を推奨します。
5. 原因の要約
|
原因 |
説明 |
|---|---|
|
Heap Maxが過大 |
819MBのHeap + 200MBのNon-Heap = 1,019MBで、Limit超過の直前 |
|
GC方式が不適切 |
Parallel GC → Containerメモリの認識不足 |
|
JVMオプションが未設定 |
-XX:MaxRAMPercentageなど、Container認識用オプションが未設定と推定 |
|
QoS Burstable |
Request ≠ Limit → メモリ保証なし、OOMの優先対象 |
MaxRAMPercentageのシミュレーション結果
MaxRAMPercentageを現在の80%から60%まで比較分析しました。
Heap使用量
|
項目 |
80% |
70% |
65% |
60% |
|---|---|---|---|---|
|
Container Limit |
1,073MB |
1,073MB |
1,073MB |
1,073MB |
|
Heap Max |
858MB |
~751MB |
~698MB |
~644MB |
|
Non-Heapの実測値 |
~200MB |
~200MB |
~200MB |
~200MB |
|
Thread Stackなどのオーバーヘッド |
~50~80MB |
~50~80MB |
~50~80MB |
~50~80MB |
|
合計推定 |
~1,108~1,131MB |
~1,001~1,031MB |
~948~978MB |
~894MB |
|
余裕 |
~42~72MB |
~95~125MB |
~179MB |
オプション別比較
|
設定 |
Heap Max |
余裕 |
評価 |
比較 |
|---|---|---|---|---|
|
現在80% |
~858MB |
~15MB |
危険 |
Non-Heap、Off-Heapを追加使用すると問題が発生 |
|
70% |
~751MB |
~42~72MB |
不安定 |
G1GCとともにContainer Limitを増加 (1.5Gi) |
|
60% |
~644MB |
~149~179MB |
推奨 |
G1GC適用 |
|
50% |
~536MB |
~257MB |
安全だがHeap不足の可能性 |
検討対象外 |
-
最も安全なアプローチは60%に設定し、モニタリングしながら必要に応じて引き上げることです。
-
Non-HeapまたはOff-Heapの200MBは固定値ではなく、追加のファイルアップロードなどの負荷状況でOOMKilledを引き起こす可能性があります。
推奨JVMオプション (最終)
# 현재
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=80
-XX:+UseParallelGC
# 권장
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=60 # Heap ~644MB로 제한
-XX:+UseG1GC # Container 친화적 GC
-XX:MaxMetaspaceSize=192m # Metaspace 상한 설정 (현재 122MB 사용 중)
-XX:MaxDirectMemorySize=140m # Direct Buffer 상한 설정 (현재 108MB 사용 중)
-
Heap Max: ~644MB
-
Non-Heap: ~200MB
-
合計: ~844MB → Container Limit 1Gi内で余裕を確保
sby