AI‐7단계‐서비스 인프라 확장성과 모니터링 설계 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki
모델 추론은 원격 RunPod GPU Worker로 분리합니다. Colab T4는 호환성·성능 측정에만 사용합니다. EC2는 FastAPI(인메모리 큐, 소비자 2개)와 PostgreSQL 참조 조회를 담당하고, 작업마다 이미지 URL과 SHA-256을 Worker
POST /internal/v1/infer에 보냅니다. Worker는 Ollama·PP-OCR·YOLO-World를 한 프로세스에서 실행하며 시작 시 Gemma를 미리 적재합니다. 인메모리 큐는 재시작 시 유실됩니다.
단일 FastAPI 인스턴스가 asyncio.Queue 하나와 작업 상태·결과를 담는 인메모리 저장소를 소유합니다. 소비자 코루틴 2개가 작업을 꺼내 파이프라인을 실행하며 모델 추론은 원격 GPU Worker에 요청합니다.
이 선택은 단순성과 비용을 우선한 MVP 결정입니다. 프로세스 재시작·배포·비정상 종료·EC2 중지 시 대기 중, 처리 중, 완료 후 조회 대기 중인 작업과 결과가 모두 사라집니다. 영속 복구, 다중 인스턴스, 무중단 배포를 제공한다고 표현하지 않습니다.
현재 EC2는 모델을 직접 실행하지 않으므로 특정 고사양 CPU 인스턴스를 전제로 하지 않습니다. FastAPI RSS와 원격 Worker 부하를 분리해 측정한 뒤 인스턴스 유형을 정하며, GPU 처리량은 고정 RunPod 배포 후 별도로 검증합니다. Uvicorn은 인메모리 큐가 분리되지 않도록 worker 1개를 유지합니다.
flowchart TD
P[PWA] --> B[서비스 백엔드]
B --> A[단일 FastAPI AI 서버]
A -->|분석 완료 콜백| B
A --> Q[인메모리 asyncio.Queue]
Q --> C[소비자 코루틴 1개]
C --> E[CPU 추론 전용 실행기]
E --> C
C --> M[인메모리 상태·결과]
A --> M
A --> PG[(AI PostgreSQL<br/>레시피 원본)]
B -->|커서 기반 동기화 조회| A
B --> MY[(백엔드 MySQL<br/>재료 원본·레시피 사본)]
A --> OT[OpenTelemetry·OpenInference 계측]
OT --> PX[Phoenix OSS<br/>요청·LLM trace]
A --> PR[Prometheus<br/>집계 지표]
E --> PR
PR --> GR[Grafana OSS<br/>대시보드·알림]
FastAPI의 Uvicorn worker는 하나만 실행합니다. 여러 worker를 띄우면 각 프로세스가 서로 다른 큐와 상태 저장소를 갖기 때문입니다. 모델 작업은 원격 Worker에 맡깁니다. 종료 시 신규 접수는 중단하지만 현재 AnalysisRuntime.stop()은 진행 작업을 기다리지 않고 소비자를 취소하므로 처리 중 작업이 유실될 수 있습니다. 완료 결과는 기본 1시간, 최대 1,000건으로 메모리에 보관합니다.
인메모리 큐의 maxsize는 대기 작업 100개로 설정하고 큐가 가득 차면 429 AI_QUEUE_FULL로 신규 접수를 거절합니다. 오류 응답에는 retryable=true, retryAfterMs=3000과 작업량이 많다는 안내 문구를 포함합니다. 분석이 끝나면 AI가 환경설정에 등록된 백엔드 POST /api/v1/internal/image-analyses/callback로 완료 콜백을 보내고, 백엔드는 analysisId로 멱등 처리합니다. GET /ai/v1/analyses/{analysisId}는 정상 폴링이 아니라 예상 완료 제한시간이 지난 콜백 누락 작업에 대한 제한된 확인용입니다. 제한시간과 확인 횟수는 실제 완료시간 분포로 정합니다. 콜백 전달 상태와 재시도 기록도 AI 메모리에 있으므로 재시작 이후 전달은 보장하지 않습니다.
서비스 제공 시간에는 AI 인스턴스를 실행 상태로 유지합니다. AI 서버가 중지된 동안에는 작업을 접수하거나 상태를 조회할 인메모리 런타임도 없으므로, 백엔드는 요청을 보관했다고 표시하지 않고 503 AI_GATEWAY_UNAVAILABLE과 직접 입력 또는 나중에 다시 시도하는 경로를 제공합니다. 자동 시작 방식은 현재 결정하지 않았습니다.
분석 작업의 analysisId, requestId, 상태, 중간값, 최종 결과는 FastAPI 프로세스 수명 안에서만 유효합니다. 같은 프로세스 안에서는 동일한 requestId와 동일 입력을 기존 작업에 연결하고, 다른 입력이면 충돌로 처리합니다. 재시작 후에는 기존 멱등성 기록도 사라지므로 백엔드는 새 작업으로 다시 요청할 수 있습니다.
한 작업의 단계는 QUEUED → PROCESSING → COMPLETED|FAILED이며 내부 현재 단계는 작업 객체에 기록합니다. 단계별 메시지 재전달, 가시성 시간, lease, 실패 대기열은 사용하지 않습니다. 모델 단계가 실패하면 정책이 허용하는 제한된 자체 재시도를 같은 작업 실행 안에서 수행하고, 끝내 실패하면 오류 코드를 결과에 남깁니다. 프로세스가 죽은 작업은 실패 결과도 남지 않으며 자동 복구하지 못합니다.
완료 결과 TTL이 지나거나 최대 보관 건수를 넘으면 메모리에서 제거합니다. 제거 후 조회는 404 ANALYSIS_NOT_FOUND가 됩니다. 제거 순서와 수치는 운영 측정 후 확정하며, 메모리가 부족해질 때까지 무제한 보관하지 않습니다.
사용자·냉장고·재료의 원본은 백엔드 MySQL이 소유합니다. 백엔드는 권한을 확인한 현재 재고 스냅샷을 추천 요청에 넣고, AI는 이를 사용자 재고 원본으로 저장하거나 수정하지 않습니다.
레시피 원본은 AI PostgreSQL이 소유합니다. AI는 전달받은 재료로 식품안전나라를 우선 조회하고, 사용 가능한 후보가 없으면 TheMealDB를 조회하며, 두 구조화 API 모두 사용 가능한 후보가 없을 때만 Tavily를 사용합니다. 검증·정규화하고 저장이 허용된 레시피만 PostgreSQL에 저장합니다. 공급자별 이용 조건과 저장 허용 범위는 운영 정책으로 관리합니다.
백엔드의 주기 작업은 FastAPI 동기화 API에서 변경분을 커서로 가져와 자신의 MySQL recipes, recipe_ingredients, recipe_steps를 트랜잭션으로 갱신합니다. AI 서버에 백엔드 MySQL 쓰기 권한을 주지 않습니다. 실제 동기화 주기, 한 페이지 크기, 최초 전체 동기화 절차와 인증 방식은 백엔드·AI 팀 합의가 필요합니다.
현재 토큰 지표의 대상은 GPU Worker 안의 Ollama gemma4:e2b 호출입니다. OCR, 이미지 분류, 식품안전나라, TheMealDB와 Tavily 호출은 토큰 기반 모델 호출이 아니므로 같은 토큰 지표에 합산하지 않습니다.
Ollama API의 완료 응답은 다음 값을 나노초 단위 시간과 함께 제공합니다.
| Ollama 응답 필드 | 의미 | 기록할 지표 |
|---|---|---|
prompt_eval_count |
입력 프롬프트 토큰 수 | llm_prompt_tokens_total |
prompt_eval_duration |
입력 프롬프트 처리시간 | llm_prompt_duration_seconds |
eval_count |
생성한 출력 토큰 수 | llm_completion_tokens_total |
eval_duration |
출력 토큰 생성시간 | llm_generation_duration_seconds |
load_duration |
모델 적재시간 | llm_model_load_duration_seconds |
total_duration |
Ollama 내부 전체 처리시간 | llm_request_duration_seconds |
공식 근거: https://docs.ollama.com/api/usage
처리량은 누적값을 평균내지 않고 호출별로 계산한 뒤 histogram으로 집계합니다.
prompt_tokens_per_second = prompt_eval_count / (prompt_eval_duration / 1_000_000_000)
generation_tokens_per_second = eval_count / (eval_duration / 1_000_000_000)
total_tokens = prompt_eval_count + eval_count
prompt_eval_duration이나 eval_duration이 0이면 해당 처리량을 계산하지 않습니다. 평균뿐 아니라 중앙값, 95%·99% 완료시간과 처리량 분포를 봅니다. 비스트리밍 응답의 total_duration만으로는 첫 토큰 도착시간을 알 수 없으므로, TTFT가 필요하면 스트리밍 모드에서 요청 시작부터 첫 응답 chunk까지 별도로 측정합니다.
모델 이름, 양자화 버전, 프롬프트 버전, 입력 유형과 호출 결과를 함께 기록해야 비교가 가능합니다. 사용자 ID, 전체 OCR, 재고 목록, 원본 프롬프트와 모델 출력은 지표 label에 넣지 않습니다.
로컬 Ollama 모델에는 공급자가 청구하는 입력·출력 토큰 단가가 없습니다. 따라서 토큰 수 × 외부 API 단가를 비용으로 표시하지 않습니다. 대신 EC2 시간당 비용을 배포 시점의 실제 청구 단가로 설정하고 모델 점유시간 기준의 추정 계산비용을 별도로 표시합니다.
estimated_compute_cost
= configured_instance_hourly_cost
× model_occupied_seconds / 3600
estimated_cost_per_1k_generated_tokens
= estimated_compute_cost / eval_count × 1000
이 값은 한 인스턴스의 공용 CPU·메모리 비용을 한 호출에 배분한 추정치이며 실제 청구 항목이 아닙니다. 유휴시간과 여러 작업이 공유한 자원을 포함하는 월 실제 EC2 비용은 AWS 청구 내역에서 별도로 확인합니다. Tavily 크레딧과 TheMealDB 키 비용도 LLM 토큰 비용에 합치지 않고 공급자 호출 비용으로 분리합니다.
EC2는 UTC 날짜별 파일 로그와 요청·응답 감사 로그를 기록하고 Worker는 단계별 시간·Ollama 토큰 진단값(diagnostics)을 반환합니다. 아래 관측 도구가 이 데이터를 수집·조회·시각화합니다.
| 도구 | 역할 | 선택 이유 | 운영 제약 |
|---|---|---|---|
| OpenTelemetry + OpenInference | FastAPI 요청, Ollama 호출, 레시피 검색을 하나의 trace로 연결 | 공개 표준이라 특정 관측 제품에 종속되지 않음 | Ollama 응답의 토큰 필드는 수동 span 속성 매핑 필요 |
| Phoenix OSS | 요청별 단계, 프롬프트 버전, 토큰, 지연과 오류 trace 조회 | 오픈소스이며 OTLP/OpenInference를 지원하고 토큰·비용 속성을 조회 가능 | 자체 호스팅 저장공간·백업·접근 통제 필요 |
| Prometheus | 큐, HTTP, 모델, 토큰과 시스템 지표 집계 | 오픈소스이며 시계열 지표와 alert rule에 적합 | 고유 ID를 label로 쓰면 cardinality가 폭증하므로 금지 |
| Grafana OSS | Prometheus 대시보드와 알림 시각화 | 무료 자체 호스팅 가능 | 대시보드·알림 규칙과 사용자 접근을 직접 관리 |
Phoenix 공식 문서: https://arize.com/docs/phoenix/, https://arize.com/docs/phoenix/tracing/how-to-tracing/cost-tracking
Grafana·Prometheus 공식 문서: https://grafana.com/docs/grafana/latest/fundamentals/getting-started/first-dashboards/get-started-grafana-prometheus/
Phoenix에는 한 분석 요청을 root span으로 보내고 ocr, ingredient_candidate, ollama_classification, recipe_provider, normalization을 자식 span으로 연결합니다. 성공 여부, 오류 코드, 모델·프롬프트·파서 버전, 토큰 수와 시간만 기본 기록합니다. 원본 이미지, 전체 OCR, 사용자 재고, API 키와 인증 헤더는 보내지 않습니다. 디버깅용 입력·출력 본문 수집은 별도 개인정보 승인을 받기 전까지 비활성화합니다.
관측 도구 장애가 사용자 요청을 실패시키면 안 됩니다. trace와 metric 전송은 비동기로 처리하고 짧은 타임아웃 뒤 폐기할 수 있어야 합니다. Phoenix·Prometheus가 내려가도 분석과 레시피 추천은 계속하며 관측 누락 횟수만 로컬 운영 로그에 남깁니다.
Phoenix, Prometheus와 Grafana를 AI 모델 인스턴스에 함께 배치할지는 아직 확정하지 않습니다. 먼저 개발 환경에서 저장량과 CPU·메모리 사용량을 측정하고, 모델 추론 자원을 침범하면 별도 관측 인스턴스나 관리형 무료 구간을 검토합니다. trace 보관기간, 성공 요청 샘플링 비율과 백업 정책도 측정·개인정보 검토 후 정합니다.
| 지표 | 수집 위치 | 목적 |
|---|---|---|
| 인메모리 큐 길이·가장 오래된 작업 대기시간 | FastAPI 런타임 | 접수 속도가 처리 속도를 넘는지 확인 |
| 큐 거절 수·콜백 성공·실패·재시도 수 | FastAPI 런타임 | 과부하와 결과 전달 누락 확인 |
| 전체·단계별 실행시간 | 작업 구조화 로그 | OCR, Gemma, 레시피 공급자 호출 병목 구분 |
| 프롬프트 입력 토큰·출력 토큰·총 토큰 | Ollama 완료 응답·Phoenix | 프롬프트 증가와 불필요한 출력 탐지 |
| 프롬프트 처리 TPS·생성 TPS | Ollama 완료 응답·Prometheus | 모델·양자화·서버 변경 전후 처리량 비교 |
| 모델 적재시간·TTFT·전체 LLM 시간 | Ollama·FastAPI 계측 | cold load, 첫 응답 지연, 생성 병목 구분 |
프롬프트 버전별 오류·NEEDS_REVIEW 비율 |
Phoenix·평가 집계 | 빠르지만 품질이 낮은 프롬프트 배포 방지 |
| EC2 점유시간 추정비용·월 실제 비용 | 비용 지표·AWS 청구 내역 | 토큰당 추정 계산비용과 실제 인프라 비용 비교 |
| 완료·실패·만료·직접 입력 전환 수 | FastAPI와 백엔드 집계 | 유실 가능성과 사용자 영향 확인 |
| CPU·프로세스 메모리·디스크 | EC2 수집 도구 | 이벤트 루프·실행기·모델 자원 확인 |
| 인메모리 결과 건수와 추정 사용량 | FastAPI 런타임 | TTL과 최대 보관 건수 결정 |
| 프로세스 재시작·비정상 종료 | EC2·컨테이너 로그 | 복구 불가능 작업의 발생 범위 확인 |
| 레시피 검색 성공·실패·공급자 제한 | AI 레시피 모듈 | 출처 품질과 이용 가능성 확인 |
| 동기화 지연·페이지 실패·revision 충돌 | AI API와 백엔드 동기화 작업 | PostgreSQL 원본과 MySQL 사본의 차이 확인 |
| 공급자별 호출·Tavily 크레딧·TheMealDB 키 비용 | 비용 관리 모듈 | 무료 한도와 승인된 외부 API 예산 준수 |
Prometheus label은 model, model_version, prompt_version, stage, status, error_code처럼 값의 종류가 제한된 항목만 허용합니다. analysisId, requestId, 사용자 ID, 재료명과 원문을 label로 넣지 않습니다. 개별 요청 연결은 Phoenix의 trace ID를 사용합니다.
구체적인 경고 임계값은 부하 측정 전에는 정하지 않습니다. 초기 대시보드는 큐 대기시간, 전체 분석시간, Ollama 입력·출력 토큰, prompt TPS, generation TPS, 모델 적재시간, 오류율, 프로세스 메모리와 외부 API 호출량을 함께 표시합니다. 로그에는 원본 영수증, 인증정보, 전체 OCR, 사용자 재고 스냅샷을 남기지 않습니다.
| 시험 | 확인할 내용 |
|---|---|
| 동시 분석 1·3·5·10건 | 큐 증가, API 응답성, 처리시간, 최대 메모리, 작업 누락 |
| 지속 유입 | 유입 중단 후 큐가 회복되는지와 접수 제한 필요성 |
| 큐 상한 도달 | 대기 작업 100개 상태에서 다음 신규 요청에 즉시 429 AI_QUEUE_FULL을 반환하고 생성 중인 작업 객체를 정리하는지 |
| CPU 작업 실행 | 상태 조회가 이벤트 루프 차단 없이 응답하는지 |
| 고정 프롬프트 반복 | 입력·출력 토큰과 TPS 계산이 Ollama 원본 응답과 일치하는지 |
| 프롬프트 버전 비교 | 같은 고정 평가 입력에서 토큰 수·지연·정확도를 함께 비교하는지 |
| 모델 cold/warm 실행 |
load_duration, TTFT, 전체시간 차이를 분리하는지 |
| 관측 도구 장애 | Phoenix·Prometheus 중단이 분석 요청을 실패시키지 않는지 |
| 콜백 실패·중복 | 제한 재시도와 GET 확인 경로가 동작하고 백엔드가 같은 analysisId를 멱등 처리하는지 |
| 고 cardinality 방지 | Prometheus label에 요청·사용자·원문 값이 들어가지 않는지 |
| 정상 종료 | 신규 접수 차단과 진행 작업의 유예시간 내 완료 여부 |
| 강제 종료·재시작 | 작업이 복구되지 않고 이전 ID 조회가 404가 되는 명시적 제약 확인 |
| 결과 TTL·최대 건수 | 제거 이후 404, 메모리 상한, 최근 작업 보존 순서 |
| 레시피 중복 수집 | 같은 source + sourceRecipeId + sourceRevision이 중복 원본을 만들지 않는지 |
| 동기화 재실행 | 같은 cursor 페이지를 반복해도 MySQL 사본이 중복되지 않는지 |
| 동기화 중 장애 | 부모·재료·단계가 부분 반영되지 않고 기존 사본이 유지되는지 |
| 비활성화 | PostgreSQL 변경이 MySQL status=INACTIVE로 반영되고 물리 삭제되지 않는지 |
현재 문서에는 위 구조의 실제 EC2 부하 결과나 동기화 시험 결과가 없습니다. 단계 2의 기존 로컬 실험은 모델 품질·시간의 참고값이며 이 인메모리 런타임의 운영 성능 증거가 아닙니다.
초기 구조는 단일 인스턴스·단일 Uvicorn worker만 지원합니다. 다음 요구가 생기면 인메모리 큐를 유지한 채 해결됐다고 주장하지 않고 작업 저장·분산 실행 구조를 다시 설계합니다.
- 재시작 후에도 작업을 복구해야 함
- 무중단 배포가 필요함
- AI 인스턴스를 여러 대 운영해야 함
- 한 인스턴스의 처리량보다 지속 유입이 많음
- 완료 결과를 장기간 조회해야 함
이 조건에 도달하면 복구 보장, 처리량, 운영 복잡도와 비용 요구사항을 기준으로 영속 작업 저장소와 분산 실행 방식을 선택합니다. 이는 초기 인메모리 큐와 분리된 확장 구조입니다.