Podは生きているのにGoだけが落ちる:GOGCとGOMEMLIMITで追うInvisible OOM Killの謎
by
Watanabe Takeshi(Buzz)
×
Copy
Open
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Slide 1
Slide 1 text
Podは⽣きているのにGoだけが落ちる GOGCとGOMEMLIMITで追うInvisible OOM Kill の謎 2026年09⽉11⽇ Go Conference 2026 1
Slide 2
Slide 2 text
About Me Buzz | Takeshi Watanabe @buzz_tkc Company 株式会社エウレカ Role Senior Backend Engineer Hobbies トライアスロン, スノボ, サッカー 2
Slide 3
Slide 3 text
3
Slide 4
Slide 4 text
2025年4月頃 4
Slide 5
Slide 5 text
数日に一回の周期で 5xxエラーが不定期に大量発生し始める 5
Slide 6
Slide 6 text
✕ Killed Go App 5xxエラー発生後に Goのプロセスが落ちる 6
Slide 7
Slide 7 text
Panic logがでていない OOM Killedもでていない Podも⽣きている Goのプロセスだけ 突然落ちたように⾒える 7
Slide 8
Slide 8 text
そんな謎が多い障害を 一つ一つ解き明かしていくお話です 8
Slide 9
Slide 9 text
Agenda 1 前提:ペアーズのArchitectureと発⽣していた問題 2 第⼀の謎:Invisible OOM 3 第⼆の謎:なぜGoがOOMでKillされたのか 4 第三の謎:なぜGCが⾛らなかったのか 5 問題を解いていく 6 まとめ 9
Slide 10
Slide 10 text
前提:ペアーズの Architectureと 発生していた問題 10
Slide 11
Slide 11 text
ペアーズの Architecture(簡略版) ● GoのアプリケーションがKubernetes(Amazon EKS)上で動いている ● 当時は歴史的経緯もあり、NginxとGoが同じコンテナ内で動いていた Go App Nginx クライアント (iOS / Android / Web) EKS 11
Slide 12
Slide 12 text
不定期に起きていた問題 ● 5xxエラーが急激に増え、Goのプロセスが落ちる ● 同じコンテナのNginxは生きている ● エラー発生後、何事もなかったようにすぐに復活する Container Nginx Go App 12
Slide 13
Slide 13 text
第⼀の謎 Invisible OOM 13
Slide 14
Slide 14 text
まずは何が起きたかを確認していく Application panic log なし Container Nginxは稼働、Goだけが不在 Kubernetes/Pod OOMKilled ‧Terminatedなし Host / Kernel TaskOOM / oom-kill / exit code 137 → ここで初めて OOM だったと判明 14
Slide 15
Slide 15 text
問題があったコンテナを分析する コンテナは技術的には複数プロセスを動かせるが、1コンテナ1プロセス が推奨。当時は歴史的経緯でNginx + Go の複数プロセス構成だった。 コンテナ ✓ Alive PID1 = init (起動シェル / entrypoint) コンテナの「親」プロセス ✓ Alive ✕ Killed Nginx Go App ⼦プロセス 前段のリバースプロキシ ⼦プロセス‧PID≠1 アプリ本体 15
Slide 16
Slide 16 text
鍵はkillされたプロセスがPID1かどうか Linux OOM Killer はcontainer memory limitを超えた場合、 コンテナ内のメモリを⼀番使⽤しているプロセスを選んで kill する。 PID1(init)をkill → Container ⾃体が終了 → Status = OOMKilled (=普通はこう⾒える) (今回)⾮PID1 のGoプロセスをkill → PID1 と Nginx は⽣存 → Container は落ちない → OOMKilledもTerminatedも出ない NginxとGoが1つのコンテナに同居しているせいで、OOMの発⽣がログか ら⾒えづらい(Invisible)状態になっていた。 16
Slide 17
Slide 17 text
補⾜:Kubernetesのバージョンで挙動が変わる 必ずGoだけが kill されると⼀般化はできず、Kubernetes / cgroup の バージョンで挙動が変わる。 バージョン 挙動 Kubernetes 1.27 以前 ⼀番メモリを使⽤しているプロセスのみ killされる。 (=Goだけ落ちる、今回のケース) Kubernetes 1.28 以降 (cgroup v2 +memory.oom.group) 同じcgroup内の任意のプロセスがまとめて killされうる。 17
Slide 18
Slide 18 text
実際に起きていた挙動の流れ ③liveness probeが失敗(Goの応答なし) ① 何かしらの原因で Container OOM Killが⾛り、 Go だけが kill される。 kubelet Kernel Go App Nginx ⑤コンテナ再起動で復活 ④Nginxがupstream(Go)不在 で502/503を返し続ける ②PID1とNgixが残るためContainer は即時終了しない Client 18
Slide 19
Slide 19 text
その結果一時的な 5xxエラーが大量発生しては、 コンテナ再起動で復活を繰り返す 19
Slide 20
Slide 20 text
Invisible OOMの調査結果 謎 調査結果 ● Pod/Containerのstatusに OOMが出ない。 ● メトリクス上だと⼀瞬5xxエ ラーが発⽣しては、すぐに回 復する挙動が繰り返される。 ● Go(⾮PID1)がKernelにOOM Kill されていた。 ● PID1とNginxが⽣存しているため Containerは落ちず、OOMが Pod/ContainerのStatusに出ず観 測しづらい状態となっていた。 20
Slide 21
Slide 21 text
第⼆の謎 なぜGoがOOMでKillされたのか 21
Slide 22
Slide 22 text
⾒えないスパイクがあったと推測 ● 観測グラフ上では container memory limitの超過は確認できなかった ● KernelのOOM Logから、メモリの超過が発⽣したのは確実 ○ 短期間のメモリスパイクが発⽣したと推測 コンテナメモリの推移 メモリ 使用量 container memory limit 到達で即 OOM Kill 実際には⾒えていなかった推移 時間 22
Slide 23
Slide 23 text
スパイクの発⽣源は単⼀ではなかった 数⽇に1回コンテナが落ち、かつ規則性がないことから、単⼀の原因では なさそうだった。 ● 重い処理で⼀時的にメモリに負荷がかかる ● リクエストが急増する 上記が不定期に重なった際に、トラフィック起因の散発的なスパイクが発 ⽣し、それが原因でOOMが発⽣したと推測。 23
Slide 24
Slide 24 text
スパイクを防げなかった原因として GOMEMLIMITが未設定だった GOMEMLIMITとは、Go runtime に与える緩い上限。近づくとGCを積極的 に実⾏し、container memory limitの前でメモリを解放させられる。 container memory limit 到達で即 OOM Kill GOMEMLIMIT (GC line) メモリ 使用量 GOMEMLIMITに近づくとGCを 積極的に実⾏し、OOMを防ぐ 時間 24
Slide 25
Slide 25 text
なぜOOMが発⽣したかの調査結果 謎 調査結果 ● メトリクス上だとcontainer memory limitの超過は観測 できなかった。 ● GoのGCが⾛らなかった。 ● KernelのOOM Logと突き合わせ て、短期的かつ散発的なスパイ クが発⽣したと予想。 ● GOMEMLIMIT未設定が原因で memory limitの前にGCを発⽕さ せられなかった。 25
Slide 26
Slide 26 text
第三の謎 なぜGCが⾛らなかったのか 26
Slide 27
Slide 27 text
実はGOGC=500を設定していたが機能していなかった GOGCとは以下の計算式で算出される、次回のGC実⾏を決める指標。 次回GCの⽬標 = live heap ×(1 + GOGC/100) GOGC=500 live heap × 6 GCが稀に発⽕ (CPU負荷↓) メモリ 使用量 GOGC=100 live heap × 2 GCが頻繁に発⽕ (CPU負荷↑) live heap (前回GC後に⽣きていたheap) 時間 27
Slide 28
Slide 28 text
GCが⾛るより先にcontainer memory limit へ到達した可能性 CPU負荷を抑える狙いでGOGCを⼤きめにしていたが、そのせいで図のように、 GCの⽬標より先にコンテナの上限に達していた可能性がありそうだった。 GOGC=500で、次回にGCが⾛るライン(live heap × 6) メモリ 使用量 container memory limit ②GCが⾛るのはもっと上 ① OOM Kill 時間 28
Slide 29
Slide 29 text
なぜGCが⾛らなかったのかの調査結果 謎 調査結果 GOGCを設定しているのにもかか わらず、なぜOOM前にGCが⾛ら なかったのか。 1. live heapが増える 2. GOGC=500の相対条件 > container memory limit 3. GCより先にcontainer memory limitへ到達 4. Kernel OOM Kill 29
Slide 30
Slide 30 text
問題を解いていく 30
Slide 31
Slide 31 text
今まで起きていた問題を整理 問題 起きていた事象 NginxとGoが 同⼀コンテナに同梱 ● ● 解消法 コンテナメモリのメトリクス NginxとGoを別コンテナ に分離する。 が見にくい。 PID1がベースアプリにな らない。 Goがcontainer memory limitを 知らない Goはcontainer memory limitを知らずにheapを伸ば し続ける。 GOMEMLIMITを設定す る。 GCが相対値で決まる スパイク時にGCのラインが GOGC=offを設定する。 container memory limitを超え る。 31
Slide 32
Slide 32 text
今まで起きていた問題を整理 問題 起きていた事象 NginxとGoが 同⼀コンテナに同梱 ● ● 解消法 コンテナメモリのメトリクス NginxとGoを別コンテナ に分離する。 が見にくい。 PID1がベースアプリにな らない。 Goがcontainer memory limitを 知らない Goはcontainer memory limitを知らずにheapを伸ば し続ける。 GOMEMLIMITを設定す る。 GCが相対値で決まる スパイク時にGCのラインが GOGC=offを設定する。 container memory limitを超え る。 32
Slide 33
Slide 33 text
NginxとGoを別コンテナに分離する 1コンテナ1プロセス(PID1に割り当て)に分離。メモリやCPUといったメトリ クスを個別にトラッキングできるようにした。 副次的に今まで動いていなかったGraceful Shutdownも動くようになった。 Nginx Go App EKS Nginx Go App EKS 33
Slide 34
Slide 34 text
今まで起きていた問題を整理 問題 起きていた事象 NginxとGoが 同⼀コンテナに同梱 ● ● 解消法 コンテナメモリのメトリクス NginxとGoを別コンテナ に分離する。 が見にくい。 PID1がベースアプリにな らない。 Goがcontainer memory limitを 知らない Goはcontainer memory limitを知らずにheapを伸ば し続ける。 GOMEMLIMITを設定す る。 GCが相対値で決まる スパイク時にGCのラインが GOGC=offを設定する。 container memory limitを超え る。 34
Slide 35
Slide 35 text
GCのタイミングをGOMEMLIMITに⼀本化する 従来:GOGC=500 だけ 今回:GOGC=off + GOMEMLIMIT 次回GCの⽬標 = live heap ×(1 + GOGC/100) ● ● live heapに応じてGCのラインが 相対的に動く スパイク時にGCラインが container memory limitを超え うる。(GCより先にOOM Killが 発⽣) ● 相対的なGCラインは使⽤しない ● GOMEMLIMITに近づいたら、Go runtimeにGCを積極的に実⾏さ せる 35
Slide 36
Slide 36 text
Kubernetesのコンテナ上限から逆算してGCを制御 Kubernetesのcontainer memory limitの⼿前にGCラインを設定し、スパ イクが発⽣してもOOM前にGCを⾛らせる戦略。 GOMEMLIMIT(GC line) = container memory limit × 0.8 memory limit メモリ 使用量 GCが積極的に発動するエリア memory limit到達までの余白を作成 ※container momory.requests を applicationのmemory limitとしている GOMEMLIMIT(GC line) memory limit × 0.8 時間 36
Slide 37
Slide 37 text
上記の改善を⾏った結果 OOMが起きることもなくなり、メモリ使⽤量も安定し、リソースが有効 に活⽤される平⾏線のグラフを実現できた。 37
Slide 38
Slide 38 text
まとめ 38
Slide 39
Slide 39 text
まとめ 謎 原因 改善 第⼀の謎 Invisible OOM NginxとGoが同一コンテナに同梱。Go がOOM KillされてもContainerは生存 し、StatusにOOMが出ない。 NginxとGoを別コンテナに分離。 メトリクスを個別に監視。 第⼆の謎 なぜOOM? GOMEMLIMITが未設定。 Goがcontainer memory limitを認識 せず、memory limit前にGCを実⾏で きない。 GOMEMLIMITを設定。 container memory limitの⼿前 でGCを⾛らせる。 第三の謎 なぜGCが⾛ら ない? GOGC=500はlive heap基準の相対判 GOGC=off + GOMEMLIMITに 定。GCラインがcontainer memory limit 一本化。相対値ではなく上限基 を超え、GCより先にOOMが発生。 準でGCさせる。 39
Slide 40
Slide 40 text
We’re hiring! Career Site 40