目次 ▼
12 章

Health check

K8s がコンテナが生きているか、そしてトラフィックを受ける準備ができているかをどう判断するかのモデルを扱います。liveness · readiness · startup の3つの probe の役割分離、httpGet · tcpSocket · exec の検査方式、initialDelaySeconds · periodSeconds · failureThreshold のようなパラメータチューニング、liveness に外部依存を入れたときの cascading failure、terminationGracePeriodSeconds と preStop フックの graceful shutdown までを一連の流れで整理します。

第11章 resources.requests / limits まで私たちがつかんだのは、Pod に どれだけのリソースを与えるか のモデルでした。CPU · メモリの requests と limits でスケジューラと cgroup がその Pod をどんな条件に置くかが決まります。しかしリソースが十分だからといって、そのコンテナが 本当に仕事をしているか は別の話です。プロセスは立ち上がっているのに中でデッドロックがかかっていることもあり、コンテナは始まったばかりだが DB コネクションプールがまだ満たされていなくてトラフィックを受けてはいけない状況もあります。本章では K8s がこの2つの質問 — 「生きているか」 と 「トラフィックを受ける準備ができたか」 — をどう判断するか、そしてその判断の根拠になる3種類の probe を一連の流れで整理します。

本章の終わりには Pod マニフェストに刺さった3つの probe ブロックと terminationGracePeriodSeconds、preStop がどんな運用シナリオを防いでいるか を一行で読み取れる状態になります。

なぜ3つの probe に分けるのか #

probe を初めて見ると自然な疑問が一つ生まれます — 「コンテナが生きているか一つだけ見ればいいのではないか?」運用の視点でこの質問が単純でない理由は、「生きている」という一語の中に互いに異なる2つの意味が混ざっているから です。プロセスは立ち上がっていて OS 次元ではまともですが、その中でキャッシュを満たし切れずトラフィックを受けると即座に 502 を吐く状態があります。そのコンテナにとって「再起動すべきか」の答えは「いいえ」で、「トラフィックを送ってよいか」の答えは「まだ」です。2つの答えが異なります。

K8s はこの2つの答えを別のオブジェクトに分けました — liveness と readiness です。そして起動の遅いアプリのための保護者を一層さらに置きました — startup です。3つの probe の役割を一表に整理すると次のとおりです。

probe問う質問失敗時の K8s の行動影響範囲
livenessこのコンテナが生きているかそのコンテナを再起動そのコンテナ1個
readinessこの Pod がトラフィックを受ける準備ができたかService Endpoints からその Pod を除外トラフィックルーティング
startupこのコンテナが起動を終えたかそのコンテナを終了 (そして restartPolicy に従って再起動)コンテナ起動段階

3つの probe の決定的な違いは 失敗がどんな結果につながるか です。liveness 失敗はコンテナの再起動を、readiness 失敗はトラフィックの遮断を、startup 失敗は起動段階の終了を引き起こします。この結果の違いを知らずにマニフェストを書くと、「コンテナは生きているのに 502 が出る」とか「うまく回っていたアプリが無限再起動に陥る」のような事故に直結します。

コンテナ再起動と Pod 再生成は別の話 #

よく混同する部分を一つ先に押さえます。liveness 失敗の結果は コンテナ再起動 であって Pod 再生成 ではありません。Pod はそのまま生きていて、その中のコンテナだけが終了したあと同じ Pod で再び始まります。kubectl get pods の RESTARTS カラムが 1、2、3 のように上がるのがその信号です。Pod 自体が別のノードへ移ったり新しい IP を受け取ったりはしません。一方 readiness 失敗はコンテナを 触りません — 生きているコンテナそのままで、ただ 第5章 Service の Endpoints の一覧から除外されてトラフィックが入ってこないだけです。

3つの検査方式 — httpGet / tcpSocket / exec #

3つの probe すべてが同じ3つの検査方式のうち一つを選べます。それぞれ適したシナリオとコストが異なります。

方式動作適したワークロードコスト
httpGet指定された path / port へ HTTP GET。200 ~ 399 の応答なら成功HTTP サーバ (ほとんどの Web · API)低い
tcpSocket指定されたポートへ TCP 接続を試行。接続できれば成功非 HTTP サーバ (DB、gRPC の一部、Redis)非常に低い
execコンテナの中でコマンドを実行。exit 0 なら成功任意のスクリプトで検査が必要なワークロード高い (新しいプロセスを fork)

httpGet — 最もよくある選択 #

ほとんどの Web · API サーバには httpGet が最初の候補です。

httpGet probe — 抜粋
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
    httpHeaders:
      - name: X-Probe
        value: kubelet
  initialDelaySeconds: 10
  periodSeconds: 10

/healthz というパスは K8s エコシステムのコンベンションです — プロジェクトのコードで /health、/healthz、/ping、/-/healthy のような名前をよく見るようになります。応答コードが 200 ~ 399 の範囲なら成功と判定し、4xx · 5xx なら失敗です。応答本文は見ません。

httpGet の長所は アプリのコードが自分の状態を直接表現できる という点です。単に「プロセスが立ち上がっている」ではなく「DB コネクションプールが正常だ」「キャッシュが満たされた」のような意味を 200 / 503 に分けて応答できます。

tcpSocket — ポートさえ開いていればよい #

HTTP でないサーバには tcpSocket が自然な選択です。

tcpSocket probe — 抜粋
readinessProbe:
  tcpSocket:
    port: 5432
  initialDelaySeconds: 5
  periodSeconds: 10

PostgreSQL、MySQL、Redis のような非 HTTP サーバがよくある適用対象です。K8s がそのポートへ TCP 3-way handshake を試みて成功すれば OK、失敗すれば NG です。ただし TCP 接続ができるからといってそのサーバが本当にクエリを処理できるという意味ではありません — Postgres が始まったばかりで listen はするがまだ startup が終わっていない状態でも TCP 接続はできます。だからデータベースワークロードの readiness には tcpSocket より exec で pg_isready のようなコマンドを回す方がより正確です。

exec — 任意のコマンドで検査 #

特定のコマンドでしか表現できない検査は exec を使います。

exec probe — 抜粋
readinessProbe:
  exec:
    command:
      - /bin/sh
      - -c
      - pg_isready -h 127.0.0.1 -p 5432
  initialDelaySeconds: 5
  periodSeconds: 10

exec はコンテナの中で新しいプロセスを fork してコマンドを実行し、その exit code が 0 なら成功です。最も柔軟ですが コストが最も高いです。fork 自体が小さくない作業で、そのコマンドが sh を経て再びクライアントバイナリを立ち上げる形なら毎回の検査が重いです。1分に1回ずつ回る検査でもコンテナが数百個ならば負荷が累積します。できる限り httpGet を優先して検討し、その道が塞がれたとき tcpSocket や exec を選ぶ順序が運用の標準です。

共通パラメータ — 時間としきい値 #

3つの probe は同じ時間パラメータを共有します。一表に整理します。

フィールド意味基本値
initialDelaySecondsコンテナが始まったあと最初の検査まで待つ時間0
periodSeconds検査周期10
timeoutSeconds一度の検査が応答を待つ時間上限1
failureThreshold連続失敗何回で最終失敗とみなすか3
successThreshold連続成功何回で最終成功とみなすか (liveness / startup は 1 に固定)1

この5つの値が1つの probe の動作を完全に決めます。例えば periodSeconds: 10、failureThreshold: 3 なら、最大30秒間連続失敗が続いて初めて K8s がその probe を本当の失敗とみなすという意味です。timeoutSeconds: 1 は一度の検査が1秒以内に応答しなければその回を失敗として処理するという意味です。

基本値が運用でそのまま使うには 攻撃的すぎる 場合がよくあります。特に timeoutSeconds: 1 は GC が少し長くなったりノードの負荷が一瞬上がった瞬間にも失敗に落ちます。liveness にその基本値がそのまま入っていると、一時的な応答遅延がコンテナ再起動につながる事故に直結します。第11章 で見た CPU throttling も同じ時点に応答遅延を大きくする原因なので、リソースモデルと probe パラメータが互いに絡んで事故を作ります。運用マニフェストではほぼ常に timeoutSeconds を 3 ~ 5秒に増やし、failureThreshold も 3 ~ 5 程度にする方が安全です。

liveness probe — 生きているか #

liveness probe の役割は 死んでいるのに死んでいないふりをするコンテナ を探し出すことです。プロセスは立ち上がっているのにデッドロックに陥ってどんなリクエストにも応答できない状態、メモリリークで応答時間が無限大に伸びた状態のようなものがその対象です。liveness が失敗すると K8s はそのコンテナを SIGTERM → タイムアウト時 SIGKILL で終了し、Pod の restartPolicy に従って再び立ち上げます。Deployment の restartPolicy の基本値は Always なので、ほとんどすべてのワークロードで自動再起動が付いてきます。

liveness probe — Deployment 抜粋
spec:
  template:
    spec:
      containers:
        - name: web
          image: myapp:1.4.0
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3

このマニフェストの意味は次のとおりです。

  • コンテナが始まったあと30秒間は検査しません (initialDelaySeconds)。
  • それ以降は10秒ごとに /healthz を呼びます (periodSeconds)。
  • 一度の呼び出しが3秒以内に応答しなければその回を失敗として処理します (timeoutSeconds)。
  • 連続3回失敗すると (failureThreshold) liveness 失敗とみなし、コンテナを再起動します。

検査が本当に失敗してコンテナが再起動されると、kubectl describe pod のイベントと kubectl get pods の RESTARTS カウントに痕跡が残ります。

liveness 失敗後 — kubectl describe pod
Events:
  Type     Reason     Age   From     Message
  ----     ------     ----  ----     -------
  Warning  Unhealthy  2m    kubelet  Liveness probe failed: HTTP probe failed with statuscode: 503
  Normal   Killing    2m    kubelet  Container web failed liveness probe, will be restarted
  Normal   Pulled     2m    kubelet  Container image "myapp:1.4.0" already present on machine
  Normal   Created    2m    kubelet  Created container web
  Normal   Started    2m    kubelet  Started container web

Liveness probe failed というイベントとその直後の Container ... will be restarted が1まとまりで出る形が標準です。この痕跡がよく見える Pod は liveness probe を疑わなければなりません — 本当にコンテナがよく死ぬのか、それとも probe が攻撃的すぎてまともなコンテナを殺しているのかを見分けなければなりません。診断木の完成版は 第27章 kubectl デバッグパターン で整理します。

liveness に何を入れるべきか #

この部分が運用で最も事故が多く起きる地点です。結論から書くと — liveness probe は自分のプロセスの状態だけを見るべきです。 外部依存 (DB、キャッシュ、別のマイクロサービス) を liveness に入れてはいけません。

理由は cascading failure のためです。DB が一時的にダウンしたときにすべてのアプリコンテナの liveness が同時に失敗して同時に再起動に入ると、DB が復旧してもアプリたちはしばらくの間再び立ち上がれません。さらにひどい場合、再起動されたアプリがまた DB に届かずまた liveness が失敗し、再び再起動される無限ループに陥ります。liveness は自分の中の状態だけ、外部依存は readiness で — この分離を最初から固めて進む方が安全です。

/healthz エンドポイントは普通次の程度だけ見ます。

  • アプリのプロセスが応答を作れる (HTTP ハンドラまで到達した)。
  • 自分の中のデッドロック検知が OK です。

DB ピングや外部サービスの呼び出しはこのエンドポイントに絶対に入れないのが運用の標準です。

readiness probe — トラフィックを受ける準備ができたか #

readiness probe の役割は トラフィックルーティングのゲート です。liveness と違い readiness はコンテナを殺しません — 代わりにその Pod を Service の Endpoints の一覧から除外します。結果的にその Pod へ新しいリクエストが入ってこなくなります。

readiness probe — Deployment 抜粋
spec:
  template:
    spec:
      containers:
        - name: web
          image: myapp:1.4.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /readyz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
            successThreshold: 1

/readyz は /healthz とは別のエンドポイントにするパターンがよくあります。2つのエンドポイントが見るものが異なるからです。

  • /healthz (liveness) — 自分のプロセスの状態だけ
  • /readyz (readiness) — 自分のプロセス + DB ピング + キャッシュ接続 + 依存する外部サービスの状態

readiness が失敗した Pod は死なずに生きていますが、トラフィックだけが一時的に切れます。DB 接続が一時的にできない間 readiness が false になってトラフィックが遮断され、DB が復旧すると readiness が再び true に戻ってトラフィックが再び流れ込みます。コンテナの再起動なしで一時的な障害を吸収するモデル です。

Endpoints から外れる形の確認 #

readiness 失敗が起きたとき Endpoints (またはその後続オブジェクトである EndpointSlice) がどう変わるかを短く見ます。

Service と Endpoints
kubectl get svc web
kubectl get endpoints web
readiness がすべて正常なとき
NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
web    ClusterIP   10.96.123.45    <none>        80/TCP    1d

NAME   ENDPOINTS                                       AGE
web    10.244.1.10:8080,10.244.1.11:8080,10.244.2.5:8080   1d

3つの Pod の IP がすべて Endpoints に入っているのが正常状態です。1つの Pod の readiness が失敗に落ちると、その IP だけが Endpoints から外れます。

1つの Pod の readiness が false のとき
NAME   ENDPOINTS                                       AGE
web    10.244.1.10:8080,10.244.2.5:8080                1d

Service がその Pod へトラフィックを送りません。kubectl get pods ではその Pod が READY 0/1 状態で見え、コンテナは依然として Running です。

kubectl get pods — readiness 失敗時
NAME           READY   STATUS    RESTARTS   AGE
web-7c4d-aa1   1/1     Running   0          1d
web-7c4d-bb2   0/1     Running   0          1d
web-7c4d-cc3   1/1     Running   0          1d

READY カラムの 0/1 が核心です。コンテナは1個立ち上がっているのにそのうち0個が ready という意味で、この状態で RESTARTS は増えません。

Pod の中にコンテナが複数あるなら #

1つの Pod にコンテナが複数あってそのうち1つの readiness が false なら、Pod 全体の ready が false になって Endpoints から外れます。2つのコンテナがまともでも1つのコンテナだけ readiness が落ちると Pod 全体にトラフィックが入ってこない形です。意図された動作です — Pod は K8s のルーティング単位で、その中の1つの部品が準備できていないならその Pod へトラフィックを送らない方が安全だからです。

startup probe — 起動が遅いアプリの保護者 #

3つ目の probe である startup は 1.16 で beta、1.18 で stable になった比較的新しいオブジェクトです。解いてくれる問題が明確です — 起動が遅いアプリ です。

Java / Spring Boot、Rails、大きな ML モデルをメモリに載せるワークロードは起動に60秒以上かかることがよくあります。こういうアプリに startup probe なしで liveness だけを置くとどんなことが起こるかを追いかけてみます。アプリが起動に60秒かかるのに liveness の initialDelaySeconds: 10 なら — 起動10秒の時点から K8s が /healthz を呼び、アプリはまだ応答できないので失敗が累積して結局コンテナが死にます。K8s が再び立ち上げても同じことが繰り返されて無限再起動に陥ります。

回避策は initialDelaySeconds を90秒や120秒のように大きくすることですが、そうすると新しい問題が生まれます — アプリが本当に死んだときもその分だけ遅く検知 されます。運用中にデッドロックがかかっても最初の90秒間は無防備です。起動時間に合わせて initialDelaySeconds を大きくした代償が、普段の運用の感度の低下として返ってきます。

startup probe がこの分離をきれいに解きます。startup が成功するまで liveness と readiness は非アクティブ で、startup が一度成功するとそれ以降は startup が再び動作せず liveness / readiness が正常周期で回ります。

startup probe — 抜粋
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 10
  failureThreshold: 30

上のマニフェストは 最大5分 (10秒 × 30回) を起動に許容するという意味です。5分以内に一度でも /healthz が 200 を応答すれば startup が成功と判定され、それ以降 startup probe はもう動作しません。その次からは liveness / readiness が普段の周期で回ります。5分が過ぎても一度も成功しなければ startup 失敗と判定されてコンテナを終了させ、restartPolicy に従って再び立ち上げます。

核心の公式は単純です。failureThreshold × periodSeconds が起動に許容される最大時間 です。Spring Boot が平均60秒かかってたまに90秒かかるなら failureThreshold: 12 × periodSeconds: 10 = 120秒にする、という計算が一般的です。

よくある運用事故シナリオ3つ #

3つの probe のモデルを知った状態で、運用でよく出会う事故3つを押さえておきます。この3つを避けるだけでも health check 関連の事故の大きな部分が消えます。

事故1 — liveness だけあって readiness がない #

最もよくある1つ目の事故です。マニフェストに liveness だけ書いて readiness を書かないと、K8s は コンテナが始まるとすぐにその Pod を ready と判定 します。Service の Endpoints に即座にその Pod が追加されてトラフィックが入ってきます。

問題はコンテナが始まったばかりのときです。プロセスは立ち上がっていて listen も始めているが、DB コネクションプールがまだ満たされていなかったりキャッシュが事前ロードされていない状態です。その時点で入ってきたトラフィックが 502 を吐き、ユーザが影響を受けます。ローリングアップデート中に毎回短い 502 burst が見えたら readiness の欠落を最初に疑わなければなりません。

解決は単純です — readiness probe を追加し、その中の /readyz エンドポイントが DB ピング · キャッシュ状態を見て 200 / 503 に分けて応答するようにします。すると、その Pod が本当にトラフィックを受ける準備ができるまで Endpoints に入りません。

事故2 — liveness が攻撃的すぎる #

2つ目の事故は liveness のパラメータ自体にあります。基本値 timeoutSeconds: 1 のままで運用を始めると、DB が一時的に遅くなったり GC が長くなった瞬間に health チェックが1秒以内に応答できず失敗します。連続3回失敗するとコンテナ再起動が発動し、再起動直後のコンテナはまた GC を回してまた応答が遅れてまた失敗します。

このサイクルが一度始まると止めるのが難しいです。運用者がマニフェストで timeoutSeconds を増やすまで同じパターンが繰り返されます。運用マニフェストの liveness は timeoutSeconds: 3 ~ 5、failureThreshold: 3 ~ 5 程度にして始める方が安全です。

事故3 — DB ピングを liveness に入れた #

3つ目の事故はモデルの分離を知らずにマニフェストを書いたときに起こります。/healthz が DB ピングまで見て false を応答するようにしておくと、DB がほんの少しダウンしても すべてのアプリコンテナの liveness が同時に失敗 して同時に再起動に入ります。

DB が30秒で復旧してもアプリたちはしばらくの間再び立ち上がれません — 自分の起動時間が30秒以上かかるアプリならその時間だけ 503 がさらに長くなります。さらにひどくは、アプリが再び立ち上がってもその間に DB がまた揺れるとまた死に、また立ち上がる cascading failure に陥ります。

ルールは一行です。liveness は自分のプロセス、readiness は外部依存。 DB · キャッシュ · 別のマイクロサービスのような外部依存はどこに入れるべきですか? readiness です。DB がダウンすると readiness が false になってトラフィックが遮断され、DB が復旧すると readiness が true に戻ってトラフィックが再び流れます。コンテナは死なないので cascading failure も起こりません。

probe と graceful shutdown #

probe のモデルの上に一層さらに載るテーマが graceful shutdown です。Pod が終了するとき進行中だったリクエストが 502 にならないようにするには、トラフィックを先に切り、その次にコンテナを殺さなければなりません。 K8s はこの流れを次の段階で進めます。

  1. Pod が Terminating 状態に入ります。
  2. K8s がその Pod の IP を Endpoints から除去します (トラフィック遮断の開始)。
  3. 同時にコンテナに SIGTERM を送ります。
  4. terminationGracePeriodSeconds (基本30秒) 以内にコンテナが終了するのを待ちます。
  5. 時間が過ぎても死ななければ SIGKILL で強制終了します。

ここで微妙な部分は 2段階と3段階がほぼ同時に起こる という点です。Endpoints の更新は K8s コントロールプレーンを経て各ノードの kube-proxy に伝播される間に時間がかかりますが、SIGTERM は即座に到達します。結果的に SIGTERM を受けたコンテナが終了を始めたばかりなのに、まだ Endpoints の更新が広がり切らずにその Pod へ最後のリクエストがいくつかさらに入ってくる時間の窓 (window) が生まれます。そのリクエストたちが終了中のコンテナに届いて 502 になります。

PreStop フックで時間の窓を埋める #

この空白を埋めるツールが lifecycle.preStop フックです。SIGTERM を受ける前に K8s が先に実行してくれるコマンドで、普通短い sleep を置いて Endpoints の更新がすべて広がる時間を稼ぎます。

preStop フック — 抜粋
spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: web
          image: myapp:1.4.0
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

上のマニフェストの流れは次のとおりです。

  1. Pod が終了開始 → K8s が Endpoints から除去。
  2. K8s が preStop フックを実行 → 10秒間 sleep。
  3. その10秒の間に Endpoints の更新がクラスタ全体へすべて広がります — 新しいトラフィックが入ってきません。
  4. preStop が終わると K8s がコンテナに SIGTERM を送ります。
  5. コンテナが自分の中でインフライトのリクエストたちを処理してきれいに終了します。
  6. 終了が終わらなければ terminationGracePeriodSeconds (60秒) のあとに SIGKILL。

terminationGracePeriodSeconds は preStop の時間まで含みます。 つまり上の例で60秒のうち10秒が preStop に、残りの50秒が SIGTERM 後の終了に使われます。preStop を20秒にすると SIGTERM 以降の時間が40秒に減るので、2つの値を一緒に調整しなければなりません。

PodDisruptionBudget と一緒にノードアップグレード時の安全な終了の流れの本格的な運用マニュアルは 第30章 アップグレード戦略 で扱います。

SIGTERM をアプリが直接処理する #

別の道で同じ効果を出す方法もあります。アプリが SIGTERM を受けると自分の readiness エンドポイントが false を応答するようにコードに書いておくパターンです。SIGTERM が入ってくるとすぐに /readyz が 503 を応答し始め、まもなく K8s が次の readiness 検査でその Pod を Endpoints から外します。その間にインフライトのリクエストたちを処理し切って終了します。

この方式は PreStop フックなしでもきれいな graceful shutdown を作ります。ただしアプリのコード次元の SIGTERM ハンドラが正確に動作しなければならないという前提があります — コンテナの PID 1 が SIGTERM を無視すると readiness false 処理も起こりません。コンテナイメージの ENTRYPOINT が tini や dumb-init のような init ツールを経るパターンがこの問題を防いでくれます。

総合マニフェスト #

3つの probe と graceful shutdown を1つのマニフェストに集めた例です。Java Spring Boot アプリを仮定しました。

full-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-api
  template:
    metadata:
      labels:
        app: order-api
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: order-api
          image: myorg/order-api:2.3.0
          ports:
            - name: http
              containerPort: 8080
          resources:
            requests:
              cpu: "500m"
              memory: "512Mi"
            limits:
              memory: "1Gi"
          startupProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            periodSeconds: 10
            failureThreshold: 18
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
            successThreshold: 1
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

マニフェストに書かれている意図を一行ずつ押さえると次のとおりです。

  • startup probe: 起動に最大180秒 (10秒 × 18回) を許容。Spring Boot の平均起動時間 + 余裕。
  • liveness probe: startup が成功したあとから動作。/actuator/health/liveness は自分のプロセスの状態だけを見ます (DB ピングなし)。
  • readiness probe: /actuator/health/readiness が DB コネクションプールと外部依存を見ます。DB が一時的にダウンすると readiness が false になってトラフィックが遮断され、コンテナは生きています。
  • preStop sleep 10秒 + terminationGracePeriodSeconds 60秒: graceful shutdown の時間の窓を十分に確保。

Spring Boot 2.3+ の actuator が liveness と readiness エンドポイントを標準で分けて提供してくれるので、こうした設定を比較的簡単に適用できます。他のフレームワークでも同じ分離 (自分の状態 / 外部依存) を2つのエンドポイントでコード次元に作っておくパターンが運用の標準です。

Docker HEALTHCHECK と K8s probe #

Docker の HEALTHCHECK インストラクションと K8s probe の関係を一度整理しておきます。

Dockerfile の HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8080/healthz || exit 1

このインストラクションは docker run でコンテナを直接立ち上げるとき Docker デーモンがその検査を回すモデルです。docker ps の STATUS カラムに (healthy) / (unhealthy) の表示が出て、Docker Compose の depends_on.condition: service_healthy のような所でもこの値を見ます。

K8s はこの HEALTHCHECK の値を無視します。 K8s が見るのは Pod マニフェストの livenessProbe / readinessProbe / startupProbe だけです。同じイメージを K8s に載せると Dockerfile の HEALTHCHECK はただ無視され、マニフェストに probe を別途書かなければなりません。2つのモデルが似て見えますが別の層で動作すると見ればよいです — 1つのコンテナの検査は Docker が、K8s ワークロードの検査は K8s が責任を持ちます。

イメージ自体が両方で使われ得るなら2か所に同じ意図の検査が書かれていても問題ありません — ただし K8s マニフェストの probe が本当に動作する検査 だという点は明確にしなければなりません。docker-compose の healthcheck と K8s probe のより詳しいマッピングは 付録A docker-compose から k8s へ で整理します。

練習問題 #

  1. 上の本文の full-deployment.yaml を仮定した状態で、わざと /actuator/health/liveness が 503 を応答するようにするシナリオをメモに整理してみてください。K8s がどの段階 (probe 失敗カウント → Killing → Pulled → Created → Started) を経るかを時間順に書き、同じ時点に RESTARTS カウントがどう変わるかを §「liveness probe — 生きているか」のモデルと合わせて整理します。
  2. DB ピングをわざと liveness に入れたマニフェストと、同じ DB ピングを readiness にだけ入れたマニフェストの2つを仮定して比較します。DB が30秒間ダウンしてから復旧するシナリオで2つの形がそれぞれどう動作するか (cascading failure vs トラフィックだけ一時遮断) を一段落で比較整理し、§「よくある運用事故シナリオ3つ」の事故3 とどうつながるかをメモします。
  3. 本章の terminationGracePeriodSeconds: 60 + preStop: sleep 10 の組み合わせを仮定した状態で、preStop を sleep 70 に変えたときどんなことが起こるかを §「PreStop フックで時間の窓を埋める」の時間モデルで推論します。SIGTERM 以降コンテナが自分の作業を終える時間がどれだけ残るか、そして terminationGracePeriodSeconds とどう調整すべきかを一段落で整理します。

一行まとめ: liveness は「生きているか」なので失敗時にコンテナ再起動、readiness は「トラフィック準備ができたか」なので失敗時に Endpoints から除外、startup は起動が遅いアプリの保護者。liveness には自分のプロセスだけ、外部依存は readiness に — この分離が cascading failure を防ぐ。terminationGracePeriodSeconds と preStop フックが graceful shutdown の時間の窓を作り、Docker の HEALTHCHECK は K8s が無視する。

次の章 #

ここまで私たちが扱ったのは1つの Pod のリソースモデル (第11章) とその Pod の健康判定 (本章) でした。replicas: 3 のような数字は人がマニフェストに直接書きました。しかし運用クラスタのトラフィックは時間帯と曜日によって大きく揺れ、そのたびに人が replicas を手で調整する形は持続可能ではありません。

第13章 オートスケーリング では、その空白を埋める3つのオブジェクトを一連の流れで整理します。HPA (Horizontal Pod Autoscaler) は CPU · メモリ · カスタムメトリクスに従って replicas を自動で増やし減らすコントローラです。VPA (Vertical Pod Autoscaler) は1つの Pod の requests / limits 自体を自動で調整する別の軸のモデルです。Cluster Autoscaler は Pod がスケジュールされるノードが足りないときノード自体を自動で追加する一段階さらに上のオブジェクトです。そして HPA の入力メトリクスは結局 readiness が true の Pod たちからのみ集まるという点で、本章でつかんだ readiness が次の章の出発点に再び登場します。

X