ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(Eco²) Message Queue #11: Celery Prefork 병목 지점 (Concurrency, IO Bound - LLM
    이코에코(Eco²)/Message Queue 2025. 12. 24. 16:31

    https://snapshots.raintank.io/dashboard/snapshot/9JSNyj25kwGN55i0Rca176dBEcwUksNt

     

    Grafana

    If you're seeing this Grafana has failed to load its application files 1. This could be caused by your reverse proxy settings. 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy m

    snapshots.raintank.io

     
    본 문서는 Celery Pool을 Gevent로 전환하기 전, 현재 시스템의 성능 지표를 기록합니다.
    Prometheus에서 수집한 실제 메트릭을 기반으로 작성되었습니다.


    1. 측정 환경

    측정 시간 2025-12-24 15:26 KST
    누적 요청 수 126 requests
    LLM Provider OpenAI GPT-5.1
    OpenAI Tier Tier 1 (500 RPM, 500,000 TPM)
    Celery Pool prefork
    scan-worker replicas 3
    scan-worker concurrency 8 (설정값)
    Locust 100 Users, 10 ramp-ups

    2. 핵심 지표 (Prometheus 실측)

    Chain Avg Duration 41.65초 전체 파이프라인 평균
    TTFB (p50) 10.00초 첫 SSE 이벤트까지
    TTFB (p99) 10.00초 tail latency 안정적
    Requests/sec 0.0323 req/s 1시간 평균
    RPM 1.94 RPM 분당 요청
    Success Rate 100% 126/126 성공
    Active Connections 0 측정 시점 유휴

    3. 스테이지별 성능 분석 (Prometheus 실측)

    3.1 평균 소요 시간

    vision 12.25초 GPT-5.1 Vision API
    rule 8.40초 Rule-based-retrieval 규칙 매칭
    answer 14.13초 GPT-5.1 Chat Completion
    reward 5.38초 character.match 동기 호출
    합계 ~40.16초 Chain 평균 41.65초와 유사

    3.2 p99 소요 시간 (Tail Latency)

    vision 37.91초 OpenAI API 지연 시 급증
    rule 49.75초 Worker 동시성 병목 Worst case
    answer 46.63초 OpenAI API 지연 영향
    reward 15.11초 match worker 대기

    3.3 스테이지 소요 시간 비율 (100 users, 10 ramp-ups)

    ┌────────────────────────────────────────────────────────────┐
    │                    Stage 소요 시간 비율                      │
    ├────────────────────────────────────────────────────────────┤
    │                                                            │
    │  answer ████████████████████████████████████  14.13초 35%  │
    │  vision ██████████████████████████████        12.25초 30%  │
    │  rule   █████████████████████                  8.40초 21%  │
    │  reward █████████████                          5.38초 14%  │
    │                                                            │
    │  총 소요 시간: ~40.16초 (Chain 평균: 41.65초)               │
    │                                                            │
    └────────────────────────────────────────────────────────────┘

    분석:

    • answer (GPT Chat Completion)이 가장 긴 소요 시간 (35%)
    • vision (GPT Vision)이 두 번째 (30%)
    • OpenAI API 호출이 전체의 65% 차지

    4. 🔴 prefork가 효과 없는 이유

    4.1 GIL과 prefork의 관계

    ┌─────────────────────────────────────────────────────────────┐
    │              GIL과 Celery Pool의 관계                        │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  GIL (Global Interpreter Lock):                             │
    │  • 한 번에 하나의 스레드만 Python 바이트코드 실행           │
    │  • CPU-bound 작업에서 멀티스레딩 효과 없음                  │
    │  • ⚠️ 단, I/O 대기 중에는 GIL이 해제됨                      │
    │                                                             │
    │  prefork Pool의 설계 의도:                                  │
    │  • 각 Worker가 독립 프로세스 → 독립 GIL                     │
    │  • CPU-bound 작업의 진정한 병렬 처리                        │
    │  • Python GIL 우회                                          │
    │                                                             │
    │  ⚠️ 문제: 이코에코 워크로드는 CPU-bound가 아님!             │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    4.2 워크로드 분석: CPU-bound vs I/O-bound

    ┌─────────────────────────────────────────────────────────────┐
    │              이코에코 워크로드 유형                          │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ✅ 실제 (I/O-bound):                                        │
    │  ─────────────────────                                      │
    │  • scan.vision → OpenAI Vision API 호출 (I/O 대기)            │
    │  • scan.rule   → Rule-based-retrieval 규칙 매칭 (I/O 대기)     │
    │  • scan.answer → OpenAI Chat API 호출 (I/O 대기)              │
    │  • scan.reward → 로컬 캐시 조회                                │
    │                                                             │
    │  실측 결과:                                                   │
    │  ┌────────────────────────────────────────────────────┐     │
    │  │  OpenAI API (vision + answer): 65% ← I/O 대기!     │      │
    │  │                                                   │      │
    │  │  ⚠️ I/O-bound 워크로드가 주요소                        │      │
    │  └────────────────────────────────────────────────────┘     │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    4.3 왜 prefork가 효과 없는가?

    ┌─────────────────────────────────────────────────────────────┐
    │              prefork + I/O-bound = 비효율                    │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  이론: prefork는 GIL을 우회한다                                  │
    │  ────────────────────────────────                           │
    │  • 각 Worker가 독립 프로세스                                     │
    │  • 프로세스마다 독립된 GIL                                       │
    │  • CPU-bound 작업의 진정한 병렬 처리 가능                          │
    │                                                             │
    │  현실: I/O-bound에서는 GIL이 이미 해제됨                          │
    │  ────────────────────────────────────────                   │
    │  • I/O 대기 시작 → GIL 자동 해제                                │
    │  • 다른 스레드/코루틴 실행 가능                                   │
    │  • ⚠️ 2vCPU인 t3.medium에서 가용 프로세스 수가 한정적              │
    |  • ⚠️ prefork의 "독립 GIL" 장점이 무의미                        │     
    │                                                            │
    │  문제점:                                                     │
    │  ┌────────────────────────────────────────────────────┐    │
    │  │                                                    │    │
    │  │  prefork Worker의 동작:                              │    │
    │  │                                                    │    │
    │  │  Task 시작 ──▶ OpenAI API 호출 ──▶ 41초 대기           │    │
    │  │       ↑              ↓                             │    │
    │  │       │         [I/O 대기 중]                        │    │
    │  │       │              ↓                             │    │
    │  │       │         Worker는 블로킹됨!                    │    │
    │  │       │         다른 Task 처리 불가                   │    │
    │  │       │              ↓                             │    │
    │  │       └────── 응답 수신 ──▶ Task 완료                 │    │
    │  │                                                    │    │
    │  │  ⚠️ 1 Worker = 1 Task => Queue 과적재                │    │
    │  │  ⚠️ I/O 대기 중에도 Worker가 해제되지 않음                │    │
    │  │                                                    │    │
    │  └────────────────────────────────────────────────────┘    │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    4.4 Gevent와의 비교

    ┌─────────────────────────────────────────────────────────────┐
    │              prefork vs gevent 동작 비교                     │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  prefork (현재):                                            │
    │  ─────────────────                                          │
    │  Worker 1: [Task A ████████████████████████████] 41초 블로킹│
    │  Worker 2: ────────[Task B ████████████████████████] 41초   │
    │  Worker 3: ──────────────────[Task C ██████████████] 41초   │
    │                                                             │
    │  → 3 workers × 41초 = 0.07 RPS (이론)                       │
    │  → 실측: 0.0323 RPS                                         │
    │                                                             │
    │  ───────────────────────────────────────────────────────    │
    │                                                             │
    │  gevent (목표):                                             │
    │  ─────────────────                                          │
    │  Greenlet Pool: [A 시작][B 시작][C 시작]...[A 완료][B 완료] │
    │                  ↓       ↓       ↓         ↓       ↓       │
    │              I/O 대기  I/O 대기 I/O 대기  return  return   │
    │                  ↓       ↓       ↓         ↑       ↑       │
    │              [I/O 대기 중 다른 Greenlet 자동 전환]          │
    │                                                             │
    │  → 100+ 동시 I/O 가능                                       │
    │  → OpenAI Rate Limit까지 확장 가능 (~4 RPS)                 │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    5. 성능 분석

    5.1 현재 처리량

    실측 RPM 1.94 RPM Prometheus rate(1h)
    실측 RPS 0.0323 req/s RPM / 60
    Chain 평균 41.65초 avg(sum/count)

    5.2 이론적 최대 처리량

    OpenAI Tier 1 500 RPM / 2 calls 4.17 chains/sec
    prefork workers 9 workers / 41.65초 0.22 RPS
    실질 한계 min(4.17, 0.22) ~0.22 RPS

    5.3 병목 분석

    ┌─────────────────────────────────────────────────────────────┐
    │                    병목 분석                                  │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  🔴 주요 병목: prefork Pool 동기 블로킹                       │
    │                                                             │
    │  1 request → 1 worker → 41.65초 블로킹                      │
    │                                                             │
    │  ┌──────────────────────────────────────────┐               │
    │  │ Worker 1: ████████████████████████████ 41s              │
    │  │ Worker 2:              ████████████████████████ 41s     │
    │  │ Worker 3:                           █████████████ 41s   │
    │  │           ↑                         ↑                   │
    │  │           t=0                       t=41s               │
    │  └──────────────────────────────────────────┘               │
    │                                                             │
    │  9 workers × 41.65초 = 0.22 RPS (이론 최대)                 │
    │  실측: 0.0323 RPS (이론의 15%)                              │
    │                                                             │
    │  📉 효율 손실 원인:                                         │
    │  • Task 시작/종료 오버헤드                                  │
    │  • Worker 간 불균형 분배                                    │
    │  • RabbitMQ 메시지 전달 지연                                │
    │  • Celery Chain 오버헤드                                    │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    6. 시스템 제약 분석

    6.1 물리적 제약

    노드 메모리 4GB ~3.5GB 가용 t3.medium
    scan-worker replicas 3 3 정상
    concurrency 8 ~6-9 (메모리 제한) OOM 위험
    총 workers 24 (설정) 6-9 (실제) 메모리 병목

    6.2 OpenAI Rate Limit

    현재 (Tier 1) 500 500,000 4.17 (vision+answer 2회)
    Tier 2 2,000 2,000,000 16.67
    Tier 3 5,000 5,000,000 41.67

    6.3 prefork의 구조적 한계

    ┌─────────────────────────────────────────────────────────────┐
    │              prefork Pool 구조적 한계                        │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  1. 메모리 비효율                                           │
    │  ─────────────────                                          │
    │  • 각 Worker가 독립 프로세스                                │
    │  • Python 인터프리터 복제 (수십~수백 MB/프로세스)           │
    │  • 24 workers × ~150MB = 3.6GB+ 메모리 필요                 │
    │  • t3.medium (4GB)에서 OOM 발생                             │
    │                                                             │
    │  2. 동시성 한계                                             │
    │  ─────────────                                              │
    │  • 1 Worker = 1 Task (동시 처리 불가)                       │
    │  • I/O 대기 중에도 Worker 점유                              │
    │  • 실제 가용 Workers: 6-9개 (메모리 제한)                   │
    │  • 최대 동시 처리: 6-9 requests                             │
    │                                                             │
    │  3. 확장 비용                                               │
    │  ──────────────                                             │
    │  • Worker 추가 = 메모리 증가                                │
    │  • 100 동시 처리 = 100 프로세스 = 15GB+ 메모리              │
    │  • 비현실적인 리소스 요구                                   │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    7. 개선 방안

    7.1 ⚠️ Celery는 AsyncIO Pool을 공식 지원하지 않음

    ┌─────────────────────────────────────────────────────────────┐
    │              Celery AsyncIO Pool 지원 현황                   │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  Celery 5.6.0 (2025-11-30, Latest):                         │
    │  ────────────────────────────────────                       │
    │  • ❌ 네이티브 asyncio pool 미포함                              │
    │  • 공식 Pool: prefork, threads, gevent, eventlet             │
    │                                                             │
    │  외부 패키지 시도 결과:                                          │
    │  ──────────────────────                                     │
    │  • celery-pool-asyncio → ❌ Celery 5.4+ 비호환, 업데이트 중단    │
    │    (AttributeError: trace.monotonic)                        │
    │  • celery-aio-pool → ❌ stars 75 커뮤니티 기반, 미검증            │
    │    (Python 3.8-3.10만 지원)                                   │
    │                                                             │
    │  결론:                                                       │
    │  ┌────────────────────────────────────────────────────┐    │
    │  │  Celery에서 async def Task를 직접 사용할 수 없음    │    │
    │  │  → Gevent Pool이 I/O-bound에 가장 적합한 대안       │    │
    │  └────────────────────────────────────────────────────┘    │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    7.2 Gevent Pool 전환 (권장)

    동시 I/O 6-9 100+
    메모리 3.6GB+ ~500MB
    블로킹 100% (41초/req) 자동 yield
    예상 RPS 0.22 OpenAI 한계(~4 RPS)까지
    # 변경 전
    args: [-P, prefork, -c, '8']
    
    # 변경 후
    args: [-P, gevent, -c, '100']

    7.3 예상 개선 효과

    RPS 0.0323 ~4 RPS (OpenAI 한계)
    RPM 1.94 ~240 RPM
    동시 처리 6-9 100+
    메모리 3.6GB ~500MB

    7.4 왜 Gevent가 해결책인가?

    ┌─────────────────────────────────────────────────────────────┐
    │              Gevent가 I/O-bound에 최적인 이유                │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  1. Monkey Patching으로 자동 비동기화                       │
    │  ─────────────────────────────────                          │
    │  • 표준 라이브러리의 블로킹 I/O를 자동 전환                 │
    │  • 기존 동기 코드 수정 없이 사용 가능                       │
    │  • socket, time.sleep, select 등 자동 패치                  │
    │                                                             │
    │  2. 협력적 멀티태스킹 (Greenlet)                            │
    │  ─────────────────────────────                              │
    │  • I/O 대기 시 자동으로 다른 Greenlet으로 전환              │
    │  • 단일 프로세스로 수천 동시 작업 처리                      │
    │  • 메모리 효율적 (Greenlet = 수 KB)                         │
    │                                                             │
    │  3. Celery 공식 지원                                        │
    │  ────────────────────                                       │
    │  • Celery 5.6.0에서 공식 지원되는 Pool                      │
    │  • 안정성 검증됨                                            │
    │  • 외부 패키지 의존 없음                                    │
    │                                                             │
    │  동작 예시:                                                 │
    │  ┌────────────────────────────────────────────────────┐    │
    │  │  @celery_app.task                                  │    │
    │  │  def vision_task(image_url):                       │    │
    │  │      # 기존 동기 코드 그대로 사용                  │    │
    │  │      result = openai.vision(image_url)             │    │
    │  │      # ↑ gevent가 I/O 대기 시 자동 yield           │    │
    │  │      #   다른 Greenlet 실행 후 복귀                │    │
    │  │      return result                                 │    │
    │  │                                                    │    │
    │  │  # 100개 Greenlet 동시 실행                        │    │
    │  │  # 메모리: ~500MB (prefork 대비 85% 절감)          │    │
    │  └────────────────────────────────────────────────────┘    │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    8. 개선안

    1. Gevent Pool 전환 → 100+ 동시 I/O
    2. 기존 동기 코드 유지 → Monkey Patching으로 자동 비동기화
    3. 성능 재측정 → Gevent 전환 후 비교
    4. Gemini 병행 도입 검토 (RPM Limits)

    9. 핵심 교훈

    ┌─────────────────────────────────────────────────────────────┐
    │                    핵심 교훈                                 │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  1. 워크로드 유형 파악이 우선                                     │
    │  ─────────────────────────                                  │
    │  • CPU-bound vs I/O-bound 명확히 구분                          │
    │  • 실측 데이터로 검증 (Prometheus)                              │
    │                                                             │
    │  2. prefork ≠ 만능 해결책                                      │
    │  ─────────────────────────                                  │
    │  • CPU-bound에만 효과적                                        │
    │  • I/O-bound에서는 오히려 비효율                                 │
    │  • 메모리 오버헤드 고려                                          │
    │                                                             │
    │  3. Celery AsyncIO Pool 미지원                                │
    │  ──────────────────────────                                 │
    │  • Celery 5.6.0 (Latest)에서도 미포함                          │
    │  • 외부 패키지(celery-pool-asyncio, celery-aio-pool) 비호환     │
    │  • Gevent가 I/O-bound에 가장 적합한 공식 대안                     │
    │                                                             │
    │  4. 측정 → 분석 → 최적화                                        │
    │  ────────────────────────                                   │
    │  • 가정하지 말고 측정하라                                        │
    │  • 병목이 어디인지 데이터로 확인                                   │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

    10. References


    변경 이력

    2025-12-24 최초 작성 - Gevent 전환 전 현황 기록
    2025-12-24 Prometheus 실측 데이터 기반 수치 업데이트
    2025-12-24 prefork 효과 없는 이유, 한계점 분석 추가 (Foundations 기반)
    2025-12-24 Celery AsyncIO 미지원 명시

     

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango