「死んでるのに生きている」を防ぐ――L7ロードバランサー時代のヘルスチェック完全攻略
ネットワークエンジニアとして数多のサービスダウンを鎮火してきた経験から言わせてもらうと、「ヘルスチェックを甘く見ているエンジニアは、いつか必ず痛い目を見る」。
ロードバランサー(LB)が送る単なる GET / の200 OK。これだけで「よし、システムは健全だ」と安心していないだろうか? データベースが悲鳴を上げ、バックエンドのマイクロサービスがタイムアウトを連発していても、アプリケーションサーバーのルートパスが200を返し続ければ、LBはそのノードにトラフィックを流し続ける。結果、ユーザーには真っ白な画面かタイムアウトエラーが表示されるわけだ。
今日は、そんな「形だけのヘルスチェック」を卒業し、L7レベルで真のサービス可用性を担保するための設計論を叩き込む。
—
ヘルスチェックは「単なるPing」ではない
HTTPヘルスチェックのエンドポイント設計において、最も重要な原則は「依存関係の階層を意識すること」だ。単にプロセスが生きているかを見る Liveness と、依存サービスを含めてサービス提供が可能かを見る Readiness を明確に分ける必要がある。
1. Liveness Probe(生存確認)
プロセス自体が死んでいないかをチェックする。基本的には超軽量で、依存関係のチェックは一切行わない。
- URL:
/health/live - 役割: コンテナの再起動や、プロセス再起動のトリガー。
- 実装: 単に
200 OKを返すだけ。
2. Readiness Probe(準備完了確認)
そのノードがトラフィックを捌ける状態かをチェックする。ここで初めて、データベースへの接続確認や、外部APIの疎通確認を行う。
- URL:
/health/ready - 役割: LBからルーティング対象外にするかどうかの判断。
- 実装: DBの
SELECT 1や、依存するキャッシュサーバーへの疎通を確認し、失敗すれば503 Service Unavailableを返す。
—
現場で刺さる「ヘルスチェック」設計の勘所
設計時に陥りやすいワナが、「ヘルスチェックに重い処理を詰め込みすぎる」ことだ。ヘルスチェックの頻度が秒単位であれば、毎秒DBにクエリが飛ぶことになる。これは立派なDDoS攻撃だ。
推奨される実装例(Python / FastAPI)
from fastapi import FastAPI, Response, status
import databases
app = FastAPI()
db = databases.Database("postgresql://user:pass@localhost/dbname")
@app.get("/health/live")
async def live():
# プロセスが生きていればOK
return {"status": "ok"}
@app.get("/health/ready")
async def ready(response: Response):
try:
# DBへの接続確認(軽量なクエリを実行)
await db.execute("SELECT 1")
return {"status": "ready"}
except Exception:
# DBが死んでいれば503を返し、LBから切り離す
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return {"status": "db_connection_failed"}
このように、エンドポイントを分けることで、LBは「サービス自体は動いているが、今はDB待ちで処理できない」というシグナルを正しく受け取ることができるようになる。
—
インフラ運用者が直面する「深淵」と設定のコツ
LB(AWS ALBやNginx、HAProxyなど)側でヘルスチェックを設定する際、以下のパラメータを「なぜその値にするか」説明できるだろうか?
- Interval: チェックの間隔(例: 5s)。短すぎれば負荷に、長すぎれば切り離しが遅れる。
- Timeout: 応答待ち時間(例: 2s)。これが
Intervalより短いことは絶対条件だ。 - HealthyThreshold / UnhealthyThreshold: 連続成功/失敗回数。ネットワークの瞬断でサービスを落とさないためのバッファ。
Nginxの設定例(アップストリーム監視)
upstream backend_servers {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
# 5回失敗したらダウン、その後10秒待機
# 依存関係チェックのエンドポイントを指定する
keepalive 32;
}
server {
location / {
proxy_pass http://backend_servers;
}
# ヘルスチェック用エンドポイントの保護(任意だが推奨)
location /health/ready {
proxy_pass http://backend_servers/health/ready;
# タイムアウトを厳しめに設定
proxy_connect_timeout 1s;
proxy_read_timeout 1s;
}
}
—
最後に:トラブルシューティングの作法
「LBからノードが外れた」というアラートが飛んだ時、真っ先にやるべきことは何だろうか? ログファイルを探す前に、まずは curl で直接叩くことだ。
# ヘルスチェックパスを直接叩いて、レスポンスコードと時間を計測する
curl -Iv -o /dev/null -w "%{http_code} %{time_total}\n" http://<ノードのIP>:8080/health/ready
ここで 503 が返ってきていれば、アプリ側の依存関係の問題だ。Connection Refused ならプロセスが落ちている。Timeout ならネットワークか、高負荷でキューが溢れている。
ヘルスチェックは、システムの「心拍計」だ。この心拍計が正確でなければ、どんなに高度なオーケストレーションも無意味になる。RFC 7231に謳われるHTTPのステータスコードの意味を今一度噛み締め、単なる 200 OK の呪縛から抜け出してほしい。
いいか、「正常ではない状態を、正常ではないと正しく伝える」。これこそが、信頼性の高いシステムを構築するエンジニアの矜持だ。
コメント