백엔드 데이터 인프라 보강 — 캐시 스탬피드와 Hot Key

2026-05-26백엔드 데이터 인프라

백엔드 데이터 인프라 보강편. 캐시를 깔았는데도 DB가 무너지는 순간이 있다 — 캐시 스탬피드(동시 만료 폭주)와 Hot Key(한 키 트래픽 쏠림). 둘의 원인과 완화법(뮤텍스·확률적 조기 갱신·TTL 지터·로컬 캐시·키 분산), 타임아웃 정렬까지 정리한 학습 노트.

📚 백엔드 데이터 인프라 · 135편 — 캐시 스탬피드와 Hot Key

이 글은 백엔드 데이터 인프라 시리즈의 보강편이에요. 62편 Redis 캐싱 패턴에서 Look-Aside·Write-Through 같은 패턴과 운영 함정을 훑었죠. 그런데 대용량 트래픽에선 캐시가 오히려 장애를 키우는 순간이 와요. 가장 악명 높은 둘이 캐시 스탬피드(동시 만료 폭주)와 Hot Key(한 키 쏠림)예요. 이 글은 그 둘을 정면으로 풀어요.

캐시가 있는데도 DB가 무너지는 순간

이게 처음엔 직관에 어긋나요. "캐시를 깔면 DB 부하가 줄어든다며? 근데 왜 캐시 때문에 DB가 죽어?" 싶거든요. 시나리오를 보면 바로 이해돼요.

인기 데이터 하나가 캐시에 있고, 초당 수천 요청이 그 캐시를 읽고 있어요. DB는 한가해요. 그런데 그 캐시 키의 TTL이 만료되는 그 순간 — 수천 개의 요청이 동시에 "캐시에 없네?" 를 발견하고, 전부 한꺼번에 DB로 같은 쿼리를 던져요. 한가하던 DB가 순식간에 수천 개 동일 쿼리를 맞고 무너지죠. 이게 캐시 스탬피드(cache stampede), 또는 Thundering Herd(천둥 떼)예요.

비유하면 가게 셔터 같아요. 셔터가 닫혀 있는 동안(캐시 유효) 손님이 밖에 쌓여 있다가, 셔터가 올라가는(만료) 순간 다 같이 우르르 밀고 들어와 입구가 깨지는 거예요.

캐시 스탬피드를 막는 세 가지

핵심은 "동시에 만료 → 동시에 DB" 의 연쇄를 끊는 거예요.

1. 뮤텍스(mutex) 잠금 — 한 명만 갱신 — 캐시가 비었을 때 모든 요청이 DB로 가는 대신, 딱 하나의 요청만 잠금을 얻어 DB를 조회하고 캐시를 채워요. 나머지는 잠깐 기다렸다가 새로 채워진 캐시를 읽죠. Redis의 SET key value NX(없을 때만 설정)로 이 잠금을 구현해요. 가장 직접적인 해법이에요.

2. 확률적 조기 갱신(probabilistic early recomputation) — 만료를 기다리지 않고, 만료가 가까워지면 일부 요청이 확률적으로 미리 캐시를 갱신해요. 만료 직전일수록 갱신 확률이 올라가는 식이라, 수천 요청이 동시에 만료를 맞기 전에 한둘이 슬쩍 새로 채워 둬요. 트래픽이 끊기지 않는 인기 키에 잘 맞아요.

3. TTL 지터(jitter) — 여러 키의 TTL을 똑같이 주면 한꺼번에 만료돼 떼 폭주가 커져요. TTL에 ±약간의 무작위를 섞어(예: 300초 ± 30초) 만료 시점을 흩뿌리면, 동시 만료가 자연스럽게 분산돼요. 55편 TTL·만료와 함께 보면 좋아요. 가장 싸게 적용할 수 있는 완화책이에요.

🎯 실무 포인트

셋은 배타적이지 않아요. TTL 지터로 동시 만료를 흩고 + 뮤텍스로 그래도 몰린 갱신을 한 명으로 줄이는 조합이 실무에서 흔해요. 인기 키엔 확률적 조기 갱신을 더 얹고요.

Hot Key — 한 키에 트래픽이 쏠릴 때

캐시 스탬피드가 시간의 문제라면, Hot Key는 공간의 문제예요. 특정 키 하나에 트래픽이 비정상적으로 몰리는 경우죠. 갑자기 인기 폭발한 상품, 실시간 인기 검색어, 전체 공통 설정값 같은 것들.

문제는 68편처럼 클러스터로 잘 샤딩해 놨어도, 한 키는 한 노드에만 있다는 거예요. 그 키가 핫해지면 그 노드 하나만 과부하로 죽어요. 샤딩이 무력해지는 순간이에요. 푸는 방향은 둘이에요.

로컬 캐시(애플리케이션 메모리) — 핫 키만큼은 Redis에 매번 묻지 말고, 애플리케이션 프로세스 메모리에 잠깐(수 초) 들고 있어요. 그러면 트래픽이 Redis 노드에 닿기 전에 각 애플리케이션 서버에서 흡수돼요. Redis 앞에 한 겹 더 캐시를 두는 셈이에요.

키 분산(복제) — 같은 값을 key 하나가 아니라 key:1·key:2…처럼 여러 개로 복제해 서로 다른 노드에 흩고, 읽을 때 그중 하나를 무작위로 골라요. 한 노드에 쏠리던 트래픽이 여러 노드로 나뉘죠. 쓰기가 늘어나는 비용은 있지만, 읽기 쏠림을 분산하는 확실한 방법이에요.

함정 하나 더 — 타임아웃 정렬

여기서 자주 놓치는 함정 — 호출자(caller)와 피호출자(callee)의 타임아웃이 어긋나면 장애가 증폭돼요. 예를 들어 호출자는 1초 만에 포기하고 재시도하는데, 피호출자(DB·캐시)는 아직 3초짜리 작업을 붙들고 있으면, 포기된 요청은 자원을 계속 잡아먹는데 재시도가 새 부하를 또 얹어요. 결국 같은 일을 중복으로 하며 부하가 눈덩이처럼 불어요.

원칙은 "바깥 타임아웃 > 안쪽 타임아웃" — 호출자 타임아웃을 피호출자보다 넉넉히 둬서, 안쪽이 끝날 기회를 주고 헛된 재시도를 줄여요. 스탬피드·Hot Key 대응에 재시도를 얹을 땐 이 정렬을 꼭 같이 봐야 해요.

시험·면접 직전 압축 노트 — 캐시 스탬피드·Hot Key

  • 캐시 스탬피드(Thundering Herd) = 인기 키 TTL 동시 만료 → 수천 요청이 동시에 DB로 → DB 폭주
  • 비유 = 셔터 올라가는 순간 손님 우르르
  • 완화 1 = 뮤텍스 잠금(SET NX) — 한 요청만 DB 조회·갱신, 나머지는 대기
  • 완화 2 = 확률적 조기 갱신 — 만료 임박 시 일부 요청이 미리 갱신(인기 키에 적합)
  • 완화 3 = TTL 지터 — TTL에 ±무작위로 동시 만료 분산(가장 쌈)
  • 셋은 조합 가능(지터 + 뮤텍스 + 조기 갱신)
  • Hot Key = 한 키에 트래픽 쏠림 → 그 키가 있는 노드 하나만 과부하(샤딩 무력)
  • Hot Key 해법 1 = 로컬 캐시(앱 메모리에 수 초) — Redis 앞 한 겹
  • Hot Key 해법 2 = 키 분산(key:1·key:2…를 여러 노드에, 읽기 시 무작위 선택)
  • 타임아웃 정렬 = 바깥(caller) > 안쪽(callee) — 어긋나면 헛된 재시도로 부하 증폭
  • 재시도를 얹을 땐 타임아웃 정렬을 반드시 함께 확인

공식 문서: Redis — Client-side caching62편 캐싱 패턴을 함께 보면 완화책의 맥락이 잡혀요.

시리즈 다른 편

같이 읽으면 좋은 글:

답글 남기기

error: Content is protected !!