Опирается на правила:
R-SHUT-CFG-1…R-SHUT-CFG-4иR-SHUT-CFG-X1из Graceful Shutdown Style Guide → раздел 1. Runtime/конфигурация.
Важно знать
--timeout-graceful-shutdown 30обязателен — без явного значения uvicorn рвёт активные запросы немедленно на SIGTERM, клиент получает 502.- lifespan-shutdown — единственное место, где корректно переключать readiness-флаг, останавливать aiokafka, закрывать
engine.- Readiness-флаг переключается первым в lifespan-shutdown, до всего остального;
/health/ready→ 503, k8s убирает pod из endpoints.- Свой
shutting_down: boolкак module-level переменная — не интегрируется с health-эндпоинтами, k8s об этом не узнает.- Раздельные
/health/liveи/health/readyобязательны — падение liveness перезапускает pod, падение readiness убирает из endpoints.- Без правильной конфигурации rolling deploy = шторм 502 на клиентах
OrderService,ProductService,CustomerService.
Graceful shutdown в FastAPI — это оркестрированная последовательность: переключить readiness → дать k8s убрать из endpoints → дожать in-flight HTTP → остановить фоновые задачи → закрыть aiokafka → engine.dispose() → выход. Описанный ниже минимальный набор параметров uvicorn и структура lifespan покрывают эту последовательность по правилам R-SHUT-CFG-*.
--timeout-graceful-shutdown
R-SHUT-CFG-1: первый и главный параметр запуска.
# entrypoint: main.py
import uvicorn
if __name__ == "__main__":
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8080,
timeout_graceful_shutdown=30,
)
В Dockerfile/Kubernetes обычно передаётся через CLI:
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080",
"--timeout-graceful-shutdown", "30"]
Что делает:
- При SIGTERM uvicorn перестаёт принимать новые connections.
- In-flight запросы к
OrderService,ProductServiceпродолжают обрабатываться доtimeout_graceful_shutdownсекунд. - Только потом процесс завершается.
Без timeout_graceful_shutdown uvicorn использует значение None — это означает немедленное прерывание воркеров: активные HTTP рвутся, клиент получает ConnectionResetError / 502.
Явный timeout R-SHUT-CFG-2
Диапазон 20–45s — баланс между «успеть дрейнить» и «не получить SIGKILL»:
- < 20s — мало для дрейна под нагрузкой; долгие запросы к
CustomerService(оплата, списание) прерываются. - > 45s — риск SIGKILL внутри
terminationGracePeriodSeconds: 60; полный бюджет не помещается. - 30s — подходит для типичных REST API (большинство запросов < 1s, p99 ~ 5s).
Если есть долгие синхронные эндпоинты (> 10s) — не увеличивайте timeout, декомпозируйте эндпоинт на 202 Accepted + polling (см. R-SHUT-HTTP-3 в HTTP drain).
Readiness-флаг и lifespan-shutdown
R-SHUT-CFG-3: readiness переключается первым в lifespan-shutdown.
# app/state.py
from dataclasses import dataclass, field
@dataclass
class AppState:
is_ready: bool = True
app_state = AppState()
# app/main.py
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.state import app_state
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(application: FastAPI):
logger.info("startup: application ready")
app_state.is_ready = True
yield
# shutdown — readiness первым
logger.info("получили SIGTERM, начинаем graceful shutdown")
app_state.is_ready = False
# далее — остановка задач, aiokafka, engine.dispose()
await asyncio.sleep(0) # дать event loop обработать pending callbacks
app = FastAPI(lifespan=lifespan)
Последовательность на SIGTERM:
- uvicorn получает SIGTERM, вызывает lifespan-shutdown.
app_state.is_ready = False— readiness-флаг переключён./health/ready→ 503 (k8s readiness probe видит fail).- k8s убирает pod из Service endpoints (через ~5–10s, пока probe опросит).
- Новый трафик к
OrderServiceперестаёт приходить. - In-flight запросы дожимаются до
timeout_graceful_shutdown.
Health-эндпоинты R-SHUT-CFG-4
# app/routes/health.py
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from app.state import app_state
router = APIRouter()
@router.get("/health/live")
async def liveness():
return {"status": "alive"}
@router.get("/health/ready")
async def readiness():
if not app_state.is_ready:
return JSONResponse(
status_code=503,
content={"status": "not_ready"},
)
return {"status": "ready"}
# app/main.py — подключение роутера
from app.routes.health import router as health_router
app.include_router(health_router)
Разница между эндпоинтами принципиальна:
/health/live— процесс жив (event loop не завис). Падение → k8s перезапускает pod./health/ready— готов принимать трафик. Падение → k8s убирает из endpoints (что нам и нужно на shutdown).
На shutdown нужен ready=503, не live=503. Подробнее различие — Observability → health checks.
Полный lifespan для ProductService
Реальный сервис с aiokafka и SQLAlchemy:
# app/main.py
import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine
from app.database import engine
from app.kafka import consumer, producer
from app.scheduler import scheduler
from app.state import app_state
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(application: FastAPI):
await consumer.start()
await producer.start()
scheduler.start()
app_state.is_ready = True
logger.info("ProductService startup complete")
yield
logger.info("получили SIGTERM, начинаем graceful shutdown")
app_state.is_ready = False
scheduler.shutdown(wait=True)
await consumer.stop()
await producer.stop()
await engine.dispose()
logger.info("graceful shutdown complete")
Порядок закрытия в lifespan-shutdown важен:
is_ready = False— сразу, первым.scheduler.shutdown(wait=True)— ждём текущую итерацию (до 25s).consumer.stop()/producer.stop()— коммит offset, flush (до 15s).engine.dispose()— закрытие пула после задач и Kafka.
Если engine.dispose() вызвать раньше scheduler — scheduler упадёт на первом же запросе к БД (пул уже закрыт).
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
--timeout-graceful-shutdown отсутствует | R-SHUT-CFG-1 | явный параметр 20–45s |
timeout_graceful_shutdown=0 или принудительный kill | R-SHUT-HTTP-X1 | 30s типично |
timeout_graceful_shutdown < 20 | R-SHUT-CFG-2 | минимум 25s |
timeout_graceful_shutdown > 45 | R-SHUT-CFG-2 | помещаться в 60s total budget |
shutting_down: bool на уровне модуля вместо readiness-флага | R-SHUT-CFG-X1 | app_state.is_ready через lifespan |
| Readiness переключается не первым в lifespan-shutdown | R-SHUT-CFG-3 | первая строка shutdown-блока |
/health/live и /health/ready объединены в один эндпоинт | R-SHUT-CFG-4 | раздельные маршруты обязательны |
engine.dispose() до остановки фоновых задач | R-SHUT-DB-X1 | последним после задач/Kafka |
Куда дальше
- Бюджеты и observability — раскладка 60s budget и метрика
app_shutdown_duration_seconds. - HTTP drain — что происходит с in-flight запросами к
OrderService. - Kafka shutdown —
consumer.stop()иproducer.stop()в lifespan. - БД и persistence —
engine.dispose()и порядок закрытия пула. - Фоновые задачи / async / outbox —
scheduler.shutdown(wait=True)иCancelledError. - Kubernetes — preStop,
terminationGracePeriodSeconds, probes. - Идемпотентность in-flight операций — retry-safe операции на SIGTERM.