LabHub

Redis 와 캐싱 · 캐시 패턴(look-aside·write-through) · 실습

캐시 스탬피드 방어하기

LabHub 에서 이어서 보기

목표

캐시 스탬피드를 실제로 일으켜 원본 호출 폭증을 눈으로 확인하고, 잠금과 지터와 stale-while-revalidate 를 각각 붙여 방어 효과를 숫자로 비교한다.

왜 중요한가

스탬피드는 캐시가 잘 작동하고 있을 때만 일어나는 사고입니다. 인기 키일수록 위험하다는 역설 때문입니다 — 초당 수천 번 읽히던 키가 만료되는 그 찰나에 수천 개 요청이 동시에 미스를 겪고 모두 원본으로 몰려갑니다. 하나면 충분했을 조회가 수천 개가 되고, 원본이 짓눌리면 더 많은 미스가 나며 연쇄적으로 무너집니다. 이 실습에서 특히 놓치기 쉬운 것이 5번 스텝입니다. 잠금에 TTL 만 걸고 소유권 토큰을 두지 않으면, TTL 로 만료된 뒤 다른 요청이 잡은 잠금을 뒤늦게 깨어난 원래 소유자가 해제해 버립니다. 그러면 두 요청이 동시에 임계 구역에 들어갑니다. 분산 잠금을 직접 만들 때 가장 자주 빠뜨리는 부분이고, 그래서 한 스텝을 통째로 배정했습니다.

단계

1. /opt/app/slowdb.py(8151) 를 띄우고 /root/sp/naive.py 로 인기 키를 지운 직후 동시 50건을 요청한다. /root/sp/naive.outconcurrency=50 origin_calls=<n> 을 적고 n 은 20 이상이어야 한다.
2. /root/sp/lock.py 는 미스 때 SET lock:<키> <토큰> NX EX 5 로 잠금을 얻은 요청만 원본에 간다. 같은 조건으로 /root/sp/lock.outconcurrency=50 origin_calls=<n> 을 적고 n 은 3 이하여야 한다.
3. 잠금을 못 얻은 요청은 최대 2초 동안 짧게 자며 캐시를 다시 본다. 50건 모두 값을 받아야 한다. /root/sp/lock.outserved=50 을 함께 적는다.
4. 잠금 키에 TTL 이 걸려 있어야 한다. /root/sp/lockttl.txtlock_ttl=<초> 를 적고 1 이상 30 이하여야 한다.
5. 잠금 해제는 소유권 토큰이 일치할 때만 한다. /root/sp/owner.outwrong_token_release=0 right_token_release=1 을 적는다. 소스에 토큰 비교가 있어야 한다.
6. /root/sp/jitter.py 로 키 100개를 base 300초, 폭 60초 지터로 채운다. /root/sp/jitter.txtmin_ttl=<n> max_ttl=<n> distinct=<n> 을 적고 distinct 는 20 이상, max 와 min 의 차는 30 이상이어야 한다.
7. /root/sp/swr.py 는 값과 함께 논리 만료 시각을 저장하고, 만료 후에도 옛 값을 즉시 반환하면서 백그라운드로 한 번만 갱신한다. /root/sp/swr.outserved_from_stale=<n> origin_calls=<n> 을 적고 origin_calls 는 3 이하여야 한다.
8. /root/sp/compare.md 에 마크다운 표를 쓴다. 행 제목은 무방비, 싱글플라이트, stale-while-revalidate 세 개이고 원본호출 열이 있어야 한다.

참고

단계 8개

  1. 스탬피드 재현하기
  2. 싱글플라이트 잠금 걸기
  3. 잠금 대기자가 값을 받아 가게 하기
  4. 잠금 TTL 로 데드락 막기
  5. 소유권 토큰으로 남의 잠금 보호하기
  6. TTL 지터로 동시 만료 분산하기
  7. stale-while-revalidate 구현하기
  8. 세 방식의 원본 호출 수 비교하기