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