“프로세스가 산다”와
“추론할 준비가 됐다”를 나눈다
/healthz는 FastAPI만 확인하고, /readyz는 Adapter를 통해 TurboFieldfare의 health와 model ID를 확인한다. 같은 “상태 검사”처럼 보여도 운영에서 답하는 질문은 다르다.
왜 endpoint가 두 개인가?
TurboFieldfare가 재시작되는 동안 FastAPI process 자체는 멀쩡할 수 있다. 이때 liveness까지 실패시키면 장애 원인이 아닌 Backend를 함께 재시작하게 만드는 잘못된 정책으로 이어질 수 있다. Readiness만 503으로 만들어 추론 의존성이 준비되지 않았다는 신호를 준다. 현재 코드에는 이 신호를 받아 다른 route를 자동 차단하거나 트래픽을 다른 서버로 옮기는 기능이 없다. 이후 요청 수용 정책이나 모니터가 이 신호를 사용하도록 연결해야 한다.
/healthz를 사용해 container 상태를 표시한다. Docker Engine은 unhealthy label만으로 container를 자동 재시작하지 않는다. Compose에 restart: always를 쓰는 것만으로 살아 있는 unhealthy container를 재시작하지도 않는다. 일반 restart policy는 container 프로세스 종료에 반응하므로 health 상태를 보고 복구하는 별도 제어와 구분한다. Docker의 restart policy 설명에서도 종료 조건을 확인할 수 있다.현재 전체 코드
from typing import Annotated
from fastapi import APIRouter, Depends, Request, Response, status
from app.api.dependencies import get_inference_engine
from app.engines.base import InferenceEngine, InferenceError
from app.schemas.system import HealthResponse, ReadinessResponse
router = APIRouter(tags=["system"])
@router.get(
"/healthz",
response_model=HealthResponse,
status_code=status.HTTP_200_OK,
summary="Check whether the FastAPI process is alive",
)
async def healthcheck(request: Request, response: Response) -> HealthResponse:
"""Return process liveness without checking external dependencies."""
response.headers["Cache-Control"] = "no-store"
settings = request.app.state.settings
return HealthResponse(
status="ok",
service=settings.service_name,
version=settings.service_version,
environment=settings.environment,
)
@router.get(
"/readyz",
response_model=ReadinessResponse,
responses={503: {"model": ReadinessResponse}},
summary="Check inference reachability and the configured model ID",
)
async def readiness(
response: Response,
engine: Annotated[InferenceEngine, Depends(get_inference_engine)],
) -> ReadinessResponse:
response.headers["Cache-Control"] = "no-store"
try:
await engine.check_ready()
except InferenceError as error:
response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
return ReadinessResponse(status="not_ready", error_code=error.code)
return ReadinessResponse(status="ready")Annotated[..., Depends(...)]를 풀어서 읽기
Client는 engine이라는 HTTP parameter를 보내지 않는다. FastAPI가 route 호출 전에 get_inference_engine(request)를 실행하고 반환 object를 인수로 넣는다. 이를 dependency injection이라고 한다.
/readyz 한 번의 내부 순서
GET /readyz를 readiness()와 연결한다.
현재 app state에서 InferenceEngine을 가져온다.
생성용과 분리된 probe client로 /health 200과 /v1/models의 model ID를 확인한다.
성공은 200 ready, InferenceError는 503 not_ready가 된다.
오류 처리에 HTTPException을 쓰지 않고 Response status를 바꾼 뒤 동일한 ReadinessResponse를 반환했다. 성공과 실패가 같은 예측 가능한 JSON 모양을 유지해 monitor와 test가 간단해진다.
두 응답 계약
class ReadinessResponse(BaseModel):
model_config = ConfigDict(frozen=True)
status: Literal["ready", "not_ready"]
dependency: Literal["inference"] = "inference"
error_code: InferenceErrorCode | None = None| 상황 | HTTP | JSON |
|---|---|---|
| FastAPI 생존 | 200 | {"status":"ok", ...} |
| Inference ready | 200 | {"status":"ready","dependency":"inference","error_code":null} |
| Server 꺼짐 | 503 | {"status":"not_ready",...,"error_code":"unavailable"} |
| Model ID 불일치 | 503 | error_code="model_mismatch" |
Cache-Control: no-store는 browser나 proxy가 과거의 ready 결과를 cache해 현재 상태처럼 보여주지 않게 한다.
Readiness가 보장하지 않는 것
- TurboFieldfare queue에 지금 빈자리가 있다는 보장은 아니다.
- 긴 prompt가 context limit 안에 들어간다는 보장은 아니다.
- 한 token이라도 실제로 생성했다는 보장은 아니다.
- 답변 내용이 정확하거나 빠르다는 품질 보장이 아니다.
Probe는 자주 호출되어야 하므로 model 생성을 포함하지 않는다. 실제 generation은 bounded smoke test와 이후 chat integration test에서 별도로 확인한다.
초기 실제 HTTP 관찰
macOS FastAPI와 Docker Backend 모두에서 같은 200 응답을 확인했다. 자동 test에서는 가짜 ready engine과 unavailable engine을 각각 주입해 200/503 분기, 그리고 inference가 실패해도 /healthz는 계속 200임을 확인했다.
보완 후 확인한 부하 조건
실제 TCP 회귀 테스트에서 생성 연결 4개를 점유한 동안에도 정상 모델이면 200, 다른 모델이면 model_mismatch를 담은 503이 나왔다. 생성 부하를 피하려고 모델 검사 자체를 생략한 것이 아니다.