状況
午前2時。PagerDutyが発火。クラスターを確認すると、Podが60〜90秒ごとに再起動を繰り返しています。kubectl get podsを実行すると、再起動回数が急速に増加しています。イベントを確認すると、これが見つかります:
Warning Unhealthy pod/api-server-7d9f4b8c6-xk2pq Liveness probe failed: Get "http://10.0.0.1:8080/healthz": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
Kubernetesはアプリが死んでいると判断し、コンテナを強制終了して再起動し、起動を待ちます。しかしアプリの準備が整う前に再びプローブが失敗します。このループは自然には解消されません。
原因
livenessプローブはコンテナにHTTP GETを送信します。timeoutSeconds(デフォルト:1秒)以内に応答がなければ失敗とみなされます。failureThreshold回(デフォルト:3回)連続で失敗すると、Kubernetesはコンテナを再起動します。
原因は4つあります。ほとんどは10分以内に修正できます。
- プローブのタイムアウトが短すぎる — 特に高負荷時やJVM/GCの停止中、アプリが
/healthzへの応答に1秒以上かかることがあります。 - アプリの起動が遅い — アプリが接続を受け付ける準備ができる前にプローブが実行されます。
initialDelaySecondsが設定されていないか、5秒のような楽観的な値に設定されています。 - リソース不足 — コンテナのCPUがスロットリングされています。プロセスがCPUサイクルを待っているため、ヘルスエンドポイントが時間内に応答できません。
- ヘルスエンドポイントの処理が多すぎる —
/healthzルートがDB接続の確認、クエリの実行、外部サービスの呼び出しを行っています。静かなサーバーでは問題ありませんが、実際の負荷がかかると致命的になります。
まず診断する
プローブの設定をまだ変更しないでください。実際に何が失敗しているかを確認します。
イベントと再起動回数を確認する
# 再起動回数を確認
kubectl get pods -n your-namespace
# プローブ失敗イベントを確認
kubectl describe pod api-server-7d9f4b8c6-xk2pq -n your-namespace | grep -A 20 Events
現在のプローブ設定を確認する
kubectl get deployment api-server -n your-namespace -o yaml | grep -A 20 livenessProbe
Pod内部からヘルスエンドポイントをテストする
# 再起動前にPodにexecで入る
kubectl exec -it api-server-7d9f4b8c6-xk2pq -n your-namespace -- sh
# エンドポイントにアクセスして時間を計測する
time wget -qO- http://localhost:8080/healthz
1秒以上かかっている場合、それが問題です。完全に失敗する場合は、ヘルスハンドラー自体にバグがあります。
リソースの圧迫を確認する
kubectl top pod api-server-7d9f4b8c6-xk2pq -n your-namespace
kubectl top node
CPUが上限に近い場合、スロットリングによってヘルスチェックを含むすべてのリクエストが遅延している可能性が高いです。
修正1:プローブのタイミングを調整する(最も一般的な修正)
Kubernetesのプローブのデフォルト値は、高速でシンプルなアプリ向けに設計されています。1秒のタイムアウトと10秒のインターバルは、ほとんどの本番サービスには厳しすぎます。デプロイメントを調整してください:
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30 # コンテナ起動後、最初のプローブまで30秒待つ
periodSeconds: 15 # 15秒ごとにチェック(10秒ごとではなく)
timeoutSeconds: 5 # 応答に5秒を許容(デフォルトの1秒ではなく)
failureThreshold: 3 # 3回連続失敗後に再起動
successThreshold: 1
適用するには:
kubectl apply -f deployment.yaml
JVMアプリはinitialDelaySeconds: 60以上が必要なことが多いです。Spring Bootはフルコンテキストのロードでコールドスタート時に45秒かかる場合があります。高負荷のアプリにはtimeoutSeconds: 5が安全な出発点です。それでも失敗が続く場合は10に上げてください。
修正2:startupProbeを追加する(Kubernetes 1.16以降)
起動の遅さはそれ自体が問題です。大きなinitialDelaySecondsで対処するのは推測に過ぎません。CIや低性能なノードでは間違った値になります。代わりにstartupProbeを使用してください。アプリが準備完了を通知するまでlivenessプローブを保留します:
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30 # 最大5分間試みる(30 * 10秒)
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
startupProbeが最初に実行されます。一度成功すると停止し、livenessが引き継ぎます。推測は不要です。
修正3:ヘルスエンドポイント自体を修正する
遅い/healthzは最も気づきにくい原因であることが多いです。不要なロジックを取り除いてください。livenessプローブの仕事はひとつ:プロセスが生きていることを確認することです。データベース接続の確認、キャッシュのping、外部APIの検証は/readyzでreadinessプローブの対象にすべきです。ここには含めません。
正しいlivenessエンドポイントはこのようになります:
# Go
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
# Node.js
app.get('/healthz', (req, res) => res.status(200).send('ok'));
# Python/FastAPI
@app.get("/healthz")
def healthz():
return {"status": "ok"}
5ミリ秒以内に200を返すべきです。そうでない場合は、そうなるまで削ぎ落としてください。
修正4:リソース制限を増やす
CPUスロットリングは他のすべてが正常に見えるため、見落としやすいです。コンテナがCPU制限に達すると、カーネルがスロットリングを行い、2msのヘルスチェックが2秒かかるようになります。これが発生しているか確認してください:
kubectl exec -it api-server-7d9f4b8c6-xk2pq -- cat /sys/fs/cgroup/cpu/cpu.stat | grep throttled
throttled_timeがゼロ以外で増加している場合、アプリのCPUが不足しています。制限を上げてください:
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m" # スロットリングされている場合はこれを上げる
memory: "512Mi"
まずCPU制限を2倍にして再デプロイし、throttled_timeを再確認してください。
修正5:TCPまたはExecプローブに切り替える
最もシンプルなプローブが正解なこともあります。TCPはポートが開いているかどうかを確認するだけです。HTTPのオーバーヘッドもなく、ハンドラーコードも不要です。Execはコンテナ内でコマンドを実行します:
# TCPプローブ — ポートがリッスンしているか確認
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
# Execプローブ — コンテナ内でコマンドを実行
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 10
Execパターンは、起動時に/tmp/healthyを書き込み、問題を通知したいときに削除するアプリに適しています。粗削りですが、信頼性があります。
修正を確認する
# Podが安定するのを監視する
kubectl get pods -n your-namespace -w
# 再起動回数が増えなくなったことを確認する
kubectl get pods -n your-namespace
# RESTARTSの列が増加しなくなるはずです
# イベントがクリーンであることを確認する
kubectl describe pod -n your-namespace | tail -20
# Unhealthyの警告が表示されないはずです
安定したと判断する前に、少なくとも3〜5回のプローブサイクルを待ってください。periodSeconds: 15の場合、ラップトップを閉じる前に約75秒間クリーンな出力が続くことを確認してください。
予防策
- デフォルトのプローブ値を本番環境に持ち込まないでください。 デフォルト(タイムアウト1秒、インターバル10秒)はデモ用であり、実際のワークロード向けではありません。サービスごとに調整してください。
- livenessとreadinessを分離してください。 liveness = プロセスが生きているか。readiness = トラフィックを受け付けられるか。混在させると、依存関係がダウンした際にカスケード再起動が発生します。
- デプロイ前にヘルスエンドポイントをロードテストしてください。
ab -n 1000 -c 50 http://localhost:8080/healthzを実行し、p99レイテンシーを確認してください。200msを超えている場合、Kubernetesに発見される前に修正してください。 terminationGracePeriodSecondsを設定してください。処理中のリクエストが完了するのに十分な時間を確保してください。ほとんどのAPIでは30秒が妥当な出発点です。- コードレビューでプローブの設定を確認してください。他の本番設定と同様に扱ってください。見落とすと、午前2時のページが発生します。

