본문으로 건너뛰기
수천 마리의 소떼가 동시에 달려드는 모습

# Thundering Herd: 10,000개 스레드가 동시에 깨어날 때 서버가 멈추는 이유

Table of Contents

잠자는 사자의 코털을 동시에 건드린다면?

서버 개발자라면 **“컨텍스트 스위칭(Context Switch) 비용이 비싸다”**는 말을 한 번쯤 들어보셨을 겁니다. Thundering Herd(천둥처럼 달려드는 짐승 떼) 문제는 바로 이 비용을 극대화하여 멀쩡하던 서버를 순식간에 마비시키는 최악의 패턴 중 하나입니다.

상황을 한 번 상상해 봅시다.

  1. 하나의 먹이(이벤트)를 기다리는 하이에나(스레드/프로세스) 10,000마리가 잠자고 있습니다. (wait 상태)
  2. 먹이 하나가 바닥에 툭 떨어집니다. (이벤트 발생)
  3. 10,000마리가 그 소리를 듣고 동시에 눈을 뜨며(Wake up) 먹이를 향해 전력 질주합니다.
  4. 가장 빠른 한 마리가 먹이를 채갑니다. (이벤트 처리)
  5. 나머지 9,999마리는? “아, 늦었네” 하고 허탈해하며 다시 잠을 자러 갑니다. (다시 wait)

이 짧은 순간 발생한 10,000번의 기상과 9,999번의 헛걸음. 이것이 바로 CPU를 낭비하고 시스템 전체 성능을 급락시키는 Thundering Herd 현상입니다.

어디서 주로 발생할까요?

1. Accept Queue (고전적인 문제)

이는 웹 서버(Nginx, Apache) 초창기 시절의 고질병이었습니다. 여러 워커 프로세스가 하나의 소켓(listen fd)을 공유하고 accept()를 호출하며 대기합니다. 새로운 연결 요청이 하나 들어오면, 커널이 대기 중이던 모든 워커를 다 깨워버리는 방식이었죠. 다행히 현대의 리눅스 커널(2.6 이상)은 EPOLLEXCLUSIVE 플래그 등을 도입하여 “이벤트가 발생해도 하나의 프로세스만 깨우는” 기능을 제공함으로써 이 문제를 OS 레벨에서 해결했습니다.

2. Cache Stampede (현대적인 문제)

오늘날 MSA(Microservices Architecture) 환경에서 더 자주 겪게 되는 문제입니다. 인기 있는 게시글의 캐시가 만료되는 순간(TTL Expire), 그 찰나에 수천 개의 요청이 동시에 들어옵니다. 모든 요청이 “어? 캐시가 없네? DB에서 읽어와야지”라고 판단합니다. 결국 수천 개의 스레드가 동시에 데이터베이스로 돌진합니다. 결과는? DB 서버의 CPU가 100%를 치고 사망합니다.

해결책: “줄을 서시오”

이 문제를 해결하는 핵심은 **“동시성(Concurrency)을 제어”**하여 불필요한 경쟁을 없애는 것입니다.

1. 지터(Jitter) 추가하기

모두가 똑같은 시간에 깨어나거나 똑같은 시간에 캐시가 만료되지 않도록, 시간에 랜덤 값을 섞는 것입니다. 예를 들어, 캐시 만료 시간을 TTL + Random(0~10s)로 설정하면, 수천 개의 캐시 키가 동시에 만료되어 모든 서버가 동시에 DB를 타격하는 상황을 막을 수 있습니다.

2. 한 놈만 보낸다 (Mutex Lock / Promise)

캐시가 없음을 감지했을 때, 가장 먼저 도착한 요청 하나만 “내가 DB 갔다 올게, 너희는 기다려”라고 깃발(Lock)을 듭니다. 나머지 후속 요청들은 그 깃발을 보고 “아, 누가 가지러 갔구나” 하고 잠시 대기하거나(Spin Lock), 그 요청이 가져온 결과(Promise/Future)를 구독하고 기다립니다.

3. 커널 레벨의 지원 (epoll)

리눅스의 epoll 시스템 콜을 사용할 때 EPOLLEXCLUSIVE 플래그를 사용하면, 이벤트가 발생했을 때 대기 중인 프로세스 중 하나만 깨우도록 커널에 지시할 수 있습니다. Nginx 같은 고성능 서버들은 이미 이 기능을 적극적으로 활용하여 불필요한 컨텍스트 스위칭을 최소화하고 있습니다.

마치며

Thundering Herd는 **“과유불급”**을 시스템적으로 보여주는 대표적인 사례입니다. 이벤트에 빠르게 반응하는 건 중요하지만, 1개의 이벤트를 처리하기 위해 10,000개의 프로세스가 일어나는 건 명백한 자원 낭비입니다.

여러분의 시스템이 이유 없이 CPU 사용량이 튀거나, 특정 캐시 만료 시점마다 DB가 휘청거린다면 의심해 보세요. 혹시 잠자던 짐승 떼가 천둥소리를 내며 의미 없는 달리기를 하고 있지는 않은지 말입니다.

이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트

# Split-Brain 프로덕션 완벽 해결 가이드: 분산 시스템에서 두 개의 리더가 동시에 존재할 때 데이터 충돌 방지하기

게시:

Split-Brain 프로덕션 디버깅 완벽 가이드입니다. NVIDIA AIStore 실제 사례, Quorum 기반 방지, Raft/Paxos Consensus 알고리즘, STONITH Fencing으로 네트워크 파티션 상황에서 데이터 충돌을 방지하는 방법부터 Elasticsearch, Redis Cluster, Kafka 환경까지 실전 예제와 함께 설명합니다.

읽기