서비스 학습
SOURCE · be/app/api/routes/system.py · 2026-09-06

“프로세스가 산다”와
“추론할 준비가 됐다”를 나눈다

/healthz는 FastAPI만 확인하고, /readyz는 Adapter를 통해 TurboFieldfare의 health와 model ID를 확인한다. 같은 “상태 검사”처럼 보여도 운영에서 답하는 질문은 다르다.

왜 endpoint가 두 개인가?

TurboFieldfare가 재시작되는 동안 FastAPI process 자체는 멀쩡할 수 있다. 이때 liveness까지 실패시키면 장애 원인이 아닌 Backend를 함께 재시작하게 만드는 잘못된 정책으로 이어질 수 있다. Readiness만 503으로 만들어 추론 의존성이 준비되지 않았다는 신호를 준다. 현재 코드에는 이 신호를 받아 다른 route를 자동 차단하거나 트래픽을 다른 서버로 옮기는 기능이 없다. 이후 요청 수용 정책이나 모니터가 이 신호를 사용하도록 연결해야 한다.

Docker 주의: 현재 Dockerfile의 HEALTHCHECK는 /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(...)]를 풀어서 읽기

engine: Annotated[ InferenceEngine, # route가 기대하는 type Depends(get_inference_engine) # 실제 값을 구하는 방법 ]

Client는 engine이라는 HTTP parameter를 보내지 않는다. FastAPI가 route 호출 전에 get_inference_engine(request)를 실행하고 반환 object를 인수로 넣는다. 이를 dependency injection이라고 한다.

/readyz 한 번의 내부 순서

1. FastAPI route matching

GET /readyzreadiness()와 연결한다.

2. Dependency resolution

현재 app state에서 InferenceEngine을 가져온다.

3. Upstream probes

생성용과 분리된 probe client로 /health 200과 /v1/models의 model ID를 확인한다.

4. Stable response

성공은 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
상황HTTPJSON
FastAPI 생존200{"status":"ok", ...}
Inference ready200{"status":"ready","dependency":"inference","error_code":null}
Server 꺼짐503{"status":"not_ready",...,"error_code":"unavailable"}
Model ID 불일치503error_code="model_mismatch"

Cache-Control: no-store는 browser나 proxy가 과거의 ready 결과를 cache해 현재 상태처럼 보여주지 않게 한다.

Readiness가 보장하지 않는 것

Probe는 자주 호출되어야 하므로 model 생성을 포함하지 않는다. 실제 generation은 bounded smoke test와 이후 chat integration test에서 별도로 확인한다.

초기 실제 HTTP 관찰

HTTP/1.1 200 OK cache-control: no-store content-type: application/json {"status":"ready","dependency":"inference","error_code":null}

macOS FastAPI와 Docker Backend 모두에서 같은 200 응답을 확인했다. 자동 test에서는 가짜 ready engine과 unavailable engine을 각각 주입해 200/503 분기, 그리고 inference가 실패해도 /healthz는 계속 200임을 확인했다.

보완 후 확인한 부하 조건

실제 TCP 회귀 테스트에서 생성 연결 4개를 점유한 동안에도 정상 모델이면 200, 다른 모델이면 model_mismatch를 담은 503이 나왔다. 생성 부하를 피하려고 모델 검사 자체를 생략한 것이 아니다.

← 이전Engine 의존성다음 →응답 schema