MVP에서의 요청 과부하 대응 방안 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

스레드 분리(run_in_executor)

[Deprecated] Serial Task Queue 구현

semaphore 설정

1. 메시지 큐 → 적절하지 않음

a. 왜 메시지 큐가 필요한가?

가. 임베딩 작업은 고비용 (시간 + CPU/메모리 많이 씀)

  • 동시에 여러 요청이 들어오면 서버가 과부하되거나 터질 수 있음

나. 이를 방지하려면 비동기 처리 + 큐잉이 필요

  • 메시지 큐를 도입해 요청을 쌓아두고, 워커가 하나씩 순차/병렬로 처리하게 함
  • 서버는 폭주 없이 안정적으로 유지됨

b. 왜 지금 단계에서 도입이 어려운가?

가. 큐 기반 시스템에서는 즉시 응답 + 나중에 콜백 알림이 정석

  • 요청 오면 → "작업 수신"만 응답 (202 Accepted)
  • 나중에 작업 끝나면 → AI 서버가 백엔드에 직접 완료 알림 (POST /notify/embedding_done)
  • 백엔드는 이 알림을 받고 후속 흐름 시작

나. 현재 방식은 "요청 → 처리 → 응답"이 동기 방식

  • 백엔드는 요청을 보내고 임베딩이 끝날 때까지 기다림
  • 응답이 늦어지면 실패 또는 타임아웃으로 간주함

다. 메시지 큐를 도입하면, 처리는 나중에 되므로 응답이 늦어질 수 있음

  • 그대로 두면 백엔드는 “요청 실패”로 오해함

  • 즉, "성공했지만 아직 처리 안됨"이라는 상태를 전달할 방법이 없음

    → 그래서 응답 방식도 바뀌어야 함

라. 메시지 큐 도입 기준 전체 비동기 흐름

  1. 백엔드 → AI 서버
  • "이미지 임베딩 요청"

    POST /api/albums/embeddings
    {
      "images": [...],
      "album_id": "abc123"
    }
    
  1. AI 서버 → 백엔드
  • "작업 잘 받았음" (즉시 응답)

    202 Accepted
    {
      "message": "embedding task received",
      "task_id": "xyz789"
    }
    
  • 이 응답은 임베딩이 완료됐다는 뜻이 아님

  • "큐에 등록은 완료됐고, 실제 처리는 나중에 할게" 라는 의미

  1. AI 서버 내부
  • 큐에 쌓인 임베딩 태스크가 처리됨
    • 이미지 불러오기
    • CLIP 임베딩 생성
    • 캐싱 or 저장
  1. AI 서버 → 백엔드
  • "임베딩 완료 알림"

    POST /notify/embedding_done
    {
      "album_id": "abc123",
      "status": "success"
    }
    
  1. 백엔드 → 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)
    
  • 장점 : 서버 과부하 방지

  • 단점 : 느린 요청은 대기 → 사용자 입장에선 응답 지연됨

b. 백그라운드 태스크로 오프로드 (BackgroundTasks)

  • 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)

  • uvicornworker 수 조절 (-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()는 이를 스레드풀로 분리하여 비동기적으로 실행하게 하므로
      • 서버 반응성 유지
      • 동시 요청을 병렬로 처리 가능
      • 작업이 끝나도 스레드가 사라지지 않고 재사용 되기 때문에 가용성이 좋음