KubernetesのResource Limit設定を甘く見て、深夜にアラートで叩き起こされた話
数ヶ月前、自社サービスのバックエンドをDocker Composeでの運用からKubernetes(EKS)へ移行した。コンテナ化自体はそれほど難しくなかったし、デプロイの自動化も進んで「これで運用が楽になるぞ」と、かなり手応えを感じていた。
ところが、移行して数週間経ったある日の深夜。PagerDutyのアラートで叩き起こされた。Podが次々と `CrashLoopBackOff` を繰り返しているという内容だ。
ログを確認しても、アプリケーション自体は正常に起動プロセスを進めているように見える。エラーメッセージすら出ていない。まるで、何かに突然断ち切られたかのように、プロセスが忽然と消えているのだ。
結局、原因はKubernetesの `Resource Limits` 設定にあった。
「とりあえず」で設定したLimitの罠
Docker Composeの時代は、物理マシンのリソースをフルに使えるので、メモリやCPUの制限をあまり気にしなくてよかった。しかし、Kubernetesでは `requests` と `limits` の設計が運用の肝になる。
今回、私は「念のため」という理由で、メモリの `limits` を少し低めに設定していた。これが致命的だった。JavaやNode.jsといったランタイムは、起動時にある程度のメモリを確保しようとする。その際、Kubernetes側の `limits` に達してしまうと、OS(カーネル)のOOM Killerによってプロセスが強制終了させられる。
このとき、アプリケーション側には「メモリが足りない」というエラーログすら吐かせることができない。これが、デバッグを困難にする最大の要因だ。
以下に、問題が発生した時のマニフェストの一部を載せておく。
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-api
spec:
template:
spec:
containers:
- name: api-container
image: my-app:latest
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
# ここが小さすぎた。起動直後にOOMKilledされる原因。
memory: "512Mi"
cpu: "500m"
`requests` を低く設定しすぎると、ノードの空きリソースを過信してしまい、負荷が増えた瞬間にPodが不安定になる。逆に `limits` を小さくしすぎると、今回のようにプロセスが不意に死ぬ。
調査から学んだこと
このトラブルを解決するために、まずは `kubectl describe pod [pod-name]` で状態を確認した。そこでようやく `Reason: OOMKilled` という文字を見つけたときは、正直ゾッとした。
ここから、実際のメトリクス(CloudWatch Container Insights)を遡って、アプリケーションのメモリ使用量の推移と、Podが死んだタイミングを照らし合わせる作業に入った。
経験上、コンテナ化におけるリソース設計には以下の3つのポイントが重要だと痛感している。
まず、`requests` は「最低限これだけは保証してほしい」という値として、実際の起動時・稼動時のベースラインに基づいて設定すること。そして `limits` は、スパイク(急激な負荷増)を許容できるだけのバッファを持たせること。
次に、言語特有の挙動を考慮することだ。例えばJVMを使っている場合、`-Xmx` の設定とKubernetesの `limits.memory` が矛盾していると、コンテナ全体が死ぬ。コンテナ内のメモリ使用量だけでなく、ランタイムが認識しているヒープサイズとの整合性を取る必要がある。
最後に、監視体制。単に「Podが生きているか」を見るだけでなく、`container_memory_working_set_bytes` と `container_spec_memory_limit_bytes` の差分をグラフ化しておくべきだった。これさえ見ていれば、限界に達する前に予兆を察知できたはずだ。
結局、設定を見直して `limits` を十分に引き上げ、負荷試験を通じて適切なラインを見極めるまで、数日間は神経を使う運用が続いた。コンテナオーケストレーションは強力だが、その分、インフラ層とアプリケーション層の両方の知識がないと、こうした「音のない死」に対処できない。