Gunicorn 워커 수가 많아지면 thundering herd로 인해 일부 워커만 요청을 받는다
·수정 2026.05.05·수정 1회
요약
- Gunicorn은 워커들이 하나의 listening socket을 공유하는데, 워커가 많아지면 락 경쟁/컨텍스트 스위칭 비용으로 일부 워커만 요청을 처리하고 나머지는 idle 상태가 된다.
- Clubhouse는 96 vCPU + 144 워커 환경에서 CPU 25%에서 latency가 폭증했고, 144개 워커 중 29개만 요청을 받고 있었다.
- 해결: 워커 N개를 가진 Gunicorn 1개 대신 워커 1개를 가진 Gunicorn N개를 HAProxy 뒤에서 로드밸런싱.
본문
Thundering herd 증상
Clubhouse가 트래픽 스케일업 중 마주친 현상:
- 96 vCPU 인스턴스에 Gunicorn 워커 144개 띄움
- CPU 25% 사용률에서 latency 폭증, 노드 불안정
- 진단해보니 144개 워커 중 29개만 요청을 받고 있었음 — 나머지 115개는 놀고 있음
원인
"다수의 프로세스가 같은 소켓에서 다음 요청을 받기 위해 대기"
- pre-fork 모델에서 워커들이 동일한 listening socket의
accept()에서 대기 - 새 요청이 오면 모든 워커가 깨어나 경쟁 → 한 워커만 잡고 나머지는 다시 sleep
- 워커 수가 많을수록 락 경쟁 + 컨텍스트 스위칭 오버헤드가 폭발
- 커널이 깨우는 워커가 편향되면 일부만 계속 일하고 나머지는 idle
시도와 실패
1) uWSGI --thunder-lock
커널 레벨로 부하를 균등 분산. 초기 latency 2배 개선. 하지만 25% CPU 이상 트래픽 스파이크에서 socket lockup이 예측 불가하게 발생 → 요청 거절.
2) uWSGI 인스턴스 10개 + NGINX NGINX에 per-socket concurrency limit / dead socket avoidance 같은 기능이 없어 실패.
해결책: Gunicorn N개 + HAProxy
[HAProxy frontend (queue)]
↓
[Gunicorn-1 (worker=1)]
[Gunicorn-2 (worker=1)]
...
[Gunicorn-144 (worker=1)]
- Gunicorn 인스턴스 144개, 각각 워커 1개
- HAProxy가 144개 백엔드로 로드밸런싱
- 핵심 4가지:
- 144개 백엔드에 균등 분산
- per-backend concurrency = 1 (소켓당 동시 요청 1개)
- HAProxy 프론트엔드에서 중앙 큐잉
- 소켓/앱서버별 모니터링 가능
- supervisord로 Gunicorn 인스턴스들 관리
결과
| Before | After | |
|---|---|---|
| CPU 활용 한계 | 30-35% | 80% |
| Latency | baseline | 2x 개선 |
핵심 통찰
- 워커 1개짜리 프로세스 N개 = 워커 N개짜리 프로세스 1개가 아니다.
- 후자는 socket을 공유해서 thundering herd 발생, 전자는 socket이 독립적이라 LB가 정확히 분산 가능.
- 외부 로드밸런서(HAProxy)가 요청 큐잉 + 소켓당 동시성 1을 강제할 수 있을 때 의미가 있다.
- "Python 멀티프로세스 모델은 제대로 분산만 하면 합리적이다"
적용 기준
- 코어 수 적은 일반 인스턴스(소수 워커)에서는 thundering herd 영향 미미 → 굳이 필요 없음
- 수십~수백 개 워커를 굴리는 대형 인스턴스에서 CPU는 남는데 latency가 튀면 의심해볼 것