LabHub
배우기 러닝패스 코스

Redisとキャッシュ

キャッシュスタンピードを防ぐ

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 세 개이고 원본호출 열이 있어야 한다.

참고

스탬피드 재현하기

/opt/app/slowdb.py(8151) 를 띄우고 /root/sp/naive.py 로 인기 키를 지운 직후 동시 50건을 요청한다. /root/sp/naive.outconcurrency=50 origin_calls=<n> 을 적고 n 은 20 이상이어야 한다.

인기 키를 지운 직후 동시 요청을 쏟아부으면 됩니다. 원본 조회 수가 동시 요청 수만큼 늘어납니다.

싱글플라이트 잠금 걸기

/root/sp/lock.py 는 미스 때 SET lock:<키> <토큰> NX EX 5 로 잠금을 얻은 요청만 원본에 간다. 같은 조건으로 /root/sp/lock.outconcurrency=50 origin_calls=<n> 을 적고 n 은 3 이하여야 한다.

미스 때 첫 요청만 원본에 가게 합니다. 잠금 획득은 원자적이어야 합니다.

잠금 대기자가 값을 받아 가게 하기

잠금을 못 얻은 요청은 최대 2초 동안 짧게 자며 캐시를 다시 본다. 50건 모두 값을 받아야 한다. /root/sp/lock.outserved=50 을 함께 적는다.

잠금을 못 얻은 요청이 그냥 실패하면 안 됩니다. 짧게 자면서 캐시를 다시 보게 하세요.

잠금 TTL 로 데드락 막기

잠금 키에 TTL 이 걸려 있어야 한다. /root/sp/lockttl.txtlock_ttl=<초> 를 적고 1 이상 30 이하여야 한다.

잠금 소유자가 죽으면 아무도 못 들어갑니다. 잠금 자체에 수명을 주세요.

소유권 토큰으로 남의 잠금 보호하기

잠금 해제는 소유권 토큰이 일치할 때만 한다. /root/sp/owner.outwrong_token_release=0 right_token_release=1 을 적는다. 소스에 토큰 비교가 있어야 한다.

TTL 로 만료된 뒤 다른 요청이 잡은 잠금을, 늦게 깨어난 원래 소유자가 풀면 안 됩니다.

TTL 지터로 동시 만료 분산하기

/root/sp/jitter.py 로 키 100개를 base 300초, 폭 60초 지터로 채운다. /root/sp/jitter.txtmin_ttl=<n> max_ttl=<n> distinct=<n> 을 적고 distinct 는 20 이상, max 와 min 의 차는 30 이상이어야 한다.

같은 순간에 채운 키들의 만료 시각을 흩뜨립니다. 100개 키의 TTL 분포를 확인해 보세요.

stale-while-revalidate 구현하기

/root/sp/swr.py 는 값과 함께 논리 만료 시각을 저장하고, 만료 후에도 옛 값을 즉시 반환하면서 백그라운드로 한 번만 갱신한다. /root/sp/swr.outserved_from_stale=<n> origin_calls=<n> 을 적고 origin_calls 는 3 이하여야 한다.

만료돼도 옛 값을 즉시 주고 뒤에서 한 번만 갱신합니다. 값과 만료 시각을 따로 저장하면 됩니다.

세 방식의 원본 호출 수 비교하기

/root/sp/compare.md 에 마크다운 표를 쓴다. 행 제목은 무방비, 싱글플라이트, stale-while-revalidate 세 개이고 원본호출 열이 있어야 한다.

무방비, 잠금, stale-while-revalidate 를 같은 부하로 비교합니다. 표 형식과 행 제목이 채점 기준입니다.