K8s 中級 #5 Health check — liveness / readiness / startup probe

読了 20分

K8s 中級シリーズの 5 番目の記事です。#4 まで私たちが押さえたのは Pod に どれくらいのリソースを与えるか のモデルでした。CPU・メモリの requestslimits でスケジューラと cgroup がその Pod をどんな条件に置くかが決まります。しかしリソースが十分だからといってそのコンテナが 本当に仕事をしているか は別の話です。プロセスは立っているのに中でデッドロックがかかっていることがあり、コンテナがちょうど起動したが DB コネクションプールがまだ満たされておらずトラフィックを受けてはいけない状況もあります。この記事では K8s がこの 2 つの質問 — 「生きているか」「トラフィックを受ける準備ができているか」 — をどう判断するか、そしてその判断の根拠となる 3 種類の probe を 1 サイクルでまとめます。

このシリーズは K8s 中級 7 編です。

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

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

K8s はこの 2 つの答えを別のオブジェクトに分離しました — livenessreadiness です。そして起動が遅いアプリのための保護者を 1 層追加しました — startup です。3 つの probe の役割を 1 つの表にまとめると次のとおりです。

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

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

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

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

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

3 つの probe すべてが同じ 3 つの検査方式の中から 1 つを選べます。それぞれ適したシナリオとコストが違います。

方式動作適したワークロードコスト
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 より execpg_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 を優先検討して、その道が塞がったときに tcpSocketexec を選ぶ順序が運用の標準です。

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

3 つの probe は同じ時間パラメータを共有します。1 つの表にまとめます。

フィールド意味デフォルト値
initialDelaySecondsコンテナが起動した後、最初の検査までの待ち時間0
periodSeconds検査周期10
timeoutSeconds1 回の検査が応答を待つ時間の上限1
failureThreshold連続失敗何回で最終失敗とみなすか3
successThreshold連続成功何回で最終成功とみなすか (liveness/startup は 1 で固定)1

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

デフォルト値が運用でそのまま使うには 過剰に攻撃的 な場合がよくあります。特に timeoutSeconds: 1 は GC が少し長くなったりノードの負荷が一瞬上がったりした瞬間にも失敗に落ちます。liveness にそのデフォルト値がそのまま入っていると、一時的な応答遅延がコンテナ再起動につながる事故に直結します。運用マニフェストではほぼ常に 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)。
  • 1 回の呼び出しが 3 秒以内に応答しなければその回を失敗として処理します (timeoutSeconds)。
  • 連続 3 回失敗したら (failureThreshold) liveness 失敗とみなしてコンテナを再起動します。

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

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 が一組で刻まれる形が標準です。この痕跡がよく見られる Pod は liveness probe を疑うべきです — 本当にコンテナがよく死んでいるのか、それとも probe が攻撃的すぎて健全なコンテナを殺しているのかを判別する必要があります。

liveness に何を入れるべきか #

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

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

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

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

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

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 ping + キャッシュ接続 + 依存する外部サービスの状態

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 がない #

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

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

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

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

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

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

事故 3 — DB ping を liveness に入れた #

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

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

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

probe と graceful shutdown #

probe のモデルの上にもう 1 層乗るテーマが 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

terminationGracePeriodSecondspreStop の時間まで含みます。つまり上の例で 60 秒のうち 10 秒が preStop に、残り 50 秒が SIGTERM 後の終了に使われます。preStop を 20 秒にすると SIGTERM 以降の時間が 40 秒に減るので、2 つの値を一緒に調整する必要があります。

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

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

この方式は PreStop フックなしできれいな graceful shutdown を作ります。ただしアプリコード次元の SIGTERM ハンドラが正確に動作する必要があるという前提があります — Docker 上級 #6 で見た PID 1 問題と init ツールがこの地点で再び意味を持ちます。コンテナの PID 1 が SIGTERM を無視すると readiness false 処理も起きません。

総合マニフェスト #

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"]

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

  • startup probe: 起動に最大 180 秒(10 秒 × 18 回)許容。Spring Boot の平均起動時間 + 余裕。
  • liveness probe: startup が成功した後から動作。/actuator/health/liveness は自分のプロセスの状態だけを見ます (DB ping なし)。
  • 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 上級 #6 で短く触れた Docker の HEALTHCHECK インストラクションと K8s probe の関係を一度整理しておきます。

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

このインストラクションは docker run でコンテナを直接立てるときに Docker デーモンがその検査を回すモデルです。docker psSTATUS 列に (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 が本当に動作する検査 だという点ははっきりさせなければなりません。

まとめ #

この記事で押さえた流れをまとめます。

  • 3 つの probe の役割分離 — liveness は「生きているか」 → 失敗時にコンテナ再起動、readiness は「トラフィックを受ける準備ができているか」 → 失敗時に Endpoints から除外、startup は起動段階の保護者 → 成功するまで liveness/readiness 無効。
  • 3 つの検査方式httpGet(もっとも一般的、200~399 なら成功)、tcpSocket(非 HTTP サーバ)、exec(もっとも柔軟だが fork コスト)。可能なら httpGet 優先。
  • 共通パラメータinitialDelaySecondsperiodSecondstimeoutSecondsfailureThresholdsuccessThreshold。デフォルト値(特に timeoutSeconds: 1)は運用で攻撃的すぎるので上げる必要あり。
  • liveness には自分のプロセスだけ、外部依存は readiness で — DB ping を liveness に入れると cascading failure で無限再起動ループに陥る。
  • startup probe — 起動が遅いアプリ(Spring Boot、Rails)のための保護者。failureThreshold * periodSeconds が起動許容時間。1.18 から安定化。
  • graceful shutdownterminationGracePeriodSeconds(デフォルト 30 秒)、preStop フックの sleep で Endpoints 更新時間を稼ぎ、SIGTERM 後にインフライトリクエストを処理。SIGTERM を受けると readiness を false に落とすアプリコードのパターンも同じ効果。
  • Docker HEALTHCHECK は K8s が無視 — K8s が見るのはマニフェストの probe だけ。2 つのモデルは別の層で動作。

このモデルまで手に入れば、Pod のマニフェストに刻まれた 3 つの probe ブロックと terminationGracePeriodSecondspreStop がどんな運用シナリオを防いでいるかを 1 行で読めます。

次 — オートスケーリング (HPA / VPA / Cluster Autoscaler) #

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

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

X