트래픽 피크 때 캐시가 동시에 만료되면, 수많은 요청이 한꺼번에 오리진으로 몰리는 캐시 스탬피드(cache stampede) 가 발생합니다. 결과는 간단합니다. QPS 폭증 → 지연 증가 → 오류율 상승 → 연쇄 장애. 이 글은 운영자가 즉시 적용할 수 있는 세 가지 축, 요청 합치기(request coalescing)·서킷브레이커·확률적 만료(early/soft TTL) 를 중심으로 실전 설계·운영 체크리스트를 제공합니다.
1) 왜 캐시 스탬피드가 생기나
- 동시 만료: 동일 키가 같은 시각에 만료되면, 미스 난 모든 워커가 동시에 오리진 조회를 시작.
- 비싼 생성 비용: DB 조인·외부 API 호출·이미지 변환처럼 고비용 연산이 꼬리에 꼬리를 무는 구조.
- 락 부재/부적절한 TTL: 락 없이 “먼저 오는 요청부터 처리”하면 폭주 시 쉽게 붕괴.
핵심은 “만료 시점 동시성”을 제거하고, 오리진 보호 계층을 앞단에 두는 것입니다.
2) 요청 합치기(Request Coalescing): 한 명만 오리진으로
원리: 같은 키에 대한 동시 미스가 나면 오직 1개의 리더 요청만 원천 데이터를 재생성하고, 나머지는 대기 후 동일 결과를 공유합니다.
구현 패턴
- 단일 키 락: SETNX lock:key ttl=5s로 리더를 선출 → 리더만 재생성, 완료 시 값과 TTL을 세팅 후 락 해제.
- 견고한 대기: 팔로워는 짧은 백오프(예: 20–50ms)로 GET 재시도. 타임아웃 시 stale 반환(아래 확률적 만료와 결합).
- 에지·프록시: CDN/역프록시(Varnish, NGINX)에서도 Collapse Forwarding 기능으로 합치기 가능.
체크리스트
- 락 TTL은 생성 평균 시간 + 여유로 잡되 과도하게 길게 두지 말 것
- 리더 실패 시 락 자동 만료가 보장되는지 확인
- 락 경합 메트릭(락 성공률/대기 시간)을 관측 지표에 포함
3) 서킷브레이커: 비정상 구간에서 오리진 차단
오리진이 느려지거나 에러가 급증하면, 합치기로도 폭주를 완전히 상쇄하긴 어렵습니다. 이때는 서킷브레이커로 빠르게 회로를 열어 오리진 호출 자체를 제한합니다.
핵심 규칙
- 상태 3단계: Closed(정상) → Half-Open(탐색) → Open(차단).
- 전환 조건: p95 지연 또는 5xx 비율이 임계치(예: 5%) 초과 N초 연속이면 Open.
- Open 동작: 캐시가 유효하면 캐시만 반환, 없으면 stale + 경고 헤더 또는 폴백 응답.
- Half-Open 샘플링: 1초에 소수의 요청만 오리진으로 보내 정상 회복 여부 확인.
운영 팁
- 브레이커 이벤트를 알람으로 묶어 원인 분석(배포/DB/외부 API)
- 브레이커 해제 후에도 합치기/확률적 만료는 계속 유지
4) 확률적 만료(Soft TTL / Early Expiration): 만료를 분산시키기
만료 시점이 한순간에 몰리지 않게 만드는 기법입니다. 사용자가 보는 TTL(표시 TTL)은 길게 가져가되, 실제 재생성은 확률적으로 조금씩 앞당겨 수행해 동시성의 첨두를 깎습니다.
β-공식(대표적인 방식)
- 캐시 항목에 저장 시각 tₛ, 하드 TTL T, 생성 비용 추정 c를 저장.
- 요청 시 Δ = now - tₛ.
- 다음 조건이면 백그라운드 리프레시(또는 리더가 재생성) 트리거:
random(0,1) < β * exp(Δ / T) 혹은
now > tₛ + T - jitter(0..J) 형태로 조기 갱신.
쉽게 말해, 만료에 가까울수록 조금씩 먼저 갱신을 시도하되, 동시 갱신 확률을 작게 랜덤 분산합니다.
적용 요령
- 가벼운 키엔 단순 지터(jitter)만으로 충분(예: TTL ±10%).
- 무거운 키엔 β 방식 + 요청 합치기 결합.
- stale-while-revalidate: 만료 직후 오래된 값(stale) 을 즉시 반환하고, 백그라운드에서 재검증 → 사용자 체감 지연 최소화.
5) 설계 예시(의사 코드)
포인트: early refresh + 합치기 + 브레이커가 함께 돌아야 폭주를 안정적으로 흡수합니다.
6) 캐시 계층(Varnish/Redis/CDN)별 베스트프랙티스
- CDN/프록시: stale-while-revalidate, stale-if-error 적극 사용. Collapse Forwarding로 합치기.
- 엣지 함수/워커: 키별 락은 간략한 KV 락으로, 조기 갱신은 타이머·큐 사용.
- 애플리케이션 캐시(Redis): SETNX 락 + 만료 지터(±10~20%) + 메타 동시 저장.
- DB 레벨: 고비용 쿼리는 머티리얼라이즈드 뷰나 주기적 프리컴퓨트로 근본 비용을 낮춤.
7) 관측·알람 포인트
- 락 경합률: 동일 키에서 락 충돌이 많다면 TTL·지터를 조정
- stale 반환 비율: 과도하게 높으면 오리진 용량/지연 문제가 누적
- 브레이커 전환 이벤트: Open/Close 히스토리와 원인 상관분석
- 키 상위 N 분석: 오리진 QPS를 가장 많이 유발한 키와 생성 비용 프로파일
8) 운영 체크리스트(요약)
- 키별 TTL에 지터 적용(±10–20%)
- 요청 합치기: 키 락 + 팔로워 대기·재시도
- 서킷브레이커: p95 지연·5xx 임계치 기반, Half-Open 프로빙
- stale-while-revalidate 기본값 켜기
- 무거운 키 별도 분리: 프리컴퓨트·프리캐시
- 관측 대시보드: 락 경합·stale율·브레이커 이벤트·오리진 QPS
9) 자주 묻는 질문(FAQ)
Q. 합치기만 해도 충분한가요?
A. 피크 때 오리진이 느려지면 합치기 리더조차 병목이 됩니다. 서킷브레이커와 확률적 만료를 함께 써야 안전합니다.
Q. stale 반환은 SEO에 문제 없나요?
A. 정적 콘텐츠·이미지·API 응답 등 대부분은 문제 없습니다. 단, 강한 일관성이 필요한 페이지는 짧은 stale 윈도우만 허용하세요.
Q. 지터를 얼마나 줘야 하나요?
A. 트래픽 패턴에 따라 다르지만, 시작은 **±10%**에서, 경합이 크면 **±20%**까지 늘려보세요.
마무리
캐시 스탬피드 방지는 한 가지 요령이 아니라 세 가지 전략의 합입니다.
요청 합치기로 동시성 폭주를 묶고, 서킷브레이커로 비정상 구간을 빠르게 차단하며, 확률적 만료로 만료 시점을 분산하세요. 여기에 stale-while-revalidate까지 더하면, 피크 트래픽에서도 TTFB와 오류율이 안정화됩니다. 오늘 바로 지터부터 적용하고, 상위 키에 합치기·브레이커를 순차 도입해 보세요. 체감이 분명하게 달라집니다.