Thundering Herd 문제란?
Thundering Herd(썬더링 허드)는 ‘많은 프로세스나 스레드가 동시에 하나의 자원이나 이벤트를 기다리다가, 그 이벤트가 발생하는 순간 한꺼번에 깨어나 자원 경쟁을 벌이는 현상’을 말합니다. 문제는 그 중 실제로 자원을 얻어 작업을 진행할 수 있는 건 하나뿐인데, 나머지는 헛되이 깨어났다가 다시 잠드는 데 CPU와 스케줄링 비용을 낭비한다는 점입니다.
비유하자면, 문이 하나뿐인 가게 앞에 손님 100명이 줄 서 있는데 문이 열리는 순간 100명이 동시에 몸을 밀고 들어가려는 상황과 비슷합니다. 실제로 들어갈 수 있는 사람은 한 번에 한두 명인데, 나머지 98명은 헛걸음만 하고 다시 줄을 서야 합니다.
어떤 상황에서 발생할까?
대표적인 예시는 다음과 같습니다.
- 서버 소켓 하나에 여러 워커 프로세스가
accept()를 대기하다가, 커넥션 하나가 들어오면 모든 워커가 깨어나는 경우 - 캐시 만료(cache expiration) 시, 동시에 수천 개의 요청이 캐시 미스를 겪고 한꺼번에 DB로 몰려가는 경우(캐시 스탬피드라고도 불림)
- 여러 스레드가 같은
condition variable에서notifyAll()로 동시에 깨어나는 경우
이런 상황에서는 실제로 필요한 처리량보다 훨씬 많은 컨텍스트 스위칭과 락 경합이 발생해 시스템 전체 성능이 떨어집니다. 특히 트래픽이 급증하는 순간 한꺼번에 몰리기 때문에, 평소엔 괜찮다가 특정 이벤트 이후 갑자기 서버 부하가 튀는 형태로 나타나는 경우가 많습니다.
실무에서는 어떻게 막을까?
운영체제와 애플리케이션 레벨에서 다양한 완화 기법이 쓰입니다.
- 단일 알림(wake one): 리눅스 커널은
accept()대기 시 모든 프로세스를 깨우지 않고 하나만 깨우는 방식으로 개선되었습니다(SO_REUSEPORT 등 활용). - 락 기반 직렬화: 캐시 갱신 시 하나의 요청만 DB에 접근하도록 락을 걸고, 나머지 요청은 캐시가 갱신될 때까지 대기시키는 방식(캐시 스탬피드 방지)입니다.
- 지터(Jitter) 추가: 만료 시간이나 재시도 시점에 무작위 지연을 섞어, 여러 요청이 정확히 같은 시각에 몰리지 않도록 분산시킵니다.
- Exponential Backoff: 재연결이나 재시도 로직에서 대기 시간을 점점 늘려가며 재시도해, 동시 재시도가 겹치는 빈도를 낮춥니다.
결국 핵심은 ‘한꺼번에 몰리는 것’을 막는 것이 아니라, 몰리더라도 실제 작업은 소수만 수행하고 나머지는 조용히 대기하도록 설계하는 데 있습니다. 이런 원리를 이해해두면 캐시 설계나 분산 시스템의 재시도 로직을 짤 때 훨씬 안정적인 구조를 만들 수 있습니다.