본문 바로가기

카테고리 없음

캐시 스탬피드 방지: 요청 합치기·서킷브레이커·확률적 만료

트래픽 피크 때 캐시가 동시에 만료되면, 수많은 요청이 한꺼번에 오리진으로 몰리는 캐시 스탬피드(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) 설계 예시(의사 코드)

 
function getWithShield(key): val = cache.get(key) meta = cache.meta(key) // ts, ttl, cost, version if val && !shouldEarlyRefresh(meta): return val // 정상 반환 // 합치기: 리더 선출 if lock.try(key, 5s): // 리더만 오리진 호출 fresh = origin.fetch(key) cache.set(key, fresh, ttl=T, meta=now) lock.release(key) return fresh else: // 팔로워: 짧은 대기 후 캐시 재확인 wait(30ms) val2 = cache.get(key) if val2: return val2 // 서킷브레이커 확인 if circuit.isOpen(): return cache.getStale(key) or fallback() // Half-Open이면 소수만 통과 if circuit.allowProbe(): return origin.fetch(key) else: return cache.getStale(key) or fallback()

포인트: 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) 운영 체크리스트(요약)

  1. 키별 TTL에 지터 적용(±10–20%)
  2. 요청 합치기: 키 락 + 팔로워 대기·재시도
  3. 서킷브레이커: p95 지연·5xx 임계치 기반, Half-Open 프로빙
  4. stale-while-revalidate 기본값 켜기
  5. 무거운 키 별도 분리: 프리컴퓨트·프리캐시
  6. 관측 대시보드: 락 경합·stale율·브레이커 이벤트·오리진 QPS

9) 자주 묻는 질문(FAQ)

Q. 합치기만 해도 충분한가요?
A. 피크 때 오리진이 느려지면 합치기 리더조차 병목이 됩니다. 서킷브레이커와 확률적 만료를 함께 써야 안전합니다.

Q. stale 반환은 SEO에 문제 없나요?
A. 정적 콘텐츠·이미지·API 응답 등 대부분은 문제 없습니다. 단, 강한 일관성이 필요한 페이지는 짧은 stale 윈도우만 허용하세요.

Q. 지터를 얼마나 줘야 하나요?
A. 트래픽 패턴에 따라 다르지만, 시작은 **±10%**에서, 경합이 크면 **±20%**까지 늘려보세요.

마무리

캐시 스탬피드 방지는 한 가지 요령이 아니라 세 가지 전략의 합입니다.
요청 합치기로 동시성 폭주를 묶고, 서킷브레이커로 비정상 구간을 빠르게 차단하며, 확률적 만료로 만료 시점을 분산하세요. 여기에 stale-while-revalidate까지 더하면, 피크 트래픽에서도 TTFB와 오류율이 안정화됩니다. 오늘 바로 지터부터 적용하고, 상위 키에 합치기·브레이커를 순차 도입해 보세요. 체감이 분명하게 달라집니다.