ヘルスチェックパターン
サービスの稼働状態を定期的に確認し、異常を検知したらトラフィックを切り離す仕組み
ヘルスチェックパターンとは
ヘルスチェックパターンは、ロードバランサーやオーケストレーターがサービスの稼働状態を定期的に確認し、異常なインスタンスをトラフィックから自動的に切り離す仕組みである。手動での障害検知・対応を排除し、システムの可用性を向上させる。
シャローとディープの使い分け
どこまで見るかで性質が正反対になる。浅いチェックは「落ちていないこと」しか言えない代わりに巻き込み事故を起こさない。深いチェックは実質的な提供可否まで見える代わりに、依存先の障害を自分の障害として申告してしまう。
| 種類 | チェック内容 | 用途 | リスク |
|---|---|---|---|
| シャロー (浅い) | プロセスが生きているか | ALB のターゲットヘルスチェック | 依存先の障害を検知できない |
| ディープ (深い) | DB、キャッシュ、外部 API への疎通 | 監視・アラート | カスケード障害のリスク |
// シャローヘルスチェック: プロセスが生きていれば 200
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
// ディープヘルスチェック: 全依存先の状態を確認
app.get('/health/deep', async (req, res) => {
const checks = {
db: await run(() => checkWithTimeout(() => db.query('SELECT 1'), 3000)),
cache: await run(() => checkWithTimeout(() => redis.ping(), 1000)),
s3: await run(() => checkWithTimeout(() => s3.headBucket({ Bucket: 'my-bucket' }), 2000)),
};
const allHealthy = Object.values(checks).every(c => c.ok);
res.status(allHealthy ? 200 : 503).json(checks);
});
// 失敗を戻り値に畳む (例外でレスポンスを落とさない)
async function run(fn: () => Promise<unknown>) {
try {
await fn();
return { ok: true };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
}
// ms を超えたら例外にする (依存先の遅延を自分の遅延にしない)
async function checkWithTimeout(fn: () => Promise<unknown>, ms: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
fn(),
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
}),
]);
} finally {
clearTimeout(timer);
}
}
clearTimeout を忘れないこと。Promise.race は決着した時点で結果を返すが、負けた側のタイマーは消えない。数秒おきに呼ばれるヘルスチェックでタイマーを積み残すと、待機中のタイマーが常に数十本たまった状態になる。拒否する値を文字列でなく Error にしているのも意図があり、スタックが残らないと「どのチェックが遅いのか」を後から追えない。
AWS でのヘルスチェック
ALB ターゲットグループ
ALB はターゲットグループに設定したヘルスチェックパスに定期的にリクエストを送り、異常なターゲットを自動的に切り離す。チェック間隔、連続失敗回数 (即切り離しを防ぐ)、復帰に必要な連続成功回数を設定する。
2026 年 8 月時点の既定値は、ターゲット種別が instance / ip の場合でチェック間隔 30 秒、タイムアウト 5 秒、異常と見なす連続失敗が 2 回、復帰に必要な連続成功が 5 回である。ここで気付きたいのは切り離しと復帰が非対称だということだ。異常側は 2 回で切るので検知は 1 分前後で済むが、復帰側は 5 回必要なので 2 分半ほど戻ってこない。ローリング更新やスケールアウトの所要時間を見積もるときは、この復帰側の遅さが効いてくる。急ぎたければ間隔を詰めるが、その分だけ一時的な失敗で切られやすくなる。
成功と見なす HTTP ステータス (Matcher) も設定項目で、HTTP/1.1・HTTP/2 なら 200 から 499 の範囲で指定できる。つまり ALB が見ているのはステータスコードだけで、応答本文の中身は判定に使われない。
Route 53 ヘルスチェック
Route 53 のヘルスチェックは、エンドポイントの障害を検知して DNS フェイルオーバーを実行する。プライマリリージョンが障害の場合、セカンダリリージョンに自動切り替えする。
判定は 1 か所からの観測ではない。世界各地に配置されたチェッカーが同時にリクエストを送り、健全と報告したチェッカーが全体の 18 % を超えていれば健全、18 % 以下なら異常と判定する。この閾値のおかげで、特定地域からの経路障害や個々のチェッカーの失敗だけでは切り替わらない。逆に言えば「ある地域からだけ繋がらない」症状は Route 53 のフェイルオーバーでは救えないということでもある。
もう 1 つの前提は、これが DNS による切り替えだということだ。効くのはクライアントが名前解決をやり直した後なので、レコードの TTL とリゾルバーやブラウザーのキャッシュの分だけ遅れる。秒単位で切り替えたい要件には、接続を張り替える層 (ロードバランサーや AWS Global Accelerator) を併用する。
ECS タスクヘルスチェック
ECS はタスク定義にヘルスチェックコマンドを設定でき、異常と判定されたタスクを停止して新しいタスクに置き換える。コンテナをその場で作り直すのではなくタスク単位で入れ替わる、という粒度の違いを押さえておきたい。
判定に使われるのは essential を真にしたコンテナだけで、essential でないサイドカーが異常になってもタスクの状態は変わらない。逆に essential なコンテナが 1 つでも UNHEALTHY になればタスク全体が UNHEALTHY になる。設定できるのは間隔・タイムアウト・リトライ回数・開始猶予 (start period) で、起動に時間がかかるアプリでは猶予を置く。この猶予の間の失敗はリトライ回数に数えられない。
運用で助かる挙動として、ECS エージェントが切断されてもコンテナは UNHEALTHY にならない。最後に受け取った状態がそのまま保持されるので、エージェントの再起動でタスクが一斉に置き換わることはない。
カスケード障害の防止
ディープヘルスチェックの最大のリスクは、依存先の障害が自サービスの障害に波及するカスケード障害だ。
DB 一時障害 → ディープヘルスチェック失敗 → ALB がインスタンスを切り離し
→ 残りのインスタンスに負荷集中 → さらにヘルスチェック失敗 → 全インスタンス切り離し
対策:
- ALB にはシャローヘルスチェックを使う (プロセスの生存確認のみ)
- ディープヘルスチェックは監視・アラート用に別エンドポイントで提供
- ヘルスチェックにタイムアウトを設定し、依存先の遅延で自身が遅延しない
ヘルスチェックのレスポンス設計
本文の作りは、機械が読む部分と人が読む部分を兼ねさせるのが基本になる。
{
"status": "degraded",
"checks": {
"database": { "status": "healthy", "latency_ms": 12 },
"cache": { "status": "unhealthy", "error": "connection refused" },
"disk": { "status": "healthy", "free_gb": 45.2 }
}
}
healthy / degraded / unhealthy の 3 段階で表現する。一部の依存先が障害でも、コア機能が動作する場合は degraded として、トラフィックは受け続ける。
ただし、この status をロードバランサーは読まない。ALB の判定材料はステータスコードだけなので、degraded のときに 200 を返すか 503 を返すかが切り離しの唯一の分岐になる。Kubernetes の httpGet プローブも同じで、成功と見なすのは 200 以上 400 未満のステータスコードであり、応答本文は 10 KiB で読み捨てられる。本文の 3 段階は人間と監視システム向けの情報だと割り切り、切り離しの制御はステータスコード側に必ず持たせる。
Kubernetes のプローブとの対応
ALB が 1 種類のヘルスチェックに用途を兼ねさせるのに対し、Kubernetes は目的ごとにプローブを分ける。失敗したときの処置が違うので、割り当てを間違えると事故の形も変わる。
| プローブ | 確認対象 | 失敗したときの処置 |
|---|---|---|
| Startup | 起動処理が終わったか | 規定回数失敗するとコンテナを停止する。成功するまで他の 2 つは動かない |
| Liveness | プロセスが生きているか | コンテナを再起動する (既定は 3 回連続失敗で発動) |
| Readiness | リクエストを受け付けられるか | Pod の IP を Service の宛先から外す。コンテナは動いたまま |
シャローとディープの区別に当てはめると、Liveness にはシャロー、Readiness には必須の依存先だけを見る中間的なチェックを割り当てるのが扱いやすい。Liveness にディープなチェックを入れると、依存先の一時障害でコンテナが再起動を繰り返し、復旧をかえって遅らせる。起動が遅いアプリで Liveness の猶予を稼ぐために失敗回数を大きくするのも定石を外している。そこは Startup プローブの仕事で、起動完了までは Liveness の判定そのものを止められる。
ヘルスチェックパターンの背景や設計思想は関連書籍に詳しい。
この記事は役に立ちましたか?
関連用語
ロードバランシング
複数のサーバーにトラフィックを分散し、可用性とスケーラビリティを向上させる仕組み
フェイルオーバー
障害発生時にスタンバイシステムへ自動的に切り替え、サービスの継続性を確保する仕組み
オートスケーリング
トラフィックや負荷に応じてコンピュートリソースを自動的に増減させる仕組み
Liveness/Readiness プローブ
Kubernetes がコンテナの生存状態と受信準備状態を判定するためのヘルスチェック機構
Route 53
AWS のマネージド DNS サービスで、ドメイン登録、DNS ルーティング、ヘルスチェックを提供する
ガード節
関数の先頭で異常条件を早期にチェックし、ネストを浅く保つプログラミングパターン