Kubernetes Liveness・Readiness・Startup プローブの違いと設計|再起動ループを防ぐ実践ガイド
- liveness は「死んでいるなら再起動」、readiness は「準備できていなければトラフィックを止める」、startup は「起動完了まで liveness を待たせる」。
- Express では
/health(軽量・依存なし)と/ready(DB等チェック)を分け、deployment.yaml で startupProbe → livenessProbe → readinessProbe の順に設計する。 - DB 接続プール枯渇は liveness ではなく readiness で検知し、PostgreSQL接続プール枯渇の対処法の設計原則に従う。
- プローブ失敗の調査は 構造化ログ(JSON)と Loki 設計 で requestId 単位に追跡する。
- 同じ厳しい条件を liveness に入れると再起動ループの最大原因になる。
なぜ Kubernetes プローブが必要か
Pod をデプロイしたあと、Kubernetes は「コンテナが動いている」ことと「リクエストを処理できる」ことを別々に判断する必要がある。
コンテナプロセスが生きていても、次のような状態は日常茶飯事だ。
- DBマイグレーション中でまだリクエストを受け付けられない
- 依存サービスの一時障害でエラー率が上がっている
- 接続プール枯渇で新規クエリがタイムアウトしている
- GCやスレッドプール枯渇でハング気味だがプロセスは存在する
- ローリングデプロイで新旧 Pod が混在している
プローブなしでは、kubelet は「プロセスが動いていれば正常」とみなし、Service は準備できていない Pod にもトラフィックを送り続ける。結果として 502/503、デプロイ中のエラー率上昇、障害 Pod への再起動が遅れる——といった問題が起きる。
Kubernetes 1.16 以降、3種類のプローブが標準化されている。2026年の本番クラスタ(EKS 1.31、GKE 1.30 前後)でもこの3分類が基本設計の軸である。
| プローブ | 主な質問 | 失敗時の動作 |
|---|---|---|
| livenessProbe | プロセスは生きているか? | コンテナ再起動 |
| readinessProbe | トラフィックを受け付けられるか? | Endpoints から除外 |
| startupProbe | 起動は完了したか? | liveness/readiness の開始を遅延 |
この記事では liveness readiness 違い を軸に、HTTP / TCP / exec 各プローブ種別、failureThreshold の設計、グレースフルシャットダウン、2026年のベストプラクティスまで、Express 実装と deployment.yaml 完全例つきで解説する。
3種類のプローブ — 役割の根本的な違い
| プローブ | 設計の要点 |
|---|---|
| livenessProbe | デッドロック・ハング検知。チェックは軽量に。失敗 = 再起動 |
| readinessProbe | 依存関係・負荷状態を確認。失敗 = トラフィック停止(再起動なし) |
| startupProbe | 起動猶予。成功まで他プローブ無効。遅い起動のアプリ向け |
| 同じURLを共用 | アンチパターン。liveness が DB 障害で失敗 → 再起動ループ |
| HTTP / exec / tcp | HTTP推奨(アプリ層)。exec は補助、tcp はリッスンのみ確認 |
livenessProbe — 生存確認
liveness は 「このコンテナはもう復旧不能か?」 を問う。失敗すると kubelet は SIGTERM → grace period → SIGKILL の順でコンテナを再起動する。
向いているケース:
- デッドロックで HTTP 応答が永久に返らない
- メインスレッドがハングし、プロセスだけ残っている
- アプリ内部の watchdog が異常を検知した
向いていないケース:
- DB接続プールの一時枯渇(readiness で扱うべき。対処ガイド参照)
- 外部 API のレイテンシ悪化(readiness またはサーキットブレーカー)
- 起動直後のウォームアップ中(startupProbe で扱うべき)
PostgreSQL メンテナンスや PgBouncer の再起動で /ready が 503 になるのは正常な劣化だ。これを liveness に載せると CrashLoopBackOff になる。枯渇の根本原因はプール設計側で直し、トラフィック制御は readiness に任せる。
readinessProbe — 準備完了確認
readiness は 「ロードバランサーに載せてよいか?」 を問う。失敗しても Pod は再起動されない。Service の Endpoints コントローラが該当 Pod の IP をリストから外し、Ingress / Service 経由のトラフィックが届かなくなる。
ローリングデプロイでは、新 Pod が readiness 成功するまで旧 Pod にトラフィックが流れ続ける。これにより ゼロダウンタイム が実現する。
readiness で確認すべき依存関係の例:
| 依存 | チェック方法 | 失敗時の意味 |
|---|---|---|
| PostgreSQL | SELECT 1 | 書き込み・読み取り不可 |
| Redis | PING | セッション・キャッシュ不可 |
| メッセージキュー | 接続確認 | 非同期処理不可 |
| 必須の外部 API | HEAD/GET(タイムアウト短め) | 機能の一部が使えない |
オプション依存(機能フラグで無効化できるもの)は readiness 失敗にしない設計も検討する。checks オブジェクトで ok / fail / skip を返し、必須のみで 503 を決める。
startupProbe — 起動猶予
Kubernetes 1.16 で追加。起動に時間がかかるアプリで、liveness の initialDelaySeconds だけでは足りない場合に使う。
startupProbe が設定されている間、liveness と readiness は無効。startup が成功した時点から通常の liveness/readiness が動き始める。
最大起動猶予の計算:
猶予秒数 = failureThreshold × periodSeconds
例: failureThreshold: 30, periodSeconds: 10 → 最大 300 秒(5 分)の起動猶予。
Spring Boot + Flyway マイグレーション、大規模なインメモリキャッシュのウォームアップ、GPU モデルのロードなど、起動完了の定義が「ポートが開いた」では足りないケースで startup が効く。
プローブの仕組み — kubelet が何をしているか
kubelet は各プローブについて、設定された間隔(periodSeconds)でチェックを実行する。
Pod 起動
│
├─ startupProbe あり?
│ ├─ YES → startup 成功まで liveness/readiness はスキップ
│ └─ NO → 即座に liveness/readiness 開始
│
├─ livenessProbe 失敗が failureThreshold 回連続
│ └─ コンテナ再起動(restartCount++)
│
└─ readinessProbe 失敗
└─ Endpoints から Pod IP を削除(再起動なし)
主要パラメータ一覧
| パラメータ | デフォルト | 意味 |
|---|---|---|
initialDelaySeconds | 0 | コンテナ起動後、最初のチェックまでの待機 |
periodSeconds | 10 | チェック間隔 |
timeoutSeconds | 1 | 1回のチェックのタイムアウト |
successThreshold | 1 | 成功とみなす連続成功回数(liveness/startup は常に1) |
failureThreshold | 3 | 失敗とみなす連続失敗回数 |
terminationGracePeriodSeconds | 30 | liveness 失敗後の SIGTERM から強制終了まで |
failureThreshold の設計 — 2026年の実務指針
failureThreshold は「何回連続で失敗したらアクションするか」を決める。単独では意味がなく、periodSeconds とセットで解釈する。
| プローブ | 推奨 failureThreshold | 意図 |
|---|---|---|
| liveness | 3〜5 | 一時的な GC 停止を再起動にしない。ただしハング検知が遅れる |
| readiness | 1〜3 | 1 なら即座にトラフィック停止。DB 瞬断でフラップするなら 2〜3 |
| startup | 起動時間 ÷ periodSeconds × 1.5 | 起動の p99 + バッファ |
再起動までの時間(startup なし) の目安:
initialDelaySeconds + (failureThreshold × periodSeconds)
例: initialDelaySeconds: 10, failureThreshold: 3, periodSeconds: 5 → 最初の失敗判定まで最短 10 秒、その後 15 秒で再起動。
readiness の successThreshold はデフォルト 1 だが、2 に上げると「2回連続成功してから Endpoints に載せる」ため、起動直後のフラップを減らせる。ローリングデプロイで新 Pod がすぐ ready → not ready を繰り返す場合に有効。
ステージングで kubectl run の debug Pod から while true; do curl -so /dev/null -w '%{http_code} %{time_total}\n' http://api:3000/ready; sleep 1; done を回し、p99 応答時間を取る。timeoutSeconds は p99 の 2 倍、failureThreshold × periodSeconds は許容するダウンタイム以下に収める。
HTTP / TCP / exec プローブの使い分け
| プローブ種別 | 長所・短所・向き不向き |
|---|---|
| httpGet | アプリ層の応答を確認。パス・ヘッダー・ステータスコードで細かく制御可能。Express/Spring/Go の標準選択 |
| grpc(1.24+) | gRPC ヘルスプロトコル対応サービス向け。HTTP/2 ネイティブ。マイクロサービス間で一貫したチェック |
| tcpSocket | ポート LISTEN のみ。実装が不要だがデッドロックを見逃す。レガシー・サイドカーなしの最小構成向け |
| exec | シェル・PID ファイル・カスタムスクリプト。オーバーヘッド大。HTTP が使えないバッチ・ワーカー向け |
httpGet — 成功・失敗の判定
- 成功条件: HTTP ステータス 200〜399(2xx/3xx)
- 失敗条件: 4xx/5xx、接続拒否、タイムアウト
- kubelet はコンテナ内から Pod IP(または
host指定時はそのアドレス)へリクエストする
認証が必要なアプリでは /health と /ready だけ認証をバイパスするか、httpHeaders で内部トークンを送る。
tcpSocket — 限定的な用途
ポートが開いていることしか分からない。次のようなケースでのみ検討する。
- ヘルスエンドポイントを実装できないレガシーバイナリ
- プローブ対象がプロキシ(nginx の
listenのみ確認)
本番 API サーバーでは httpGet を強く推奨する。
exec — バッチ・ワーカー向け
exec はコンテナ内でコマンドを実行する。fork + exec のコストがあり、高頻度(periodSeconds: 1)では node の負荷になる。
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "test -f /var/run/worker.pid && kill -0 $(cat /var/run/worker.pid)"
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
スクリプトの終了コード 0 が成功、非 0 が失敗。シェルの引用符ミスで常に成功するバグに注意。
Express で /health と /ready を実装する
Node.js + Express での定番パターンは、エンドポイントを2つに分けることだ。
GET /health— liveness 用。プロセスが応答できるかだけ見る。DB に触れない。GET /ready— readiness 用。DB・Redis・必須の外部依存を確認する。
完全な server.js 例
// server.js
const express = require('express');
const { Pool } = require('pg');
const app = express();
const port = process.env.PORT || 3000;
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
connectionTimeoutMillis: 2000,
idleTimeoutMillis: 30000,
});
// --- Liveness: プロセス生存のみ ---
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
});
});
// --- Readiness: 依存関係の確認 ---
app.get('/ready', async (req, res) => {
const checks = { database: 'unknown' };
try {
const client = await pool.connect();
try {
await client.query('SELECT 1');
checks.database = 'ok';
} finally {
client.release();
}
res.status(200).json({
status: 'ready',
checks,
timestamp: new Date().toISOString(),
});
} catch (err) {
checks.database = 'fail';
res.status(503).json({
status: 'not_ready',
checks,
error: err.message,
timestamp: new Date().toISOString(),
});
}
});
// --- 本番 API ---
app.get('/api/users', async (req, res) => {
const result = await pool.query('SELECT id, name FROM users LIMIT 10');
res.json(result.rows);
});
// --- グレースフルシャットダウン ---
let isShuttingDown = false;
app.use((req, res, next) => {
if (isShuttingDown) {
res.set('Connection', 'close');
return res.status(503).json({ error: 'Server is shutting down' });
}
next();
});
const server = app.listen(port, () => {
console.log(JSON.stringify({
level: 'info',
message: 'Server listening',
port,
timestamp: new Date().toISOString(),
}));
});
async function shutdown(signal) {
console.log(JSON.stringify({
level: 'info',
message: 'Graceful shutdown started',
signal,
timestamp: new Date().toISOString(),
}));
isShuttingDown = true;
server.close(async () => {
await pool.end();
console.log(JSON.stringify({
level: 'info',
message: 'HTTP server closed, pool drained',
timestamp: new Date().toISOString(),
}));
process.exit(0);
});
setTimeout(() => {
console.error(JSON.stringify({
level: 'error',
message: 'Forced shutdown after timeout',
timestamp: new Date().toISOString(),
}));
process.exit(1);
}, 25000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
設計上のポイント
/healthは常に 200 — メモリリークでプロセスが応答不能になったときだけ失敗させる。DB が落ちても liveness は成功のままにする。/readyは 503 を返す — 依存障害時に Endpoints から外れる。接続プール枯渇時もここで 503 にし、再起動ではなくトラフィック分散で耐える(プール枯渇の対処)。- グレースフルシャットダウン —
SIGTERM受信後、まずisShuttingDownで readiness 相当の 503 を返し、進行中リクエストを処理してからpool.end()で終了する。 - タイムアウト — DB クエリに
connectionTimeoutMillisを設定し、プローブのtimeoutSeconds以内に応答する。 - 構造化ログ — シャットダウンや ready 失敗を JSON で出すと、Loki で requestId 単位に追跡しやすい。
TypeScript + 構造化ヘルスチェック(拡張版)
// health.ts
import type { Request, Response } from 'express';
import { pool } from './db';
import { redis } from './redis';
import { logger } from './logger';
type CheckResult = 'ok' | 'fail' | 'skip';
interface HealthResponse {
status: 'ok' | 'not_ready';
checks: Record<string, CheckResult>;
timestamp: string;
}
export async function livenessHandler(_req: Request, res: Response): Promise<void> {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
}
export async function readinessHandler(_req: Request, res: Response): Promise<void> {
const checks: Record<string, CheckResult> = {};
let allOk = true;
try {
const client = await pool.connect();
try {
await client.query('SELECT 1');
checks.postgres = 'ok';
} finally {
client.release();
}
} catch (err) {
checks.postgres = 'fail';
allOk = false;
logger.warn({ err, event: 'readiness_check_failed', dependency: 'postgres' });
}
try {
const pong = await redis.ping();
checks.redis = pong === 'PONG' ? 'ok' : 'fail';
if (checks.redis === 'fail') allOk = false;
} catch {
checks.redis = 'fail';
allOk = false;
}
const body: HealthResponse = {
status: allOk ? 'ok' : 'not_ready',
checks,
timestamp: new Date().toISOString(),
};
res.status(allOk ? 200 : 503).json(body);
}
deployment.yaml 完全例
以下は、上記 Express アプリを想定した 本番向け Deployment の完全例だ。
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
labels:
app: api-server
version: v1.2.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
version: v1.2.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: registry.example.com/api-server:1.2.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
protocol: TCP
env:
- name: PORT
value: "3000"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: database-url
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
startupProbe:
httpGet:
path: /health
port: http
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 24
livenessProbe:
httpGet:
path: /health
port: http
scheme: HTTP
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
successThreshold: 1
readinessProbe:
httpGet:
path: /ready
port: http
scheme: HTTP
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
successThreshold: 1
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- "sleep 5"
---
apiVersion: v1
kind: Service
metadata:
name: api-server
namespace: production
spec:
type: ClusterIP
selector:
app: api-server
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
軽量 Node.js API 向け(startupProbe 省略版)
起動が 5〜10 秒以内の Express API では、startup を省略し liveness の initialDelaySeconds で足りることが多い。
# deployment-light.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server-light
spec:
replicas: 2
selector:
matchLabels:
app: api-server-light
template:
metadata:
labels:
app: api-server-light
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
image: registry.example.com/api-server:1.2.0
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 2
exec と tcpSocket の YAML 例
# probes-exec-tcp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-worker
spec:
replicas: 1
selector:
matchLabels:
app: legacy-worker
template:
metadata:
labels:
app: legacy-worker
spec:
containers:
- name: worker
image: registry.example.com/legacy-worker:2.0.0
ports:
- containerPort: 8080
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "pgrep -f 'java -jar app.jar' > /dev/null"
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 8080
periodSeconds: 10
timeoutSeconds: 1
failureThreshold: 2
典型的なミス設定と再起動ループ
再起動ループ(CrashLoopBackOff)は、liveness プローブの誤設計が最も多い原因だ。
ミス1: liveness に DB チェックを入れる
# アンチパターン
livenessProbe:
httpGet:
path: /ready
port: 3000
何が起きるか: PostgreSQL のメンテナンスや接続プール枯渇で /ready が 503 → liveness 失敗 → コンテナ再起動 → 起動直後また DB 未接続 → 再失敗 → 無限ループ。
修正:
livenessProbe:
httpGet:
path: /health
port: 3000
readinessProbe:
httpGet:
path: /ready
port: 3000
ミス2: initialDelaySeconds が短すぎる
アプリの起動に 20 秒かかるのに、periodSeconds: 5 × failureThreshold: 3 = 15 秒で liveness 失敗 → 起動完了前に再起動。
修正: startupProbe を追加する。
startupProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 5
failureThreshold: 12
ミス3: timeoutSeconds < アプリ応答時間
/ready の DB クエリに 2 秒かかるのに timeoutSeconds: 1 だと毎回タイムアウト。readiness ならトラフィック停止、liveness なら再起動ループ。
修正: p99 応答時間 + 余裕を timeoutSeconds に設定(通常 2〜5 秒)。
ミス4: 高負荷時に liveness が失敗する
/health 内で重い同期処理やイベントループのブロックがあると、高負荷時にプローブ応答が遅延する。
修正: /health は即座に 200 を返す。CPU limit の見直し。failureThreshold: 5 は一時策に留める。
ミス5: グレースフルシャットダウンなし
terminationGracePeriodSeconds: 30 なのに SIGTERM を無視すると、進行中リクエストが切断される。
修正: isShuttingDown で 503、server.close()、pool.end()、preStop の sleep 5。
再起動ループの切り分け手順
kubectl get pods で STATUS が CrashLoopBackOff か確認
kubectl describe pod <name> の Events 末尾で Liveness probe failed を探す
kubectl logs <name> --previous で再起動直前のログを確認
Loki で {pod="..."} | json | event="readiness_check_failed" を検索([構造化ログ設計](/blog/structured-logging-json-loki-設計/))
失敗しているパス(/health か /ready か)と HTTP ステータスを特定
liveness ならエンドポイントを軽量化 / startupProbe 追加 / initialDelaySeconds 延長
readiness なら timeout 延長 / DB・プール障害の切り分け
kubectl get pods -n production -l app=api-server
kubectl describe pod api-server-7d8f9c-abcde -n production
kubectl logs api-server-7d8f9c-abcde -n production --previous --tail=100
kubectl exec -it api-server-7d8f9c-abcde -n production -- \
wget -qO- http://localhost:3000/health
kubectl exec -it api-server-7d8f9c-abcde -n production -- \
wget -qO- http://localhost:3000/ready
グレースフルシャットダウンの完全フロー
ローリングデプロイや kubectl delete pod では、kubelet がコンテナに SIGTERM を送る。アプリがこれを正しく処理しないと、プローブ設計が良くてもユーザーには 502 が見える。
1. kubelet → SIGTERM
2. アプリ: isShuttingDown = true → /ready は 503(readiness 失敗)
3. Endpoints コントローラが Pod IP を削除(数秒)
4. preStop sleep(オプション)で伝播ラグを吸収
5. server.close() で新規接続拒否、進行中リクエスト完了待ち
6. pool.end() / キュー切断 / キャッシュフラッシュ
7. process.exit(0)
8. grace period 超過時 → SIGKILL
terminationGracePeriodSeconds は (最大リクエスト処理時間) + (preStop sleep) + (バッファ) 以上にする。API が 60 秒かかるエンドポイントがあるなら、grace period も 60 秒超が必要だ。
アプリが /ready で 503 を返せるなら、preStop は短い sleep で足りる。レガシーアプリで readiness を制御できない場合は、preStop で curl -X POST localhost:8080/shutdown のように明示的なドレイン API を呼ぶ。
HTTP プローブの詳細設定
livenessProbe:
httpGet:
path: /health
port: 3000
scheme: HTTP
httpHeaders:
- name: X-Probe-Token
valueFrom:
secretKeyRef:
name: probe-token
key: token
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
gRPC アプリ(Kubernetes 1.24+)
livenessProbe:
grpc:
port: 50051
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
service: "my.service.Health"
periodSeconds: 5
failureThreshold: 2
ローリングデプロイとプローブの相互作用
maxUnavailable: 0 と readiness を組み合わせると、次の流れになる。
1. 新 ReplicaSet の Pod が起動
2. startupProbe(あれば)成功
3. readinessProbe 成功 → Endpoints に追加
4. 旧 Pod へ SIGTERM → readiness 失敗 → Endpoints から削除
5. 旧 Pod の grace period 中に残リクエスト処理
6. 旧 Pod 終了
2026年のベストプラクティス
1. プローブは「ユーザーが体験する経路」に近づける
Service メッシュ(Istio/Linkerd)を使う場合、sidecar 経由のトラフィックと kubelet プローブの経路がずれることがある。Kubernetes 1.28+ では native sidecar(restartPolicy: Always の init コンテナ)が安定しており、プローブがアプリコンテナを直接叩く設計を維持しやすくなった。
2. 観測可能性 — プローブとログ・メトリクスの分離
| 項目 | Kubernetes プローブ | Prometheus / Loki |
|---|---|---|
| 目的 | 自動復旧・トラフィック制御 | 可観測性・アラート |
| 間隔 | 5〜30 秒 | 15〜60 秒が多い |
| アクション | 再起動・Endpoints 更新 | 通知・ダッシュボード |
| チェック深度 | 軽量(特に liveness) | 詳細メトリクス可 |
プローブ失敗の原因調査には 構造化ログ JSON + Loki で readiness_check_failed などのイベントを記録する。メトリクス例:
| メトリクス | アラート条件 |
|---|---|
kube_pod_container_status_restarts_total | 5分で 3回以上増加 |
| readiness 失敗率 | 全 Pod の 50% 超が not ready |
| プローブレイテンシ | p99 > timeoutSeconds の 80% |
3. セキュリティ — プローブエンドポイントの露出
/health と /ready は認証なしでも、機密情報を返さない。バージョン情報の露出は最小限に。NetworkPolicy でクラスタ内部からのみ到達可能にする構成も検討する。
4. リソース制限とプローブの相互作用
CPU throttling(limits.cpu ヒット)でイベントループが遅延し、liveness が失敗することがある。requests/limits の比率を見直し、/health は同期 I/O ゼロを維持する。
5. ステージングでのカオス演習
- DB を止めて
/readyが 503 になり Pod が Endpoints から外れることを確認 kubectl execでkill -STOPし、liveness が再起動をトリガーするか確認- デプロイ中に
kubectl get endpoints -wで切り替えを観察
6. Helm / Kustomize での標準化
チームごとに probe 設定がバラつくと障害時に混乱する。ベースチャートに startup / liveness / readiness のデフォルト値を定義し、アプリごとに path と failureThreshold だけ上書きするパターンが 2026 年の定石だ。
liveness / readiness / startup の比較表
| 観点 | liveness vs readiness vs startup |
|---|---|
| 目的 | 生存 / 受付可否 / 起動完了 |
| 失敗時 | 再起動 / Endpoints除外 / liveness開始を遅延 |
| チェック内容 | 軽量・依存なし / 依存込み / 起動完了の目安 |
| 典型エンドポイント | /health / /ready / /health または同じ |
| periodSeconds | 10〜30秒 / 5〜10秒 / 5〜10秒 |
| 必須度 | 推奨 / 本番必須 / 遅い起動時のみ |
EKS / GKE / minikube での差分
| 環境 | 注意点 |
|---|---|
| minikube | ローカルなので DB も Pod 内。readiness の依存先を docker-compose と揃える |
| EKS | ALB Ingress の healthcheck-path を /ready に合わせる |
| GKE | BackendConfig で healthCheck を設定 |
| Istio | 1.16+ ではプローブ rewrite が改善。native sidecar と併用を検討 |
よくある設計判断
liveness は省略できるか?
デッドロック検知手段がなくなる。supervisor が別にあれば省略もあり得るが、Kubernetes では liveness 設定が一般的。
readiness なしでデプロイすると?
新 Pod が起動完了前にトラフィックを受け、500 エラーが出る。readiness は本番必須と考えてよい。
同じ periodSeconds にすべきか?
必須ではない。readiness を 5 秒、liveness を 15 秒のように分けてよい。
チェックリスト — デプロイ前の最終確認
-
/healthは DB 等に依存せず、常に高速(< 100ms)で 200 を返す -
/readyは必須依存のみ確認し、503 で失敗を表現する - 接続プール枯渇時は readiness でトラフィックを切り、プール設計を見直す
- liveness と readiness で別パスを使っている
- 起動に 30 秒超かかるなら startupProbe を設定した
-
timeoutSeconds≥ プローブ先の p99 応答時間 -
failureThreshold × periodSecondsが再起動までの許容時間として妥当 - SIGTERM でグレースフルシャットダウンし、進行中リクエストを捌ける
- プローブ失敗を 構造化ログ で記録している
-
kubectl describe podで再起動ループがないことを確認した
関連記事
この記事は API・バックエンド テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。
| トピック | 記事 |
|---|---|
| APIキャッシュ設計ガイド | APIキャッシュ設計ガイド|ETag・Cache-Control・304・CDNキャッシュキーの実務 |
| Web APIのエラーハンドリング設計 | Web APIのエラーハンドリング設計|RFC 9457・error_code・400/422/409の使い分け |
| Web APIのべき等性(Idempotency)設計 | Web APIのべき等性(Idempotency)設計|二重決済を防ぐ実装ガイド |
| API設計におけるページネーションの使い分け:オフセット型とカーソル型の比較 | API設計におけるページネーションの使い分け:オフセット型とカーソル型の比較 |
| APIのレート制限(Rate Limiting)実装ガイド | APIのレート制限(Rate Limiting)実装ガイド|アルゴリズム選定と429エラーの適切なハンドリング |
| APIレート制限と429対応 | APIレート制限と429対応|X-RateLimit・Retry-Afterを使ったクライアント実装 |
| WebHook設計のベストプラクティス | WebHook設計のベストプラクティス|署名・再試行・べき等性で信頼性を担保する |
| ZodでAPI境界のランタイム検証を設計する | ZodでAPI境界のランタイム検証を設計する|z.infer・Expressミドルウェア・zod-to-openapi(2026) |
| OpenAPI 3.1仕様のCIバリデーション完全ガイド | OpenAPI 3.1仕様のCIバリデーション完全ガイド|Spectral Lint・Breaking Change検出・GitHub Actions |
| GraphQL N+1 問題の解決 | GraphQL N+1 問題の解決|DataLoader バッチング実装と Apollo Server 完全ガイド |
| Circuit Breaker + Retry パターン実装ガイド | Circuit Breaker + Retry パターン実装ガイド|Node.js opossum/cockatiel・指数バックオフ・Jitter |
| Sagaパターン完全ガイド | Sagaパターン完全ガイド|Orchestration vs Choreography と補償トランザクション(Node.js) |
まとめ
liveness readiness 違いは一言で言えば、「再起動するか、トラフィックを止めるか」の違いだ。Kubernetes プローブ設定で最も重要なのは、liveness を軽く、readiness を厳しくすること。
Express では /health と /ready を分離し、deployment.yaml では startupProbe(必要なら)→ livenessProbe → readinessProbe の順で組み立てる。DB や接続プールの問題で liveness が失敗する設定は再起動ループの温床なので、まずそこを疑う。
グレースフルシャットダウンでは SIGTERM 後に readiness を先に落とし、terminationGracePeriodSeconds 内に処理を完了させる。障害調査では 構造化ログと Loki でプローブ失敗とリクエストログを突合する。
デプロイ後は kubectl describe pod の Events と kubectl get endpoints -w で実際の挙動を確認し、閾値を実測値に合わせて調整する——これが 2026 年時点での Kubernetes プローブ設計 の実務的な完成形だ。