FastAPI 동작 원리를 활용한 전략 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

FastAPI + Uvicorn 동작 원리

1. 현재 우리 서비스는?

a. 작업 유형 정리

구분 설명 처리 시간 성격
이미지 I/O + torch 처리 이미지 로딩 + torch 추론 1.5~2초 CPU + C 연산, 장시간 점유
나머지 CPU-heavy 요청 수치 계산, 전처리 등 수~수십 ms 순수 Python, 짧은 연산

b. 현재 시스템 구조 요약

  • 모든 엔드포인트는 async def로 작성됨
  • 모든 CPU 바운드 작업은 await loop.run_in_executor(...)로 스레드풀에 offload됨
  • 병렬 제어를 위해 직렬 큐 기반 구조 도입:
    • 이미지 처리 요청 → 1 worker 직렬 큐
    • 짧은 CPU 작업 4개 엔드포인트 → 1 worker 직렬 큐

c. 현재 구조의 문제점

가. 짧은 작업끼리 직렬 큐 공유 → 병목

  • 서로 다른 4개 엔드포인트가 하나의 직렬 큐를 공유하면서 동시 실행 불가
  • 작업 하나가 실행되는 동안 나머지는 불필요하게 대기
  • 각 요청은 ms 단위로 끝남에도 수백 ms 지연 발생 가능

나. 스레드풀은 유휴 상태인데 활용 못함

  • 직렬 큐 때문에 작업이 늦게 큐에서 풀리니,
  • 스레드는 놀고, 요청은 밀리는 비효율 상태

다. 직렬 큐는 긴 작업엔 좋지만 짧은 작업엔 독

  • 이미지 추론처럼 2초 이상 걸리는 작업엔 직렬 큐로 제어하는 것이 좋음
  • 하지만 ms 단위로 끝나는 작업엔 병렬 허용이 실시간성 확보에 유리

d. 개선 방안: 세마포어 기반 병렬 제한으로 전환

가. 전략 목표

  • 긴 작업은 제한된 병렬성 또는 직렬 유지
  • 짧은 작업은 병렬로 처리하되, 최대 동시 처리 수를 제한하여 시스템 안정성 확보

나. 병렬성 설정 기준

항목 권장 수치
CPU 논리 코어 수 8
torch 내부 스레드 1 (torch.set_num_threads(1))
전체 ThreadPoolExecutor max_workers 16
이미지 처리 세마포어 수 2~3
짧은 작업 세마포어 수 6~10
  • 세마포어 수는 처리 시간 × TPS × 안정 여유를 고려해 설정하고, 실제 트래픽에 따라 점진적으로 조정

다. 테스트 및 튜닝 전략

  1. 부하 시뮬레이션: k6, Locust로 각 엔드포인트에 TPS 부여
  2. 응답 지표 분석: p95/p99 응답시간, 타임아웃률, 오류율
  3. CPU 사용률 확인: htop, psutil 등으로 추적
  4. 세마포어 수 변화 실험: 4, 6, 8, 10 등 단계별 변경 후 지표 비교

2. 개선 계획

a. 기본 스레드풀 제한

from concurrent.futures import ThreadPoolExecutor
loop.set_default_executor(ThreadPoolExecutor(max_workers=16))
  • 전체적으로 동시에 실행 가능한 CPU 작업 수를 16으로 제한

b. 세마포어 기반 병렬 제어

# 긴 작업: torch 추론
image_semaphore = asyncio.Semaphore(3)

# 짧은 작업: 빠른 계산
fastcpu_semaphore = asyncio.Semaphore(10)

3. 결론 요약

항목 문제 개선 전략
긴 작업 (이미지 추론) 적절 유지 or Semaphore(2~3)
짧은 작업 (4개) 병목, 지연 Semaphore(6~10) 도입
전체 스레드풀 효율 저하 ThreadPoolExecutor 활용 극대화
응답 안정성 요청 지연 발생 병렬 제어로 실시간성 확보