안녕하세요. 자바파커입니다.
캐시 적중률이 99%인데 DB가 갑자기 멈췄습니다. 캐시를 더 많이 쓰고 있었는데 왜 장애가 났을까요?
원인은 인기 키 하나의 만료 시각이었습니다.
캐시가 없어서가 아니라, 모두가 같은 순간 캐시가 없다는 사실을 발견해서 터집니다.
TTL이 1 → 0이 되는 순간 요청이 DB로 폭발하고, 락 하나로 다시 한 줄이 되는 과정은 52초 영상으로 준비했습니다. 공개되면 이 자리에 연결합니다.
Cache Stampede가 발생하는 순서
평소 요청 흐름은 단순합니다.
요청 → Cache HIT → 응답하지만 인기 키가 만료되면 동시에 도착한 요청이 모두 MISS를 봅니다.
100 requests → Cache MISS × 100 → DB query × 100첫 요청이 DB 값을 읽어 캐시에 넣는 동안 나머지 99개도 같은 일을 시작합니다. 이 중복 작업이 DB 연결과 CPU를 소모하고 응답 지연을 키웁니다. 느려진 DB 때문에 요청이 더 오래 남고, 남은 연결이 다시 늘어나는 양의 피드백이 생깁니다.
적중률 99%가 장애를 숨기는 이유
하루 전체로 보면 대부분 HIT라 적중률은 높게 유지될 수 있습니다. 그러나 장애는 평균이 아니라 짧은 한순간에 발생합니다.
- 평균 적중률: 99%
- 특정 인기 키 만료 직후: MISS 100%
- DB 쿼리: 평소 1회 → 순간 100회
그래서 전체 hit ratio만 보면 원인을 놓칩니다. 키별 MISS 급증과 원본 저장소 동시 요청 수를 함께 봐야 합니다.
해결 1 — 한 요청만 재생성한다
대표적인 방법은 짧은 락입니다.
Request 1 → lock 획득 → DB 조회 → Cache SET
Request 2~100 → 대기 또는 이전 값 반환Redis에서는 SET key value NX PX <ttl>처럼 원자적으로 락을 잡을 수 있습니다. 락에는 반드시 만료시간이 있어야 합니다. 값을 재생성하던 프로세스가 죽어도 영원히 잠기지 않게 하기 위해서입니다.
락 소유자 확인 없이 다른 프로세스의 락을 삭제하면 안 됩니다. 고유 토큰을 저장하고, 토큰이 일치할 때만 Lua 스크립트로 삭제하는 방식이 안전합니다.
해결 2 — TTL에 지터를 섞는다
수천 개 키가 배포 시점에 함께 생성되면 같은 시각에 만료될 수 있습니다. TTL을 모두 정확히 10분으로 두지 말고 작은 랜덤 값을 더합니다.
TTL = 600초 + random(0~60초)지터는 여러 키의 동시 만료를 흩뜨립니다. 단, 인기 키 하나에 요청이 몰리는 문제까지 해결하지는 못하므로 락과 목적이 다릅니다.
해결 3 — 만료 전에 갱신한다
논리적 만료시각을 값과 함께 저장하고, 실제 캐시는 조금 더 오래 유지할 수 있습니다.
- 아직 신선함: 현재 값 반환
- 곧 만료: 현재 값 반환 + 백그라운드 갱신
- 갱신 중: 다른 요청은 이전 값 사용
이를 stale-while-revalidate 패턴으로 볼 수 있습니다. 최신성이 몇 초 늦어도 되는 조회에 잘 맞습니다.
락에도 실패 모드가 있다
분산 락을 추가했다고 끝은 아닙니다.
- 락 TTL보다 재생성이 오래 걸림
- 락 보유 프로세스가 중간에 종료됨
- 모든 대기 요청이 동시에 타임아웃됨
- 캐시와 DB가 동시에 느려짐
락 획득 실패 시 무조건 기다릴지, 이전 값을 줄지, 빠르게 실패할지는 데이터의 최신성 요구사항에 따라 결정해야 합니다.
무엇을 모니터링할까
| 지표 | 의미 |
|---|---|
| 키별 MISS 비율 | 특정 인기 키 만료 감지 |
| 원본 동시 요청 수 | stampede 크기 |
| 값 재생성 시간 | 락 TTL 근거 |
| lock wait time | 대기 지연 |
| stale 응답 수 | 완화 정책 동작 여부 |
정리
- 캐시 스탬피드는 같은 키의 동시 MISS가 원본 저장소를 덮치는 현상입니다.
- 적중률 평균이 높아도 짧은 만료 순간에 장애가 날 수 있습니다.
- 한 요청만 재생성하도록 락을 걸고, 나머지는 기다리거나 이전 값을 씁니다.
- 여러 키의 동시 만료는 TTL 지터로 흩뜨립니다.
캐시는 부하를 없애는 것이 아니라 시간을 옮깁니다. 만료 시각을 설계하지 않으면 그 부하가 한순간에 돌아옵니다.
자주 묻는 질문 (FAQ)
캐시 스탬피드와 캐시 애벌랜치는 같은 뜻인가요?
스탬피드는 인기 키 하나가 만료돼 같은 데이터를 재생성하려는 요청이 몰리는 현상입니다. 애벌랜치는 많은 키가 비슷한 시각에 만료되거나 캐시 계층 전체가 장애를 일으켜 원본 저장소로 대규모 트래픽이 쏟아지는 더 넓은 상황을 가리킵니다.
모든 캐시 재생성에 분산 락이 필요한가요?
아닙니다. 원본 조회가 싸고 동시 요청이 적다면 중복 계산을 허용하는 편이 단순합니다. 재생성 비용이 크거나 인기 키에 트래픽이 집중될 때 락, 요청 병합, stale-while-revalidate를 적용합니다.
TTL 지터는 어느 정도가 적당한가요?
고정 비율은 없습니다. 시작점으로 기본 TTL의 5~15%를 무작위로 더하거나 빼고, 키별 만료 분포와 MISS 급증을 관찰해 조정합니다. 너무 큰 지터는 데이터 신선도 요구사항을 깨뜨릴 수 있습니다.