Pod は Running なのに、なぜ Service からの応答が来たり来なかったりするのか?
kubectl get pods を叩くと全部 Running で緑色に見える。なのにデプロイ直後だけ一部のリクエストが timeout したり、起動に時間がかかるアプリだけがいつまで経っても安定せず再起動を繰り返したりする——そんな経験はないだろうか。原因の大半は liveness・readiness・startup という 3 つのヘルスチェックのうち、どれかの役割を混同しているところにある。
- liveness probe が失敗すると kubelet がコンテナを強制終了して再起動する一方、readiness probe が失敗すると EndpointSlice から Pod の IP が外れてトラフィックが来なくなるだけで、コンテナ自体は動き続ける。
- startupProbe を設定しないと、起動に
initialDelaySeconds + failureThreshold × periodSeconds(既定値では 0 + 3 × 10 = 30 秒)以上かかるアプリは、まだ起動中の状態を liveness probe に「死んでいる」と誤判定され再起動ループに陥る。 - Deployment の
minReadySecondsは既定 0 のため、readiness probe が一度成功した瞬間に Pod はローリングアップデート上「利用可能」と数えられてしまう。
「Running」はトラフィックを受け取れることを意味しない
kubectl get pods の STATUS 列に並ぶ Running は、コンテナプロセスが起動して動き続けていることを示すだけの表示だ。Pod が Running になった瞬間からリクエストを正しく処理できるとは限らない。DB接続の初期化中かもしれないし、キャッシュのウォームアップがまだ終わっていないかもしれない。
素朴には「デプロイのたびに Pod が再起動を繰り返すなら livenessProbe の設定を疑えばいい」と思うかもしれない。しかし実際の現場でつまずきやすいのは再起動そのものではなく、Pod は何ともないのに一部のリクエストだけ通ったり通らなかったりする、という現象の方だ。原因は Service が実際にトラフィックを転送する先の Pod IP 一覧である EndpointSlice にそのPodのIPが載っているかどうかにあり、載せるかどうかを決めているのが readinessProbe である。
liveness probe — 生きているかどうかだけを判定する
livenessProbe は、コンテナ内のプロセスがまだ応答できる状態かどうかを一定間隔で確認する。既定値は実行間隔を表す periodSeconds が10秒、応答待ちの上限を表す timeoutSeconds が1秒、連続失敗の許容回数を表す failureThreshold が3回で、この3回連続で失敗すると kubelet はコンテナを強制終了し、作り直す。デッドロックに陥って二度と応答しないプロセスを、外部から強制的に入れ替えるための仕組みだ。
livenessProbe に DB や外部APIへの疎通確認を書きたくなるが、これは避けたほうがよい。DB 側が一時的に落ちている間、そのDBに依存する全Podのlivenessが一斉に失敗して再起動され、再起動後もDBが直っていなければ同じ理由でまた失敗する。本来はDBの復旧を待てばよいだけの障害が、無関係な全Podの再起動の連鎖に変わってしまう。livenessは自分のプロセスが応答するかだけを見て、外部依存の生死には関与させないのが基本方針だ。
ヘルスチェック失敗
┌───────┴───────┐
▼ ▼
liveness失敗 readiness失敗
▼ ▼
kubeletが再起動 EndpointSliceから除外
▼ ▼
RESTARTS+1 Podは動いたまま
トラフィック停止のみ
新人研修中の社員を思い浮かべてほしい。研修期間(startupProbe)の間、上司はまだ電話を取り次がず、体調を気にして声をかけることもしない——独り立ちしていないと分かっているからだ。研修が明けると、日々の体調チェック(liveness)と、今日は電話に出られる状態かの確認(readiness)は別々に行われるようになる。体調チェックで深刻な不調が見つかれば長期の休職という重い対応(再起動)になるが、電話に出られないという確認は、その日だけ他の人に振り分ける程度の軽い対応で済む。
readiness probe — トラフィックの蛇口を開け閉めする
readinessProbe が失敗すると、EndpointSlice を管理するコントローラがそのPodのIPを、一致する全Serviceの EndpointSlice から取り除く。Pod自体はkillされず動き続けるし、kubectl get pods の RESTARTS も増えない。ただ Service 経由のトラフィックだけが来なくなる。原因(例えばDBへの接続が一時的に切れている)が解消されて再びreadinessProbeが成功すれば、IPは自動的にEndpointSliceへ戻る。
Deployment のローリングアップデートも readinessProbe の結果で進行が制御される。同時に停止してよいPod数を表す maxUnavailable と、同時に追加してよいPod数を表す maxSurge は既定でそれぞれ 25% だが、新しいPodがreadyになるまで古いPodの入れ替えは進まない。ただし、readyになってから「利用可能」と数えるまでの待ち時間を表す minReadySeconds の既定値は 0 のため、readinessProbe が一度成功した瞬間にそのPodは「利用可能」とカウントされる。起動直後だけ一瞬成功してすぐ失敗に転じるような不安定なPodでは、この既定のままだと安定を確認せずに次のPodの入れ替えへ進んでしまう。minReadySeconds に数十秒を設定すると、その秒数だけReady状態を維持できたPodだけを「利用可能」と数えるため、フラッピングするPodを入れ替えの母数から締め出せる。
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 5
minReadySeconds: 30 # 既定0 → 安定確認なしに進む
startupProbe — 遅い起動を liveness の誤爆から守る
startupProbe を設定すると、それが成功するまで liveness と readiness のプローブ自体が実行されない。起動に時間がかかるアプリケーションが、まだ立ち上がっている途中の状態を liveness に「死んでいる」と誤判定されて再起動され、再起動のたびにまた起動待ちからやり直す——というループを防ぐための仕組みだ。
startupProbe が必要かどうかの目安は initialDelaySeconds + failureThreshold × periodSeconds という式で計算できる。既定値のまま(0 + 3 × 10 = 30秒)このアプリの起動が30秒を超えるなら、livenessProbe だけでは起動完了前に失敗としてカウントされ切ってしまう。JVM を使うアプリなど起動に数十秒から数分かかる場合は、livenessと同じエンドポイントを見る startupProbe を failureThreshold 大きめに設定して用意し、起動を待つ猶予をそちらに持たせるのが定石だ。
Rails や Go のアプリに /healthz のようなヘルスチェック用エンドポイントを1つだけ用意し、それを liveness と readiness の両方に使い回す Deployment manifest をよく見かける。DBへの接続確認までそのエンドポイントに詰め込んでしまうと、DBが数分落ちただけで依存する全Podが再起動を繰り返し、DB復旧後もPodの起動待ちが重なって復旧がさらに遅れる。プロセスの生死だけを見る軽いエンドポイントをlivenessに、DBやキャッシュへの接続確認を含む重いエンドポイントをreadinessに、と分けて書くのが実務上の定石になっている。
readinessProbe が失敗している間、Pod は動き続けたまま Service の転送先から外れることを kind クラスタで確認する。
kind create cluster --name probe-demo
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: web
labels: { app: web }
spec:
containers:
- name: nginx
image: nginx:1.27
ports: [{ containerPort: 80 }]
readinessProbe:
httpGet: { path: /secret.html, port: 80 }
periodSeconds: 2
failureThreshold: 1
EOF
kubectl expose pod web --port=80 --name=web-svc
sleep 5
echo "--- ファイルが無い状態 ---"
kubectl get pod web
kubectl get endpoints web-svc
kubectl exec web -- sh -c 'echo ok > /usr/share/nginx/html/secret.html'
sleep 3
echo "--- ファイルを作った後 ---"
kubectl get pod web
kubectl get endpoints web-svc
kind delete cluster --name probe-demo
最初の kubectl get pod web は READY が 0/1 で、web-svc の ENDPOINTS は空のはずだ。/secret.html を作った後は READY が 1/1 になり ENDPOINTS に Pod の IP が現れる。この間 RESTARTS の列はずっと 0 のままで、readinessProbe の失敗が一度も Pod を再起動していないことが分かる。
- readiness probe が失敗したら、そのPodも再起動される — 実際にはEndpointSliceからの除外のみでコンテナは動き続ける。原因が直らない限りいつまでもトラフィックが来ないだけで、Podの再起動カウントは増えない。
- livenessProbe にDBやRedisへの疎通チェックを書けば安全性が上がる — 実際にはDB側が落ちている間、依存する全PodのlivenessProbeが一斉に失敗し次々と再起動される。DBが直っていなければ再起動後も同じ理由で失敗するため、無関係な再起動の連鎖がDB復旧後の負荷をさらに増やす。外部依存の疎通確認はreadinessProbeの役割であり、livenessは自プロセス内で完結する軽い確認に留めるのが定石だ。
- startupProbeを省略しても、initialDelaySecondsを長めに設定すれば同じことだ — initialDelaySecondsは最初のプローブ実行までの一律の待ち時間で、起動が早く終わったPodも同じ秒数待たされる。startupProbeは実際に成功するまで動的に待つため、起動時間がばらつくアプリでは無駄な待ちを作らずに済む。
- liveness probe
- プロセスが応答できる状態かを確認し、失敗が続くとkubeletがコンテナを再起動するプローブ。
- readiness probe
- トラフィックを受け取れる状態かを確認し、失敗するとEndpointSliceからPodが外れるプローブ。
- startupProbe
- 成功するまでliveness/readinessの実行を止め、起動が遅いアプリを誤判定から守るプローブ。
- EndpointSlice
- Serviceが実際にトラフィックを転送する先のPod IPの一覧を管理するリソース。
- minReadySeconds
- Podがreadyになってからローリングアップデート上「利用可能」と数えるまでの待ち時間。既定は0。
- Liveness, Readiness and Startup Probes | Kubernetes — 3種のプローブの定義・既定値・EndpointSliceとの関係を示す一次情報。
- Configure Liveness, Readiness and Startup Probes | Kubernetes — 実際にmanifestを書いてkubectl describeで挙動を確認する手順。
- Deployments — Min Ready Seconds | Kubernetes — minReadySecondsとローリングアップデートの進行の関係。