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 × 안정 여유를 고려해 설정하고, 실제 트래픽에 따라 점진적으로 조정
다. 테스트 및 튜닝 전략
- 부하 시뮬레이션:
k6
, Locust
로 각 엔드포인트에 TPS 부여
- 응답 지표 분석: p95/p99 응답시간, 타임아웃률, 오류율
- CPU 사용률 확인:
htop
, psutil
등으로 추적
- 세마포어 수 변화 실험: 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 활용 극대화 |
응답 안정성 |
요청 지연 발생 |
병렬 제어로 실시간성 확보 |