에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/캐시

[Redis] 캐시 스탬피드, 관통, 사태: 원인과 해결 방법 (TTL jitter, 뮤텍스, Bloom Filter)

반응형
[Redis] 캐시 스탬피드, 관통, 사태: 원인과 해결 방법 (TTL jitter, 뮤텍스, Bloom Filter)

[Redis] 캐시 스탬피드, 관통, 사태: 원인과 해결 방법 (TTL jitter, 뮤텍스, Bloom Filter)

캐시는 평소엔 DB를 지켜주지만 어떤 순간엔 오히려 장애의 방아쇠가 돼요. 인기 있는 캐시 하나가 만료되는 순간 수많은 요청이 동시에 DB로 쏟아지거나, 캐시가 통째로 비는 순간 DB가 무너지는 식이에요. 이런 캐시발 장애가 크게 세 가지예요. 스탬피드·관통·사태예요.

캐시 스탬피드를 중심에 두고 그 해법들을 하나씩 보면서, 같은 뿌리에서 나오는 캐시 관통과 사태까지 함께 다뤄요. 캐싱 기본은 스프링 캐싱 글을 먼저 보면 좋아요.

01. 캐시 스탬피드란

캐시 스탬피드(thundering herd)는 인기 있는 캐시 키가 만료되는 순간, 그걸 기다리던 수많은 요청이 동시에 캐시 미스가 나서 한꺼번에 DB로 몰리는 현상이에요.

캐시 스탬피드 발생 구조. 평소엔 인기 키 하나가 수천 요청을 캐시에서 막아주지만, 만료 직후 재적재 전 틈에 들어온 요청이 전부 미스가 나서 같은 DB 쿼리가 수천 건 동시에 직행한다

평소에는 그 키 하나가 수천 요청을 캐시에서 막아주고 있었어요. 그런데 만료되는 그 찰나에, 아직 아무도 캐시를 다시 못 채운 사이 들어온 요청들이 전부 "캐시에 없네" 하고 DB를 쳐요. 한 건이면 될 DB 조회가 동시에 수천 건이 되는 거예요. 인기 키일수록 폭발이 커요.

02. 해법 1, 뮤텍스(분산 락)로 한 명만 갱신

가장 직접적인 해법은 "캐시를 채우는 건 한 요청만 하게" 하는 거예요. 캐시 미스가 나면 분산 락을 잡고 락을 잡은 한 요청만 DB를 조회해 캐시를 채워요. 나머지는 잠깐 기다렸다 채워진 캐시를 읽어요.

이러면 DB 조회가 동시에 수천 건이 아니라 한 건이 돼요. 다만 락 대기가 생기니, 락을 못 잡은 요청을 어떻게 처리할지를 정해야 해요. 보통 두 가지예요. 아주 짧게 기다렸다 채워진 캐시를 읽거나, 기다리지 않고 잠깐 오래된 값을 그대로 주는 거예요.

주의할 함정 — 락 자체가 또 다른 병목이 될 수 있어요. 수천 요청이 한 락을 두고 대기하면 그 대기가 곧 응답 지연이에요. DB 폭주를 락 정체로 바꾼 것뿐일 수 있죠. 그래서 뮤텍스는 "정말 갱신 비용이 크고 인기 있는 소수의 키"에만 쓰고 대기 시간 상한을 둬서 너무 오래 매달리지 않게 해요. 모든 캐시에 락을 거는 건 과해요.

03. 해법 2, TTL을 흩어서 동시 만료를 막기

여러 키가 같은 시각에 한꺼번에 만료되면 스탬피드가 여러 키에서 동시에 터져요. 예를 들어 캐시를 한 번에 워밍하면 전부 같은 TTL이라 같은 시각에 만료돼요.

그래서 TTL에 약간의 무작위(jitter)를 더해요. 10분 고정이 아니라 9~11분처럼 흩으면, 만료 시각이 분산돼서 동시 만료가 줄어요. 한 줄 더한 것뿐인데 여러 키가 동시에 터지는 걸 크게 줄여줘요.

// 고정 TTL 대신 ±랜덤 jitter
long ttl = 600 + ThreadLocalRandom.current().nextLong(-60, 60);  // 540~659초
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttl));

04. 해법 3, 만료 전에 미리 갱신 (PER)

더 영리한 방법은 만료되기 전에 미리 갱신하는 거예요. 확률적 조기 갱신(PER, Probabilistic Early Recomputation)이라고 해요.

아이디어는 만료가 가까워질수록 점점 높은 확률로 "내가 미리 갱신할게"를 한 요청이 자원해서 하는 거예요. 만료 시각에 다 같이 미스 나기 전에, 그 전에 한 요청이 백그라운드로 갱신해두니 스탬피드 자체가 안 생겨요. 구현이 조금 복잡하지만 트래픽이 큰 핵심 키에 효과가 좋아요.

05. 해법 4, 오래된 값을 주면서 뒤에서 갱신

실시간성이 조금 양보돼도 되면, 만료된 직후에도 잠깐 오래된 값을 그대로 주고 갱신은 백그라운드에서 하는 방법도 있어요. 사용자는 약간 낡은 값을 받지만 빠르게 응답받고 갱신은 한 요청만 비동기로 해요.

이러면 DB로 몰리는 일도 없고 응답도 안 느려져요. "잠깐의 낡음을 허용할 수 있는가"가 적용 기준이에요. 가격·재고처럼 정확해야 하는 건 안 맞고, 조회수·추천 목록처럼 약간 낡아도 되는 데 맞아요.

06. 스탬피드, 어느 해법을 고를까

캐시 스탬피드 해법 4종 비교. TTL jitter를 기본으로 깔고, 갱신 비용이 큰 키엔 뮤텍스, 트래픽이 큰 핵심 키엔 PER, 낡은 값을 허용할 수 있으면 stale 응답과 백그라운드 갱신을 더한다
  • TTL jitter — 가장 싸고 항상 켜둘 만해요. 동시 만료만 흩어도 효과가 커서, 다른 해법의 기본 깔개로 같이 써요.
  • 뮤텍스(락) — 갱신 비용이 크고 정확한 값이 필요할 때. 단 락 대기가 생겨요.
  • PER(조기 갱신) — 트래픽이 아주 큰 소수의 핵심 키에. 구현이 복잡한 대신 스탬피드를 근본적으로 없애요.
  • 오래된 값 + 백그라운드 갱신 — 잠깐의 낡음을 허용할 수 있을 때. 가장 매끈하지만 정확성을 양보해요.

대부분은 TTL jitter를 기본으로 깔고 특별히 인기 있고 갱신이 무거운 키에만 뮤텍스나 PER을 더해요. 모든 키에 복잡한 해법을 다 적용할 필요는 없어요. "이 키가 만료될 때 DB가 감당할 수 있나"를 기준으로, 위험한 키에만 더 강한 해법을 붙이는 거예요.

07. 캐시 관통 (penetration), 없는 데이터를 계속 조회

두 번째 장애는 캐시 관통이에요. 캐싱 글에서 봤듯, 조회 결과가 없으면(null) 보통 캐시를 안 해요. 그래서 "존재하지 않는 키"를 계속 요청하면 매번 캐시 미스가 나서 DB를 쳐요. 캐시가 막아주질 못하는 거예요.

공격적으로 없는 ID를 마구 던지면 이게 DB 부하가 돼요. 막는 방법은 두 가지예요. 하나는 null도 캐시하는 거예요. "이 키는 없음"을 짧은 TTL로 캐시하면 반복 조회를 막아요. 다른 하나는 Bloom Filter예요. "존재할 수도 있는 키" 집합을 메모리 적게 유지해서, 확실히 없는 키는 DB까지 안 가고 걸러내요.

08. 캐시 사태 (avalanche), 한꺼번에 무너짐

세 번째는 캐시 사태예요. 대량의 캐시가 동시에 만료되거나, Redis 자체가 잠깐 다운되면, 그 순간 모든 요청이 DB로 쏟아져 DB가 무너지는 거예요. 스탬피드가 인기 키 하나의 문제라면, 사태는 캐시 전체가 한꺼번에 비는 더 큰 규모예요.

대응은 앞의 것들을 합쳐요. TTL jitter로 동시 만료를 흩고 Redis를 고가용성(복제·이중화)으로 두어 통째 다운을 막아요. DB 앞에 회로 차단기나 레이트 리밋을 둬서 DB가 받는 양 자체를 제한해요. "캐시가 비어도 DB가 버티게" 설계하는 거예요.

09. 3대 장애 정리

캐시 3대 장애 원인과 해법 매핑. 스탬피드는 jitter와 뮤텍스와 PER로, 관통은 null 캐싱과 Bloom Filter로, 사태는 jitter와 고가용성과 DB 보호 장치로 막는다
  • 스탬피드 — 인기 키 만료 순간 동시 미스. → 뮤텍스·PER·jitter
  • 관통 — 없는 데이터 반복 조회. → null 캐싱·Bloom Filter
  • 사태 — 대량 동시 만료·Redis 다운. → jitter·고가용성·DB 보호

셋 다 뿌리는 같아요. "캐시가 못 막아주는 순간 DB가 직접 트래픽을 받는다"예요. 그래서 해법도 결국 "그 순간을 분산하거나, DB가 그 순간을 버티게" 하는 두 방향이에요.

10. 자주 만나는 문제

특정 시간마다 DB 부하가 튀어요

캐시가 같은 시각에 동시 만료되는 거예요. TTL에 jitter를 더해 만료를 흩어요. 배포 직후나 캐시 워밍 직후 일정 시간 뒤에 반복적으로 튄다면 거의 확실해요.

없는 데이터 조회로 DB가 계속 맞아요

캐시 관통이에요. null을 짧은 TTL로 캐시하거나 Bloom Filter로 걸러요.

Redis가 잠깐 죽었더니 서비스가 같이 죽어요

캐시 사태예요. Redis 고가용성과 함께, 캐시 실패 시 DB로 넘어가되 DB가 받는 양을 제한하는 보호 장치를 둬요.

정리

캐시는 평소엔 방패지만 비는 순간 약점이 돼요. 세 가지 캐시발 장애를 이렇게 막아요.

  • 스탬피드(인기 키 만료 순간 동시 미스) → 뮤텍스·PER·TTL jitter
  • 관통(없는 데이터 반복 조회) → null 캐싱·Bloom Filter
  • 사태(대량 동시 만료·Redis 다운) → jitter·고가용성·DB 보호

셋의 뿌리는 하나예요. "캐시 미스가 몰리는 순간을 분산하거나, DB가 그 순간을 버티게" 하는 거죠. 캐싱·분산 락 위에 선 캐시 장애 편이고 DB가 받는 양 자체를 제한하는 레이트 리미팅으로 이어져요.

출처: Cache stampede · Redis — Patterns

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."