概要
livenessProbe
「Podを再起動すべきか」を判断。プロセス自体の生死のみ確認。依存サービスの状態は見ない(見るとcascading failureを起こす)。
readinessProbe
「トラフィックを流すか」を判断。依存サービス(DB・Cache)に接続できる場合のみ200を返す。失敗時は503でServiceから切り離し。
プロトコル一致
MySQLはTCP 3306(MySQL独自プロトコル)。RedisはRESP(TCP 6379)。requests.get() ではなく専用クライアントで確認する。
ベアexceptの排除
except: はKeyboardInterruptやSystemExitも補足してしまう。必ず具体的な例外クラス(pymysql.Error 等)を指定する。
問題
GKE上で動くPythonサービスのヘルスチェックエンドポイントを実装しています。以下の「悪いコード」を読み、問題点を指摘した上で改善してください。
制約・前提条件
- Python 3.12 / Flask 3.x
- GKE上で動作(
livenessProbe/readinessProbe設定済み) - 依存サービス: MySQL(TCP接続)、Redis(TCP接続)
requestsではなく、実際のプロトコルに合わせた接続確認を行うこと- K8sの
livenessProbe/readinessProbeの違いも踏まえてステータスコードの設計を説明すること
期待する回答形式: コード + 設計説明
悪いコード (Before)
このコードには 4つの設計上の問題 が隠れています。見つけてみてください。
import requests
from flask import Flask, jsonify
app = Flask(__name__)
DB_HOST = "10.0.0.1"
DB_PORT = 3306
CACHE_HOST = "10.0.0.2"
CACHE_PORT = 6379
@app.route("/health")
def health():
# DBチェック
try:
r = requests.get(f"http://{DB_HOST}:{DB_PORT}/ping", timeout=1)
db_ok = r.status_code == 200
except: # ← ベアexcept(問題1)
db_ok = False
# Cacheチェック
try:
r = requests.get(f"http://{CACHE_HOST}:{CACHE_PORT}/ping", timeout=1) # ← HTTP GETでRedis?(問題2)
cache_ok = r.status_code == 200
except:
cache_ok = False
if db_ok and cache_ok:
return jsonify({"status": "ok"}), 200
else:
return jsonify({"status": "error"}), 200 # ← 常に200を返す(問題3)
# ↑ livenessとreadinessの区別もない(問題4)
ヒント(段階的開示)
ヒント1 — 方向性
requests でMySQLやRedisにHTTP GETするのは根本的に間違い。それぞれのプロトコルで接続確認する方法を考えよう。また、ステータスコードが常に200なのはK8sにとって何が問題か?
ヒント2 — アプローチ
- MySQL接続確認:
pymysqlでTCP 3306に接続してみる - Redis接続確認:
redis-pyのping()を使う livenessProbeは「Podを再起動すべきか」を判断する。readinessProbeは「トラフィックを流すか」を判断する- 両者で
/health/liveと/health/readyを分けるのが実践的
ヒント3 — 目指す構造
# liveness: プロセス自体が死んでいないかを確認(依存サービスは見ない)
@app.route("/health/live")
def liveness():
return jsonify({"status": "ok"}), 200
# readiness: 依存サービスに接続できるかを確認(失敗時は503)
@app.route("/health/ready")
def readiness():
checks = {}
checks["db"] = _check_db()
checks["cache"] = _check_cache()
ok = all(checks.values())
return jsonify({"status": "ok" if ok else "degraded", "checks": checks}), 200 if ok else 503
問題点分析
| # | 問題点 | 分類 | 影響 | 改善方法 |
|---|---|---|---|---|
| 1 | requests.get() でMySQL/Redisに接続しようとしている |
プロトコル不一致 | MySQL/RedisはHTTPではないため必ず失敗する(ハンドシェイクエラー) | pymysql / redis-py の専用クライアントを使う |
| 2 | 常に200を返す(障害時も200) | ステータスコード設計 | K8sのProbeは200-399を成功とみなすため「障害中でもPodが正常」と判断される | readinessは失敗時に503を返す |
| 3 | ベアexcept(except:)の使用 |
例外設計 | KeyboardInterruptやSystemExitも捕捉してしまう | except pymysql.Error: / except redis.RedisError: と具体化 |
| 4 | liveness/readinessの区別がない | 設計ミス | 依存サービス障害でlivenessをFailさせると、Pod再起動ループで障害を悪化させる | /health/live と /health/ready に分離 |
Probe設計図 — liveness vs readiness 分離
模範解答
"""GKE向けヘルスチェックエンドポイント。
liveness と readiness を分離し、K8s Probe の仕様に準拠した
ステータスコードを返す。依存サービスは各プロトコルで確認する。
"""
import socket
from http import HTTPStatus
import pymysql
import redis
from flask import Flask, jsonify
app = Flask(__name__)
# 接続設定(実運用では環境変数から取得)
DB_HOST = "10.0.0.1"
DB_PORT = 3306
DB_USER = "health_check"
DB_PASSWORD = "secret"
CACHE_HOST = "10.0.0.2"
CACHE_PORT = 6379
CONNECT_TIMEOUT_SEC = 2
def _check_db() -> bool:
"""MySQLにTCP接続できるかを確認する。"""
try:
conn = pymysql.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
connect_timeout=CONNECT_TIMEOUT_SEC,
)
conn.close()
return True
except pymysql.Error:
return False
def _check_cache() -> bool:
"""RedisにPINGを送って応答を確認する。"""
try:
client = redis.Redis(
host=CACHE_HOST,
port=CACHE_PORT,
socket_connect_timeout=CONNECT_TIMEOUT_SEC,
)
return client.ping()
except redis.RedisError:
return False
@app.route("/health/live")
def liveness():
"""livenessProbe 用エンドポイント。
プロセス自体が応答できるかだけを確認する。
依存サービスの状態は見ない(依存サービス障害でPodを再起動しても無意味なため)。
"""
return jsonify({"status": "ok"}), HTTPStatus.OK
@app.route("/health/ready")
def readiness():
"""readinessProbe 用エンドポイント。
依存サービス(DB・Cache)に接続できる場合のみトラフィックを受け付ける。
接続不可の場合は 503 を返し、Podをロードバランサから切り離す。
"""
checks = {
"db": _check_db(),
"cache": _check_cache(),
}
all_healthy = all(checks.values())
status_code = HTTPStatus.OK if all_healthy else HTTPStatus.SERVICE_UNAVAILABLE
return (
jsonify(
{
"status": "ok" if all_healthy else "degraded",
"checks": checks,
}
),
status_code,
)
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
ポイント解説
1
ステータスコード設計
K8sのProbeは200-399を成功、それ以外を失敗とみなす。常に200を返すと「障害中でもPodが正常」と判断され、トラフィックが流れ続ける。readinessは失敗時に503を返すことでPodをServiceから切り離す。
K8sのProbeは200-399を成功、それ以外を失敗とみなす。常に200を返すと「障害中でもPodが正常」と判断され、トラフィックが流れ続ける。readinessは失敗時に503を返すことでPodをServiceから切り離す。
2
liveness / readiness の分離
livenessはプロセスの生死のみ確認する。依存サービスの障害でlivenessをFailさせると、Pod再起動ループに入り障害を悪化させる(cascading failure)。readinessだけを503にすればPodは生き続け、依存サービス復旧後に自動復帰できる。
livenessはプロセスの生死のみ確認する。依存サービスの障害でlivenessをFailさせると、Pod再起動ループに入り障害を悪化させる(cascading failure)。readinessだけを503にすればPodは生き続け、依存サービス復旧後に自動復帰できる。
3
プロトコル一致
MySQLはHTTPではなくMySQL独自プロトコル(TCP 3306)。RedisもHTTPではなくRESP(TCP 6379)。
MySQLはHTTPではなくMySQL独自プロトコル(TCP 3306)。RedisもHTTPではなくRESP(TCP 6379)。
requests.get() でこれらに接続しようとしてもハンドシェイクで必ず失敗する。
4
ベアexceptの排除
except: はKeyboardInterruptやSystemExitも補足してしまう。必ず具体的な例外クラスを指定する。
実務への応用
MOps/販促システムのArgo Workflowsジョブ
BigQueryやCloud Pub/Subに依存している場合、同様のヘルスチェック設計が活きる。WorkflowのPodに readinessProbe を設定しておくと、依存サービス障害時にジョブが途中でハングせずに早期失敗できる。また、DatadogのService Checkと組み合わせて、/health/ready の503をアラートトリガーにするのが実践的。
次のステップ
発展問題:
/health/ready にタイムアウト付き並列チェック(concurrent.futures.ThreadPoolExecutor)を実装し、複数依存サービスの応答を並列に確認する
- 参考: Kubernetes公式ドキュメント「Configure Liveness, Readiness and Startup Probes」
- 参考: Google Cloud「GKE best practices: health checks」
今日のまとめ
liveness(再起動判断)とreadiness(トラフィック制御)を分離し、障害時は503を返すことで、K8sのセルフヒーリングを正しく機能させる。
依存サービスの確認は必ずそのプロトコルで行う(MySQL →
依存サービスの確認は必ずそのプロトコルで行う(MySQL →
pymysql、Redis → redis.ping())。except: ではなく except SpecificError: が例外処理の基本。