Опирается на правила:
R-OBS-HC-1…R-OBS-HC-3иR-OBS-HC-X1…R-OBS-HC-X3из Observability Style Guide → раздел 4. Health checks.
Важно знать
- Liveness и readiness — разные роуты, разные семантики, разные зависимости.
/health/live— UP пока процесс отвечает. Не должен зависеть от внешних систем./health/ready— UP когда сервис готов принимать трафик: БД доступна, зависимости прогреты.- Custom check для критичной внешней системы — с TTL-кешем, чтобы probe не дудосила provider.
/infoсодержитgit_commit,build_version,service_name— для отладки версии в проде.- Health — техническое состояние, не бизнес.
order_count > N→ DOWN — антипаттерн (R-OBS-HC-X1).- Liveness не зависит от DB: иначе K8s рестартует pod в loop при кратковременной недоступности.
- Эндпоинты health/info размещают на отдельном management-порту (
R-OBS-CFG-1).
Health checks — то, на что смотрит Kubernetes, ALB и load balancer, решая «дать ли трафик в этот pod». Неправильно настроенные probes — главный источник каскадных отказов: одна реплика DB лагает → все pods unhealthy → весь сервис недоступен.
Liveness vs Readiness
R-OBS-HC-1: FastAPI не имеет встроенного Actuator — роуты создаются явно. Зато разделение liveness/readiness выходит чистым: никаких XML-конфигов, только функции.
from fastapi import FastAPI
app = FastAPI()
@app.get("/health/live")
async def liveness() -> dict:
return {"status": "UP"}
@app.get("/health/ready")
async def readiness() -> dict:
checks = await run_readiness_checks()
status = "UP" if all(c["status"] == "UP" for c in checks) else "DOWN"
return {"status": status, "checks": checks}
| Эндпоинт | Что проверяет | Что делает K8s |
|---|---|---|
/health/live | Процесс отвечает, event loop не завис | UP → продолжать; DOWN → рестартует pod |
/health/ready | DB доступна, зависимости прогреты | UP → шлёт трафик; DOWN → снимает из Service endpoints |
Семантическое различие критично:
- Если DB недоступна 5 секунд — readiness должен стать DOWN (трафик уйдёт на другие реплики), liveness должен остаться UP (рестарт pod не поможет, DB та же).
- Если event loop завис — liveness DOWN, K8s убивает pod, новый стартует с чистым состоянием.
K8s манифест для FastAPI-сервиса:
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /health/live
port: 9090
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 9090
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
Порт 9090 — отдельный management-порт, не business 8080. В FastAPI management-приложение монтируется отдельным ASGI-процессом (R-OBS-CFG-1):
import uvicorn
from fastapi import FastAPI
management_app = FastAPI(docs_url=None, redoc_url=None)
management_app.include_router(health_router)
management_app.include_router(info_router)
management_app.include_router(metrics_router)
if __name__ == "__main__":
import asyncio
async def serve_both():
config_business = uvicorn.Config(app, host="0.0.0.0", port=8080)
config_management = uvicorn.Config(management_app, host="0.0.0.0", port=9090)
server_business = uvicorn.Server(config_business)
server_management = uvicorn.Server(config_management)
await asyncio.gather(server_business.serve(), server_management.serve())
asyncio.run(serve_both())
Custom check с TTL-кешем
R-OBS-HC-2: для каждой критичной внешней системы — отдельная async-функция с TTL-кешем. Без кеша: K8s проверяет readiness каждые 5 секунд × 10 реплик = 20 ping/s на provider только от health-check.
import asyncio
import time
from dataclasses import dataclass, field
from typing import Optional
import httpx
@dataclass
class CachedCheck:
result: dict
expires_at: float
_sber_cache: Optional[CachedCheck] = None
_sber_lock = asyncio.Lock()
SBER_TTL_SECONDS = 10
async def check_sber_payment() -> dict:
global _sber_cache
now = time.monotonic()
if _sber_cache and _sber_cache.expires_at > now:
return _sber_cache.result
async with _sber_lock:
if _sber_cache and _sber_cache.expires_at > now:
return _sber_cache.result
result = await _do_check_sber()
_sber_cache = CachedCheck(result=result, expires_at=now + SBER_TTL_SECONDS)
return result
async def _do_check_sber() -> dict:
try:
async with httpx.AsyncClient(timeout=2.0) as client:
resp = await client.get("https://pay.sber.ru/ping")
resp.raise_for_status()
return {"status": "UP", "provider": "sber"}
except Exception as exc:
return {"status": "DOWN", "provider": "sber", "error": str(exc)}
Двойная проверка внутри asyncio.Lock — защита от race condition при параллельных coroutine: первый захватывает лок, остальные ждут; когда первый кладёт в кеш и отпускает — остальные видят свежий результат и не идут к provider.
Для DB и Redis auto-check — через asyncpg и redis.asyncio:
async def check_postgres(pool: asyncpg.Pool) -> dict:
try:
async with pool.acquire(timeout=1.0) as conn:
await conn.fetchval("SELECT 1")
return {"status": "UP", "component": "postgres"}
except Exception as exc:
return {"status": "DOWN", "component": "postgres", "error": str(exc)}
async def check_redis(redis_client) -> dict:
try:
await redis_client.ping()
return {"status": "UP", "component": "redis"}
except Exception as exc:
return {"status": "DOWN", "component": "redis", "error": str(exc)}
Итоговый readiness:
from fastapi import APIRouter, Response
health_router = APIRouter()
@health_router.get("/health/ready")
async def readiness(response: Response) -> dict:
checks = await asyncio.gather(
check_postgres(postgres_pool),
check_redis(redis_client),
check_sber_payment(),
)
checks_list = list(checks)
all_up = all(c["status"] == "UP" for c in checks_list)
if not all_up:
response.status_code = 503
return {
"status": "UP" if all_up else "DOWN",
"checks": checks_list,
}
Статус 503 при DOWN — обязателен: K8s читает HTTP-статус, а не тело.
/info с git и build метаданными
R-OBS-HC-3: аналог /actuator/info — эндпоинт, который отдаёт версию и commit сборки. Без него каждый инцидент начинается с «какая версия в проде сейчас» и поиска через CI-логи.
Метаданные записываем в файл на этапе сборки CI:
# .github/workflows/build.yml
- name: Write build info
run: |
cat > app/build_info.json << EOF
{
"git_commit": "${{ github.sha }}",
"git_branch": "${{ github.ref_name }}",
"build_time": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"version": "${{ github.run_number }}"
}
EOF
import json
from pathlib import Path
from fastapi import APIRouter
info_router = APIRouter()
def _load_build_info() -> dict:
path = Path(__file__).parent / "build_info.json"
if path.exists():
return json.loads(path.read_text())
return {"git_commit": "dev", "version": "local"}
_BUILD_INFO = _load_build_info()
@info_router.get("/info")
async def info() -> dict:
return {
"service": {"name": "order-service"},
"build": _BUILD_INFO,
}
Результат /info:
{
"service": {"name": "order-service"},
"build": {
"git_commit": "5380f21abc...",
"git_branch": "main",
"build_time": "2026-06-18T14:30:00Z",
"version": "142"
}
}
Liveness зависит от внешних систем
R-OBS-HC-X2: классическая ловушка.
# КАТАСТРОФА — liveness вернёт 503 при недоступности DB
@app.get("/health/live")
async def liveness_bad():
result = await check_postgres(postgres_pool)
if result["status"] != "UP":
raise HTTPException(status_code=503, detail=result)
return {"status": "UP"}
Сценарий: PG лагает 30 секунд из-за VACUUM FULL. Liveness DOWN → K8s убивает pod → новый стартует, та же DB лагает → DOWN → убивает → loop. Все реплики падают за минуту, сервис недоступен.
Liveness обязан проверять только сам процесс — event loop жив, asyncio отвечает. Внешние зависимости — только в readiness.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Business-state в health check (if order_count > N: status = "DOWN") | R-OBS-HC-X1 | техническое состояние; бизнес → SLO |
| Liveness зависит от DB/Redis/external | R-OBS-HC-X2 | только readiness зависит от внешних |
Probe делает business-операцию (create_test_order) | R-OBS-HC-X3 | light probe (ping, cached, SELECT 1) |
| check без TTL-кеша при частом опросе K8s | R-OBS-HC-2 | TTL 5–30 секунд + asyncio.Lock |
/health/ready возвращает 200 при DOWN в теле | R-OBS-HC-1 | HTTP 503 при любом DOWN-компоненте |
Нет /info с git_commit и версией | R-OBS-HC-3 | build_info.json из CI + /info эндпоинт |
Куда дальше
- Конфигурация — management-порт, exposed эндпоинты, отдельный ASGI.
- Context propagation — contextvars, request_id/user_id в middleware с очисткой.
- Logging — structlog, structured context, PII-маскировка.
- Metrics — prometheus-client, бизнес-метрики, cardinality.
- SLO и алерты — бизнес-цели отдельно от probes, error budget.
- Tracing — OTel автоинструментация, manual span, sampling.