防住缓存击穿
한국어 원문으로 표시합니다.
목표
캐시 스탬피드를 실제로 일으켜 원본 호출 폭증을 눈으로 확인하고, 잠금과 지터와 stale-while-revalidate 를 각각 붙여 방어 효과를 숫자로 비교한다.
왜 중요한가
스탬피드는 캐시가 잘 작동하고 있을 때만 일어나는 사고입니다. 인기 키일수록 위험하다는 역설 때문입니다 — 초당 수천 번 읽히던 키가 만료되는 그 찰나에 수천 개 요청이 동시에 미스를 겪고 모두 원본으로 몰려갑니다. 하나면 충분했을 조회가 수천 개가 되고, 원본이 짓눌리면 더 많은 미스가 나며 연쇄적으로 무너집니다. 이 실습에서 특히 놓치기 쉬운 것이 5번 스텝입니다. 잠금에 TTL 만 걸고 소유권 토큰을 두지 않으면, TTL 로 만료된 뒤 다른 요청이 잡은 잠금을 뒤늦게 깨어난 원래 소유자가 해제해 버립니다. 그러면 두 요청이 동시에 임계 구역에 들어갑니다. 분산 잠금을 직접 만들 때 가장 자주 빠뜨리는 부분이고, 그래서 한 스텝을 통째로 배정했습니다.
단계
/opt/app/slowdb.py(8151) 를 띄우고/root/sp/naive.py로 인기 키를 지운 직후 동시 50건을 요청한다./root/sp/naive.out에concurrency=50 origin_calls=<n>을 적고 n 은 20 이상이어야 한다./root/sp/lock.py는 미스 때SET lock:<키> <토큰> NX EX 5로 잠금을 얻은 요청만 원본에 간다. 같은 조건으로/root/sp/lock.out에concurrency=50 origin_calls=<n>을 적고 n 은 3 이하여야 한다.- 잠금을 못 얻은 요청은 최대 2초 동안 짧게 자며 캐시를 다시 본다. 50건 모두 값을 받아야 한다.
/root/sp/lock.out에served=50을 함께 적는다. - 잠금 키에 TTL 이 걸려 있어야 한다.
/root/sp/lockttl.txt에lock_ttl=<초>를 적고 1 이상 30 이하여야 한다. - 잠금 해제는 소유권 토큰이 일치할 때만 한다.
/root/sp/owner.out에wrong_token_release=0 right_token_release=1을 적는다. 소스에 토큰 비교가 있어야 한다. /root/sp/jitter.py로 키 100개를 base 300초, 폭 60초 지터로 채운다./root/sp/jitter.txt에min_ttl=<n> max_ttl=<n> distinct=<n>을 적고 distinct 는 20 이상, max 와 min 의 차는 30 이상이어야 한다./root/sp/swr.py는 값과 함께 논리 만료 시각을 저장하고, 만료 후에도 옛 값을 즉시 반환하면서 백그라운드로 한 번만 갱신한다./root/sp/swr.out에served_from_stale=<n> origin_calls=<n>을 적고 origin_calls 는 3 이하여야 한다./root/sp/compare.md에 마크다운 표를 쓴다. 행 제목은무방비,싱글플라이트,stale-while-revalidate세 개이고원본호출열이 있어야 한다.
참고
- 원자적 잠금:
SET lock:key <uuid> NX EX 5— 반환이 OK 면 획득입니다. - 안전한 해제는 값 비교 후 삭제여야 하며, 이 둘도 원자적이어야 완전합니다(Lua 스크립트).
- 동시 요청:
for i in $(seq 50); do curl -s ... & done; wait - 흔한 실수 1: 잠금 대기자를 그냥 실패시키는 것 — 사용자 절반이 오류를 봅니다.
- 흔한 실수 2: 잠금 해제를 무조건
DEL로 하는 것 — 남의 잠금을 풀 수 있습니다.
스탬피드 재현하기
/opt/app/slowdb.py(8151) 를 띄우고 /root/sp/naive.py 로 인기 키를 지운 직후 동시 50건을 요청한다. /root/sp/naive.out 에 concurrency=50 origin_calls=<n> 을 적고 n 은 20 이상이어야 한다.
인기 키를 지운 직후 동시 요청을 쏟아부으면 됩니다. 원본 조회 수가 동시 요청 수만큼 늘어납니다.
싱글플라이트 잠금 걸기
/root/sp/lock.py 는 미스 때 SET lock:<키> <토큰> NX EX 5 로 잠금을 얻은 요청만 원본에 간다. 같은 조건으로 /root/sp/lock.out 에 concurrency=50 origin_calls=<n> 을 적고 n 은 3 이하여야 한다.
미스 때 첫 요청만 원본에 가게 합니다. 잠금 획득은 원자적이어야 합니다.
잠금 대기자가 값을 받아 가게 하기
잠금을 못 얻은 요청은 최대 2초 동안 짧게 자며 캐시를 다시 본다. 50건 모두 값을 받아야 한다. /root/sp/lock.out 에 served=50 을 함께 적는다.
잠금을 못 얻은 요청이 그냥 실패하면 안 됩니다. 짧게 자면서 캐시를 다시 보게 하세요.
잠금 TTL 로 데드락 막기
잠금 키에 TTL 이 걸려 있어야 한다. /root/sp/lockttl.txt 에 lock_ttl=<초> 를 적고 1 이상 30 이하여야 한다.
잠금 소유자가 죽으면 아무도 못 들어갑니다. 잠금 자체에 수명을 주세요.
소유권 토큰으로 남의 잠금 보호하기
잠금 해제는 소유권 토큰이 일치할 때만 한다. /root/sp/owner.out 에 wrong_token_release=0 right_token_release=1 을 적는다. 소스에 토큰 비교가 있어야 한다.
TTL 로 만료된 뒤 다른 요청이 잡은 잠금을, 늦게 깨어난 원래 소유자가 풀면 안 됩니다.
TTL 지터로 동시 만료 분산하기
/root/sp/jitter.py 로 키 100개를 base 300초, 폭 60초 지터로 채운다. /root/sp/jitter.txt 에 min_ttl=<n> max_ttl=<n> distinct=<n> 을 적고 distinct 는 20 이상, max 와 min 의 차는 30 이상이어야 한다.
같은 순간에 채운 키들의 만료 시각을 흩뜨립니다. 100개 키의 TTL 분포를 확인해 보세요.
stale-while-revalidate 구현하기
/root/sp/swr.py 는 값과 함께 논리 만료 시각을 저장하고, 만료 후에도 옛 값을 즉시 반환하면서 백그라운드로 한 번만 갱신한다. /root/sp/swr.out 에 served_from_stale=<n> origin_calls=<n> 을 적고 origin_calls 는 3 이하여야 한다.
만료돼도 옛 값을 즉시 주고 뒤에서 한 번만 갱신합니다. 값과 만료 시각을 따로 저장하면 됩니다.
세 방식의 원본 호출 수 비교하기
/root/sp/compare.md 에 마크다운 표를 쓴다. 행 제목은 무방비, 싱글플라이트, stale-while-revalidate 세 개이고 원본호출 열이 있어야 한다.
무방비, 잠금, stale-while-revalidate 를 같은 부하로 비교합니다. 표 형식과 행 제목이 채점 기준입니다.