[트러블슈팅] AI 서버 동시 요청 시 GPU OOM
목차
한 줄 요약
동시 요청 제한 없이 AI 서버에 보내다가 하루 5-10회 GPU OOM이 터졌어요.
ThreadPool(max=4) + Semaphore(permits=2)로 이중 동시성 제어를 걸어서 OOM 0회로 잡았어요.
증상
운영 중에 음성 분석 요청이 몰리면 Python AI 서버가 죽었어요.
Pod가 OOMKilled 상태로 재시작되고, 재시작되는 동안 모든 분석 요청이 타임아웃.
하루에 5-10회 반복됐어요.
Grafana의 컨테이너 모니터링 대시보드에서 AI 서버 Pod의 재시작 횟수가 하루 단위로 올라가는 걸 확인했어요.
환경
- Python FastAPI (AI 서버), PyTorch + GPU (8GB VRAM)
- Spring Boot (API 서버), WebClient 비동기 호출
- Docker Compose 단일 서버 구성
원인 분석
GPU 메모리를 계산해봤어요.
GPU 전체 메모리 8GB 중 모델 로딩에 약 3GB가 상시 점유돼요.
추론 1건당 약 2-3GB를 써요.
최대 동시 처리는 (8 - 3) / 2.5 = 약 2건이에요.
그런데 Kafka Consumer에서 이벤트가 들어오는 대로 AI 서버에 요청을 쏘고 있었어요.
동시에 5건만 들어오면 필요 메모리가 VRAM을 초과하니 OOM이 나는 게 당연했어요.
AI 분석 담당 팀원과 같이 nvidia-smi로 GPU 메모리 사용량을 모니터링하면서 동시 요청 수와 OOM 발생 시점을 대조했어요.
동시 3건까지는 안정적이고, 4건부터 가끔 스파이크가 나고, 5건 이상이면 거의 확실하게 OOM이 터졌어요.
해결: ThreadPool + Semaphore 이중 제어
단순히 동시 요청을 줄이면 되는 문제가 아니었어요.
WAV 변환과 AI 분석이 같은 Consumer에서 처리되는데, WAV 변환은 CPU 바운드라 빠르게 끝나고 AI 분석은 GPU를 오래 점유하거든요.
같은 스레드풀로 처리하면 AI 분석이 WAV 변환까지 블로킹하거든요.
그래서 두 가지를 분리했어요.
ThreadPool(max=4): 시스템 내부 리소스(CPU, 메모리) 보호.
최대 4개 스레드까지 작업을 처리해요.
Semaphore(permits=2): 외부 서비스(AI 서버 GPU) 보호.
4개 스레드가 동시에 실행되더라도 AI 서버에는 2개만 동시 요청해요.
permits를 2로 잡은 이유는, GPU 계산상 최대 동시 처리가 약 2건이고, 요청마다 메모리 사용량이 다르기 때문에(음성 길이, 복잡도에 따라 편차) 안전 마진을 포함한 수치예요.
검토했지만 선택하지 않은 대안
| 방식 | 장점 | 단점 | 판단 |
|---|---|---|---|
Kafka max.poll.records 조절 | 설정만 변경 | Consumer 레벨 제한이라 GPU 메모리와 직접 연동 안 됨. WAV 변환까지 같이 제한됨 | 탈락 |
| Resilience4j RateLimiter | 시간 기반 요청률 제한 | AI 분석은 시간당 N건이 아니라 동시 N건이 문제. 처리 시간이 가변적이라 rate 기반은 부적합 | 탈락 |
| ThreadPool만 (Semaphore 없이) | 단순 | WAV 변환과 AI 분석이 같은 풀이면 AI가 WAV를 블로킹. 풀을 분리해도 AI 전용 풀 max=2로 하면 Semaphore와 사실상 동일 | 부분 채택 |
| ThreadPool + Semaphore | 내부(CPU) + 외부(GPU) 자원을 독립 제어 | 구현 약간 복잡 | 선택 |
ThreadPool만으로도 가능하지만, Semaphore로 “외부 GPU 자원에 대한 동시 접근 제한”을 명시적으로 분리한 게 Bulkhead 패턴의 의도를 더 잘 표현해요.
Semaphore Bean 설정
ThreadPool 설정
Semaphore 사용 코드
작업별 리소스 분리
| 작업 타입 | ThreadPool | Semaphore | 이유 |
|---|---|---|---|
| WAV 변환 | 5~10 | 8 | CPU 바운드, 빠른 처리 |
| 음성 분석 | 2~4 | 2 | GPU 사용, 무거운 AI 처리 |
| 이미지 처리 | 3~6 | 4 | 중간 수준 |
| 배치 복구 | 2~4 | 3 | 백그라운드 처리 |
무거운 작업(AI 분석)이 가벼운 작업(WAV 변환)을 블로킹하지 않도록 풀을 분리한 게 핵심이에요.
결과
| 지표 | 개선 전 | 개선 후 |
|---|---|---|
| AI 서버 OOM 발생 | 하루 5-10회 | 0회 |
| 평균 분석 대기 시간 | 실패로 무한 대기 | 30초 |
| GPU 활용률 | 불안정 (100% 스파이크) | 85% 안정 |
| 분석 성공률 | ~70% | 99%+ |
요청이 폭주해도 세마포어 대기열에서 순차 처리되니 Pod 재시작이 0회가 됐어요.
참고 자료
Summary
Eliminated daily 5-10 GPU OOM crashes by implementing dual concurrency control: ThreadPool (max=4) for system resources and Semaphore (permits=2) for GPU protection, achieving zero OOM incidents.
Symptoms
During operation, the Python AI server crashed when voice analysis requests spiked. Pods restarted as OOMKilled, causing all analysis requests to timeout during restart. This repeated 5-10 times daily. Grafana’s container monitoring dashboard showed the AI server Pod restart count climbing daily.
Environment
- Python FastAPI (AI server), PyTorch + GPU (8GB VRAM)
- Spring Boot (API server), WebClient async calls
- Docker Compose single-server setup
Root Cause
GPU memory calculation: 8GB total, ~3GB constant for model loading, ~2-3GB per inference. Maximum concurrent processing: (8 - 3) / 2.5 ≈ 2.
But the Kafka Consumer was firing requests to the AI server as fast as events arrived. 5 concurrent requests exceed available VRAM — OOM was inevitable.
Monitoring GPU memory with nvidia-smi alongside the AI team, we correlated concurrent request counts with OOM timing. 3 concurrent was stable, 4 showed occasional spikes, 5+ almost guaranteed OOM.
Solution: ThreadPool + Semaphore Dual Control
Simply reducing concurrent requests wasn’t enough. WAV conversion and AI analysis share the same Consumer, but WAV conversion is CPU-bound (fast) while AI analysis occupies the GPU long. A shared thread pool lets AI analysis block WAV conversion.
Two mechanisms were separated:
ThreadPool (max=4): Protects internal system resources (CPU, memory). Up to 4 threads process tasks.
Semaphore (permits=2): Protects external service (AI server GPU). Even with 4 threads running, only 2 can simultaneously request the AI server.
Permits were set to 2, matching the GPU’s theoretical max concurrent capacity of ~2, with per-request memory varying by audio length and complexity.
Semaphore Bean Config
ThreadPool Config
Semaphore Usage Code
Per-Task Resource Isolation
| Task Type | ThreadPool | Semaphore | Reason |
|---|---|---|---|
| WAV Conversion | 5~10 | 8 | CPU-bound, fast |
| Voice Analysis | 2~4 | 2 | GPU-heavy AI processing |
| Image Processing | 3~6 | 4 | Medium workload |
| Batch Recovery | 2~4 | 3 | Background processing |
The key is separating pools so heavy tasks (AI analysis) don’t block light tasks (WAV conversion).
Results
| Metric | Before | After |
|---|---|---|
| AI server OOM | 5-10/day | 0 |
| Avg analysis wait | Infinite (failures) | 30s |
| GPU utilization | Unstable (100% spikes) | 85% stable |
| Analysis success rate | ~70% | 99%+ |
Even under request surges, sequential processing via semaphore queue reduced Pod restarts to zero.
References
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.