MVP에서의 요청 과부하 대응 방안 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki
[Deprecated] Serial Task Queue 구현
1. 메시지 큐 → 적절하지 않음
a. 왜 메시지 큐가 필요한가?
가. 임베딩 작업은 고비용 (시간 + CPU/메모리 많이 씀)
- 동시에 여러 요청이 들어오면 서버가 과부하되거나 터질 수 있음
나. 이를 방지하려면 비동기 처리 + 큐잉이 필요
- 메시지 큐를 도입해 요청을 쌓아두고, 워커가 하나씩 순차/병렬로 처리하게 함
- 서버는 폭주 없이 안정적으로 유지됨
b. 왜 지금 단계에서 도입이 어려운가?
가. 큐 기반 시스템에서는 즉시 응답 + 나중에 콜백 알림이 정석
- 요청 오면 →
"작업 수신"
만 응답 (202 Accepted
) - 나중에 작업 끝나면 → AI 서버가 백엔드에 직접 완료 알림 (
POST /notify/embedding_done
) - 백엔드는 이 알림을 받고 후속 흐름 시작
나. 현재 방식은 "요청 → 처리 → 응답"이 동기 방식
- 백엔드는 요청을 보내고 임베딩이 끝날 때까지 기다림
- 응답이 늦어지면 실패 또는 타임아웃으로 간주함
다. 메시지 큐를 도입하면, 처리는 나중에 되므로 응답이 늦어질 수 있음
-
그대로 두면 백엔드는 “요청 실패”로 오해함
-
즉, "성공했지만 아직 처리 안됨"이라는 상태를 전달할 방법이 없음
→ 그래서 응답 방식도 바뀌어야 함
라. 메시지 큐 도입 기준 전체 비동기 흐름
- 백엔드 → AI 서버
-
"이미지 임베딩 요청"
POST /api/albums/embeddings { "images": [...], "album_id": "abc123" }
- AI 서버 → 백엔드
-
"작업 잘 받았음" (즉시 응답)
202 Accepted { "message": "embedding task received", "task_id": "xyz789" }
-
이 응답은 임베딩이 완료됐다는 뜻이 아님
-
"큐에 등록은 완료됐고, 실제 처리는 나중에 할게" 라는 의미
- AI 서버 내부
- 큐에 쌓인 임베딩 태스크가 처리됨
- 이미지 불러오기
- CLIP 임베딩 생성
- 캐싱 or 저장
- AI 서버 → 백엔드
-
"임베딩 완료 알림"
POST /notify/embedding_done { "album_id": "abc123", "status": "success" }
- 백엔드 → AI 서버
-
후속 작업 요청 (중복/분류/하이라이트 등)
POST /api/albums/dedup POST /api/albums/category POST /api/albums/highlight ...
마. 핵심 포인트
단계 | 누가 | 무엇을 | 왜 |
---|---|---|---|
1 | 백엔드 | AI 서버에 임베딩 요청 | 유저 요청에 따른 작업 시작 |
2 | AI 서버 | 요청 잘 받았다고 응답 | 큐에 넣고 바로 응답 (서버 폭주 방지) |
3 | AI 서버 | 임베딩 실행 (워커에서) | 실제 태스크 수행 |
4 | AI 서버 | 백엔드에 완료 알림 | 후속 작업 시작 트리거 |
5 | 백엔드 | 후속 작업 요청 | 유저에게 결과 제공 or 더 많은 처리 수행 |
c. 요약 및 결론
가. 요약
항목 | 메시지 큐 없음 | 메시지 큐 있음 |
---|---|---|
응답 시점 | 임베딩 끝난 뒤 | 큐에 넣자마자 |
백엔드 인식 | 요청-응답 = 완료 | 응답 = 수신, 완료는 나중에 알림 |
문제 | 응답 지연 = 실패로 오해 | ✅ 응답은 빠르게, 완료는 콜백으로 |
나. 결론
- 현재 MVP 배포를 앞둔 상황에서 메시지 큐를 도입하기에는 바꿔야할 것이 많다.
- 단기적으로 대응할만한 대안이 필요
2. 메시지 큐 없이 임베딩 비동기 요청을 안전하게 처리하기 위한 대응 전략
a. 요청 동시 처리 제한 (rate limiting / throttling)
-
FastAPI + Uvicorn 구조라면 동시 요청 수 제한이나 큐처럼 순차 실행이 가능함
-
예: 내부
asyncio.Semaphore
또는asyncio.Queue
를 사용해 요청을 직렬화semaphore = asyncio.Semaphore(2) # 동시에 2개만 처리 @app.post("/api/albums/embeddings") async def embed_endpoint(data: ImageList): async with semaphore: return await process_embedding(data)
-
장점 : 서버 과부하 방지
-
단점 : 느린 요청은 대기 → 사용자 입장에선 응답 지연됨
BackgroundTasks
)
b. 백그라운드 태스크로 오프로드 (-
FastAPI의
BackgroundTasks
를 사용하면, 응답은 바로 주고 실제 작업은 백그라운드에서 실행 가능from fastapi import BackgroundTasks @app.post("/api/albums/embeddings") async def embed_endpoint(data: ImageList, background_tasks: BackgroundTasks): background_tasks.add_task(process_embedding, data) return {"message": "embedding started"}
-
장점 : 응답을 빠르게 할 수 있다.
-
단점 : 백엔드는 여전히 “완료됐는지”는 모름 → 후속 요청은 클라이언트 타이머로 기다려야 함
c. 작업 큐 직접 구현 (메모리 기반 큐)
-
메시지 큐를 도입하긴 어렵지만 간단한 in-memory 작업 큐는 구현 가능
import queue from threading import Thread task_queue = queue.Queue() def worker(): while True: task = task_queue.get() do_embedding(task) task_queue.task_done() Thread(target=worker, daemon=True).start() @app.post("/api/albums/embeddings") def enqueue_task(data: ImageList): task_queue.put(data) return {"message": "queued"}
-
장점 : 메시지 큐 없이도 순차 처리 가능
-
단점 : 프로세스가 죽으면 큐도 날아감 → 영속성 없음
d. 서버 차단 방지 설정들 (infra-level)
uvicorn
에 worker 수 조절 (-workers 1~2
)- 이미지 임베딩 inference가 무거우면, 임베딩 서버만 따로 분리하거나 배치 처리 주기 설정
- 필요 시 모델 로딩 캐싱, CPU/메모리 제한 등 리소스 절약 옵션도 병행
3. 현실적인 MVP 대응 정리
방식 | 특징 | 추천 여부 |
---|---|---|
asyncio.Semaphore |
동시 처리 제한 | ✅ 간단하고 효과적 |
BackgroundTasks |
빠른 응답 + 비동기 처리 | ✅ 적절 |
queue.Queue + 쓰레드 |
간단한 직렬 큐 | ✅ 중간 규모까지 |
메시지 큐 (Celery 등) | 강력하지만 구조 변화 필요 | ❌ 지금은 보류 |
a. 핵심 목표
- CPU 바운드 작업으로 인해 FastAPI 서버 전체가 응답하지 못하는 상황 방지
- 한 번에 여러 작업 요청이 들어오더라도 안정적이고 예측 가능한 처리 보장
- 향후 메시지 큐 도입 시에도 쉽게 이식 가능한 구조 설계
b. 기본 전략
구성 요소 | 적용 방식 | 역할 |
---|---|---|
run_in_executor() |
전 작업 공통 적용 | CPU-heavy 작업을 이벤트 루프에서 분리하여 실행 |
semaphore |
embedding 작업에 세마포어 4 적용(max_workers = 8) | 시간이 오래 걸리는 embedding 작업이 전체 스레드를 점유하는 상황을 방지 |
- 왜 run_in_executor()를 모든 작업에 적용하는가?
- FastAPI의
async def
엔드포인트 내에서 CPU-heavy 작업을 직접 실행하면 이벤트 루프가 막힘 - 이벤트 루프가 막히면 다른 요청을 받아도 응답하지 못하게 되어 병렬 처리 불가
run_in_executor()
는 이를 스레드풀로 분리하여 비동기적으로 실행하게 하므로- 서버 반응성 유지
- 동시 요청을 병렬로 처리 가능
- 작업이 끝나도 스레드가 사라지지 않고 재사용 되기 때문에 가용성이 좋음
- FastAPI의