ヘルスチェックの設計 — livenessとreadinessをどう判定するか

ヘルスチェックの設計で最も多い失敗は、「プロセスが起動していること」と「リクエストを正しく処理できること」を同一視することです。この混同が、サービスは動いているのにトラフィックが途絶える事態や、誤った再起動ループで本番環境を不安定にする障害を引き起こします。liveness(生存確認)とreadiness(受付可否確認)を明確に分けて設計し、「健全とは何か」を事前に言語化することが、安定した本番運用の起点になります。

「生きているのに振り分けられる」不健全判定の構造

プロセス生存とサービス提供能力は別問題

アプリケーションプロセスが起動していることと、受け取ったリクエストを正しく処理できることは、まったく別の状態です。20台以上のアプリサーバーを束ねたクラスタ環境では、プロセスは起動しているがコネクションプールが枯渇しているインスタンスや、メモリリークが進行して応答が極端に遅くなっているインスタンスが数台混じっていても、TCPポートへの接続チェックだけではすべてHealthyと判定されます。ロードバランサーは正常なインスタンスと等しくトラフィックを振り分けるため、不健全なインスタンスへのリクエストが積み重なってエラーレートが上昇するまで、問題が表面化しません。

チェック設計の甘さが生む連鎖障害

デプロイ直後の深夜に、ロードバランサーが全インスタンスをHealthyと判定し続けたにもかかわらずエラーレートが急上昇した、という話を現場でよく聞きます。調査すると、新バージョンのアプリがDB接続の初期化に失敗したまま起動しており、ヘルスチェックのエンドポイントは固定文字列を返すだけの実装だったことが多いです。チェックがプロセスの存在しか確認していないため、機能不全のインスタンスへトラフィックが流れ続ける構造になっています。設定変更やロールバックで一時的に収束しても、同じ設計を持ち続ける限り、次のデプロイで再発します。

アンチパターン——静的応答と起動直後のチェック

現場でよく見られるアンチパターンは2種類あります。ひとつは、チェックエンドポイントが固定文字列を返すだけの静的実装です。開発初期には便利に機能しますが、3〜5環境を抱えるプロジェクトでは本番と開発でチェック内容が乖離し、本番に近い環境でのチェック設計を後から設計し直すことになります。もうひとつは、起動してから初期化が完了する前にlivenessチェックが走り、UnhealthyとみなされてすぐPodが再起動するループです。どちらも「動いているはずなのに切り離される」という不健全判定の典型であり、実装前に潰しておくべき落とし穴です。

何を見て「健全」と判定するかの設計判断

エンドポイントの応答だけで判定する限界

HTTPの200応答が返ってきても、確認できるのは「エンドポイントが何らかの応答を返せた」という事実だけです。DBクエリが規定時間内に返っているか、コネクションプールに空きがあるか、バックグラウンドワーカーが滞留していないかは、固定文字列を返すだけのエンドポイントでは一切判定できません。「200が返るから健全」という判断は、設計の放棄に等しいと言えます。トラフィックの少ない時間帯では問題が表面化しにくく、ピーク時に突然エラーレートが跳ね上がるパターンに陥りやすいため、平常時のチェックで安心することが最も危険です。

「健全の定義」を先に言語化する

実装に入る前に、「このサービスが健全である状態とは何か」を測定可能な形で定義します。たとえば「DB読み取りが500ms以内に完了し、コネクションプールの空き率が10%以上あること」のように、条件を数値で明文化します。この定義がないまま実装を進めると、担当者が変わるたびにチェック内容がずれ、半年後には誰も理由を説明できないエンドポイントが残ります。設計ドキュメントにこの定義を記載することで、後から加わったメンバーが判断の根拠を追えるようになり、改修のたびに一から議論し直す手間を省けます。

ステータスコードと応答内容の使い分け

ロードバランサーはHTTPステータスコードで振り分けを判断します。より詳細な内部状態(DBの応答時間・外部APIの到達性など)を知りたい場合は、レスポンスボディに構造化データを含め、監視ツールがその内容を可視化する設計にします。ただしロードバランサーがボディを解析できるかは製品によって異なるため、「ステータスコードで振り分け、ボディで詳細を監視」という役割分担を明確にすることが現実的です。チェックの目的が「切り離し」なのか「可視化」なのかを最初に整理することが、設計の出発点になります。

liveness(生存)とreadiness(受付可否)を分ける理由

livenessは「殺す基準」、readinessは「外す基準」

livenessとreadinessは、Unhealthyになったときのオーケストレーターの反応がまったく異なります。livenessがUnhealthyになるとコンテナが強制終了・再作成されます。readinessがUnhealthyになるとトラフィックから切り離されるだけで、再起動はされません。この違いを理解せずに同じエンドポイントで両方を兼ねると、DB接続が一時的に切れただけで再起動ループが始まる設計上の事故が起きます。「殺してよい状態」と「外してよい状態」は異なる条件であり、それぞれに個別のしきい値と判定ロジックが必要です。

分けないと起きる再起動ループ

数百台規模のKubernetesクラスタで、livenessプローブにDBへの疎通確認を含めていたところ、DBのメンテナンス作業中に全Podが同時に再起動し始めたという事例があります。Pod再起動中はDBへの接続試行が集中してDBの応答が遅延し、その遅延がlivenessをさらにUnhealthyにするという連鎖です。livenessとreadinessが分離されていれば、readinessをUnhealthyにしてトラフィックを切り離すだけで済んだはずです。一度再起動ループに入ると手動介入なしでは復旧できないケースもあり、深夜帯に発生した場合の影響は大きくなります。

startup probeを加えた三層設計

JVMの起動や大規模なキャッシュの初期ロードなど、起動時間が長いアプリでは、liveness/readinessの二層だけでは起動中の誤判定を防ぎきれないことがあります。startup probeを別途設定し、「起動が完了するまでliveness・readinessのチェックを保留する」三層構造にすることで、誤った再起動を防げます。初期化に30秒以上かかるアプリでは、startup probeのfailureThresholdとperiodSecondsの積を初期化完了時間の1.5倍程度に設定することが判断の目安になります。この三層設計は、起動の遅いアプリを安定してデプロイするための基本構成です。

比較表: TCP/HTTP/依存先込みチェックの向き不向き

TCPチェックの守備範囲と限界

TCPチェックはポートへの接続確立だけを確認します。実装コストが最も低く、アプリの内部状態に依存しないため壊れにくい特性があります。ただし確認できるのは「プロセスがリッスンしているか」だけであり、アプリが正常にリクエストを処理できるかは判定できません。非HTTPプロトコルのサービスや起動確認の最低保証として使う用途には適していますが、HTTPアプリのreadiness確認をTCPチェックだけで済ませるのは守備範囲が狭すぎます。チェック方式の選定は、「何を確認したいか」を先に決めてから行うことが重要です。

HTTPチェックで何を確認するか

HTTPチェックはステータスコードの確認に加え、エンドポイント内でDB接続の確認を行い、応答時間がしきい値を超えたら503を返す実装が標準的な設計です。チェックエンドポイント自体が重い処理にならないよう、専用の軽量エンドポイントを設けることが重要です。通常のAPIエンドポイントをそのままチェックに転用すると、チェック自体がサービスの負荷になります。また、チェック用エンドポイントは認証を要求しない設計にすることが多いため、応答に含める情報が意図しない情報漏洩にならないか、設計時に確認します。

依存先込みチェックの設計上の注意点

依存先(DB・キャッシュ・外部API)の到達性をヘルスチェックに含める設計は、強い判定力を持つ反面、依存先の一時的な遅延や瞬断が自サービスの不健全判定に直結します。依存先込みのチェックはreadinessには使えますが、livenessには原則として含めるべきではありません。複数の依存先を確認する場合は、それぞれのタイムアウトを個別に設定し、一つの依存先の応答待ちがチェック全体をブロックしない設計にします。チェックに含める依存先は、後述する強依存と弱依存の分類に基づいて選択します。

チェック方式 確認できること 確認できないこと 主な用途 livenessへの適合 readinessへの適合
TCP接続 ポートのリッスン状態 アプリの処理能力・内部状態 非HTTPプロトコル、起動の最低保証 △(プロセス存在のみ) ✕(処理能力を確認できない)
HTTP(固定応答) エンドポイントの到達性 実処理の健全性・依存先の状態 最も簡易な生存確認 ○(プロセス+HTTP層の確認) △(readinessとしては不十分)
HTTP(DB疎通込み) DBへの到達性・基本クエリ応答 複雑なビジネスロジックの正常性 readinessの主力 ✕(DB障害で再起動ループになる) ○(強依存先の確認として適切)
HTTP(依存先全込み) 主要依存先の総合状態 依存先側の問題との切り分け 強依存先をすべて含めたい場合 ✕(連鎖障害のリスクが高い) △(依存先の分類が前提)
gRPC Health Check gRPCサービスの応答状態 HTTP以外の副作用・内部状態 gRPCサービスのreadiness確認 △(プロトコル層の確認のみ) ○(gRPCサービスに限定)

依存先の障害をどこまで自分の不健全とみなすかの境界

強依存と弱依存を分類する

設計の起点は、依存先を「これがなければリクエストを処理できない強依存」と「なくても機能の一部が低下するだけの弱依存」に分類することです。ECサービスであれば商品在庫DBは強依存ですが、レコメンドエンジンは弱依存に分類できます。強依存先の障害はreadinessのUnhealthyに含める候補になりますが、弱依存先の障害を含めると、サービス全体の可用性が弱依存先の安定性に引きずられます。この分類を設計段階で行い、設計ドキュメントに残すことが、後続の判断の根拠になります。

DBが落ちたとき自分もUnhealthyになるべきか

DBへの依存が強い場合、DB障害時にはインスタンスをUnhealthyとしてロードバランサーから外すのが基本方針です。ただしDBのタイムアウト設定が長い場合、チェック自体がタイムアウトするまで応答を待ち続け、その待機時間がアプリの負荷になることがあります。DBチェックのタイムアウトはヘルスチェック全体のタイムアウトより短く設定する必要があります。readinessタイムアウトが5秒であれば、DBチェックのタイムアウトは2〜3秒以内に収めることが一般的な目安です。タイムアウトの入れ子関係は設計段階で整理しておかないと、後からの変更が難しくなります。

依存先障害を自分の不健全に伝播させない設計

外部APIやSaaSへの依存がある場合、その障害を自サービスのUnhealthyに直接伝播させない設計が主流です。外部APIが一時的に503を返していても、自サービスのインスタンスはトラフィックを受け付けつつ、フォールバック応答や機能縮退モードで動作を継続します。この方針のもとでは、ヘルスチェックで外部APIの到達性を直接確認するのではなく、アプリの内部状態(リクエストキューの滞留、連続エラーの閾値超過など)を判定基準とします。依存先の障害を「自分がUnhealthyになる根拠」とするかどうかは、サービスの可用性設計方針と整合させる必要があります。

チェックが厳しすぎて自ら落とす境界の見極め

タイムアウトとしきい値の設定判断

チェックのタイムアウトを短く設定しすぎると、一時的な高負荷時に全インスタンスが同時にUnhealthyになるリスクがあります。しきい値の設計は、failureThreshold(何回連続でUnhealthyになったら外すか)とperiodSeconds(チェック間隔)の積として考えます。「2回連続・10秒間隔」なら20秒のバッファが生まれ、「1回・5秒間隔」なら5秒の応答遅延で切り離されます。設定値は本番トラフィックのピーク時のレイテンシ分布を参照して決めるべきであり、平常時の平均値を基準にするとピーク時に誤判定が多発します。

カスケード障害のトリガーになるリスク

チェックが厳しすぎると、一部インスタンスがUnhealthyとして外れ、残りに負荷が集中し、それがさらにUnhealthyを連鎖させるカスケード障害を自ら引き起こすことがあります。たとえば10台のうち2台が外れると、残り8台で同量のトラフィックを処理することになります。8台が10台分の負荷(125%相当)を処理できるキャパシティを持っていなければ、連鎖的に落ちていきます。最小稼働インスタンス数と1インスタンスが処理できる最大スループットを事前に算出した上でしきい値を逆算して設計することが、この種の事故を防ぎます。

チェック頻度と負荷のトレードオフ

チェック間隔を短くすれば障害検知が早まりますが、チェック自体がアプリへのリクエストとして負荷になります。DB疎通を確認するチェックを10秒間隔で実行し、インスタンスが100台あると、1分間に600回のDBクエリがチェック用途だけで発生します。チェック頻度・インスタンス数・チェック内容の組み合わせが予期しない負荷源になることを意識した設計が必要です。軽量なチェックは10〜15秒間隔、DB疎通込みのチェックは30秒程度を基準に検討するのが一般的ですが、間隔を延ばすほど障害検知が遅れるため、SLAとのトレードオフで判断します。

ヘルスチェック設計チェックリスト

以下のチェックリストを設計フェーズと実装後レビューで活用します。各項目が「Yes」でない場合は、設計上の判断が未確定の状態です。手を動かすのはその判断を固めてからです。

  1. 「このサービスが健全である状態」を測定可能な条件として文章化しているか(「200が返る」は条件ではない)
  2. livenessとreadinessを別エンドポイントに分離しているか
  3. livenessには、再起動が正当化されるほどの深刻な条件のみを含めているか(依存先への疎通確認を含めていないか)
  4. startup probeが必要なアプリ(初期化に30秒以上かかるなど)で、設定が入っているか
  5. 依存先を強依存と弱依存に分類し、ヘルスチェックへの反映方針を決めているか
  6. 依存先チェックのタイムアウトが、ヘルスチェック全体のタイムアウト内に収まっているか
  7. チェックエンドポイントが軽量か(通常APIの転用・重い処理の混入がないか)
  8. failureThresholdとperiodSecondsをピーク時のレイテンシ分布から逆算しているか(平常時の平均値を基準にしていないか)
  9. 最小稼働インスタンス数でのキャパシティを試算し、カスケード障害のリスクを確認しているか
  10. チェック頻度×インスタンス数×チェック内容で生じる追加負荷を試算しているか
  11. 固定文字列を返すだけのエンドポイントをreadinessに使っていないか
  12. ロードバランサーとオーケストレーターそれぞれのチェック方式・制約(ボディ解析の可否・プロトコルの対応など)を確認しているか

▶ 手を動かして本番まで到達する

その設計判断を、RHEL10の本番環境で手を動かして固めませんか

ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。

最大6名の少人数で、構築実務者の到達点まで引き上げます。座学で終わらせず、本番相当の環境で判断を検証する2日間です。

ヘルスチェックの設計を本番環境で検証する少人数講座へ →

Linuxセミナー上級編(RHEL10)|最大6名の少人数ハンズオン