Zettelkasten

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가지:
    1. 144개 백엔드에 균등 분산
    2. per-backend concurrency = 1 (소켓당 동시 요청 1개)
    3. HAProxy 프론트엔드에서 중앙 큐잉
    4. 소켓/앱서버별 모니터링 가능
  • 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가 튀면 의심해볼 것

참고