566. 캐시 스탬피드 (Cache Stampede) 및 확률적 갱신 회피 기법

⚠️ 이 문서는 대규모 트래픽이 몰리는 분산 캐시 환경(Redis 등)에서, 모두가 찾고 있는 인기 데이터의 캐시 유효기간(TTL)이 만료되는 순간 수만 개의 요청이 동시에 DB로 쏟아져 들어가 DB를 마비시키는 '캐시 스탬피드(Cache Stampede)' 현상과 이를 해결하기 위한 뮤텍스 락(Mutex Lock) 및 확률적 조기 갱신(Probabilistic Early Expiration) 기법을 다룹니다.

핵심 인사이트 (3줄 요약)

  1. 본질: 캐시가 만료된 0.01초의 틈을 타고 들어온 수만 명의 사용자(스레드)가, 서로 자기가 캐시를 채워 넣겠다고 동시에 원본 DB에 무거운 쿼리를 날려버려 뒷단 시스템을 도미노처럼 붕괴시키는 현상이다.
  2. 가치: 캐시 만료 시점의 트래픽을 정교하게 제어함으로써, 아무리 트래픽이 폭주해도 백엔드 DB에는 딱 1번의 쿼리만 날아가도록 보장하여 전체 시스템의 가용성과 성능을 지켜낸다.
  3. 기술 체계: 한 놈만 DB에 다녀오게 하는 뮤텍스 분산 락(Mutex Lock) 방식과, TTL이 진짜 끝나기 전에 누군가 선제적으로 캐시를 갱신하게 만드는 확률적 조기 갱신(Probabilistic Early Expiration, PER) 알고리즘이 대표적인 해결책이다.

Ⅰ. 캐시 스탬피드(Cache Stampede): 코끼리 떼의 질주

캐시가 뚫리는 단 1초의 틈이 대재앙을 부른다.

  1. 상황극 (The Disaster):
    • 네이버 메인 뉴스 기사를 캐시(Redis)에 10분(TTL) 동안 올려두었다. 1초에 1만 명이 접속해서 캐시를 잘 읽어간다.
    • 10분이 지나 캐시가 만료(Expired)되었다.
    • 하필 그 1초 사이에 1만 명의 사용자가 동시에 접속했다. 1만 명 모두 캐시에 기사가 없는 것(Cache Miss)을 확인했다.
  2. 중복 쿼리 폭풍 (Thundering Herd):
    • 1만 명의 앱 서버 스레드들은 각자 자기가 기사를 퍼오겠다며 일제히 원본 오라클 DB에 SELECT 쿼리를 날린다.
    • DB는 똑같은 기사를 1만 번 읽어내느라 CPU가 100% 치솟고 마비된다. 설상가상으로 1만 명의 스레드가 0.1초 뒤에 똑같은 기사 데이터를 들고 와서 Redis에 1만 번 SET을 덮어치며 Redis마저 뻗어버린다.

📢 섹션 요약 비유: 인기 아이돌 콘서트장(웹사이트) 입구에서 경호원(캐시)이 표를 나눠주고 있다가, 경호원이 화장실에 간(TTL 만료) 10초의 틈이 생겼습니다. 기다리던 1만 명의 팬(사용자 요청)이 이성을 잃고 한꺼번에 문(DB)을 부수고 돌진하여 콘서트장이 박살 나는(Stampede: 코끼리 떼의 질주) 무시무시한 현상입니다.


Ⅱ. 해결책 1: 분산 락 (Mutex Lock) - "한 놈만 갔다 와라"

가장 직관적이고 무식하게 코끼리 떼를 막아 세우는 방법이다.

  1. Lock의 획득 (Mutex):
    • 캐시가 만료된 것을 확인한 1만 명 중, 가장 먼저 도착한 1명만 Redis의 분산 락(예: SETNX lock_news_1)을 획득한다.
    • 락을 얻은 1명만 대표로 원본 DB에 가서 기사를 읽어오고 Redis에 캐시를 채워 넣는다.
  2. 나머지 9,999명의 대기 (Spin Lock):
    • 락을 얻지 못한 9,999명은 DB로 돌진하지 못하고, 캐시가 채워질 때까지 0.1초 간격으로 while 문을 돌며 기다리거나(Sleep & Retry) 이전의 낡은 캐시 값을 임시로 반환받는다.
  3. 단점 (병목과 교착 상태):
    • 대표 1명이 DB에서 데이터를 가져오는 데 10초가 걸리면(쿼리 지연), 9,999명도 화면이 멈춘 채 10초를 기다려야 한다(Blocking). 만약 대표가 락을 쥔 채로 죽어버리면 전체 시스템이 뻗어버리는 데드락(Deadlock) 위험이 있다.

📢 섹션 요약 비유: 경호원이 화장실에 갈 때, 콘서트장 문에 자물쇠(Lock)를 채우고 열쇠를 줄을 선 첫 번째 팬 1명에게만 줍니다. "너 혼자 들어가서 포스터(데이터) 1장 챙겨서 복사해와. 나머지 9,999명은 문밖에서 대기해!"라고 막아서, 문(DB)이 부서지는 것을 막는 물리적인 통제법입니다.


Ⅲ. 해결책 2: 확률적 조기 갱신 (Probabilistic Early Expiration / PER)

수학적 마법을 통해 아예 캐시가 '만료되는 순간' 자체를 없애버린다.

  1. 논리적 만료의 트릭 (Logical Expiration):
    • Redis에 TTL을 10분으로 세팅하지 않고 무한대(영원히 안 지워짐)로 둔다.
    • 대신 데이터 안에 [만료 예정 시간 = 10분 뒤]라는 시간표를 글자로 적어둔다.
  2. 확률적 계산식 (XF-Fetch Algorithm):
    • 만료 시간 1분 전쯤부터 접속한 사용자들은, 캐시를 읽으면서 내부적으로 주사위를 굴린다(확률 계산).
    • 만료 시간이 가까워질수록 이 주사위가 '당첨'될 확률이 기하급수적으로 높아진다.
    • 주사위에 당첨된 딱 1명의 사용자만 백그라운드 스레드(Asynchronous)로 몰래 DB에 다녀오게 한다.
  3. 백그라운드 갱신 (Non-blocking):
    • 당첨된 1명조차도 화면이 멈추지 않는다. 자기는 캐시의 낡은 데이터를 받아 즉시 화면을 보면서, 보이지 않는 뒷단에서 DB를 조회해 캐시를 최신 값으로 몰래 업데이트해 둔다.
    • 결과적으로 캐시가 '진짜 0초'가 되어 1만 명이 돌진하는 사태를 사전에 100% 방지할 수 있다.

📢 섹션 요약 비유: 경호원이 화장실에 가기 전(TTL 만료 1분 전), 줄 서 있는 사람들에게 주사위를 굴리게 합니다. 당첨된 1명에게 "야, 너 빨리 뒷문으로 가서 포스터 새것으로 교체해 놓고 와"라고 심부름을 시킵니다. 아무도 눈치채지 못하는 사이에 포스터가 새것으로 바뀌어 있기 때문에, 정작 10분이 지나도 문 앞에서는 아무런 소동(Stampede)도 일어나지 않는 세련된 마술입니다.