LabHub
开始
学习 学习路径 课程

打造好服务的计算机科学 — 用测量重新学习教科书概念

按交错顺序测量缓存的谎言

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

결정적 시뮬레이터로 캐시 쓰기·읽기 전략 여섯 개를 같은 끼어들기 순서에 넣어 보고, 시나리오마다 오래된 읽기 수최대 오래됨(ms) 을 잽니다. 끝에는 영영 고쳐지지 않는 조합을 가려내고, 숫자를 근거로 전략 하나를 고릅니다.

왜 중요한가

캐시 불일치는 재현이 어렵습니다. 몇 ms 차이로 겹친 요청 두 개가 만들고, 다음 날 다시 돌리면 사라집니다. 그래서 '쓰기 후 삭제면 괜찮다', 'TTL 이 있으니 최대 TTL 만큼만 틀린다' 같은 믿음이 검증 없이 남습니다. 연산이 끝나는 시각을 정수 ms 로 못박은 시뮬레이터에서는 같은 순서를 몇 번이고 다시 돌릴 수 있고, 전략만 바꿔 같은 순서에 넣어 볼 수 있습니다. 패턴 설명은 'Redis 와 캐싱' 코스에 있으니, 여기서는 순서와 숫자만 봅니다.

재료

/opt/fixtures/svccs/cache/ 아래에 있습니다. 읽기만 하세요.

sim.py                 시뮬레이터. 맨 위 설명에 원시 연산·제너레이터 규칙·명령줄 사용법이 있다
example_strategies.py  시작 틀 — write_update 와 read_aside
scenarios/*.json       시나리오 다섯. name · about(설명) · initial · warm(미리 채운 키) · ttl_ms · delay_ms ·
                       actors[{name, kind(read|write), key, value, start, lat{연산: ms}}]

명령줄: python3 /opt/fixtures/svccs/cache/sim.py <시나리오.json> <전략파일.py> <전략이름> > 기록.json 전략 이름은 update·delete_after·delete_before·double_delete·cas·ttl 이고, 각각 쓰는 함수는 sim.py 의 STRATEGIES 표에 있습니다(ttlwrite_delete_after + read_aside 에 시나리오의 ttl_ms 를 줍니다).

기록의 모양

events(연산이 끝난 순서, seq·t·actor·op …), commits(seq·key·version·value·t = 커밋 시각·writer), reads(seq = 그 읽기의 마지막 연산 seq · actor · key · start · t = 끝난 시각 · value · version), final_db, final_cache(키마다 value·version·set_t·expires 또는 null) 입니다.

측정 규칙

오래된 읽기  r 보다 작은 seq 로 커밋된 같은 키의 버전 중 가장 큰 것이 r.version 보다 크다
오래됨(ms)   r.t − (버전 r.version+1 커밋의 t)            ← 쓰기를 시작한 시각이 아니다
stuck        끝났을 때 final_cache 의 항목이 expires 가 null 이고 version 이 final_db 보다 작다
돌려줄 것    {"stale_reads": 정수, "max_stale_ms": 정수 또는 null, "stuck": 불리언}
             stuck 이면 max_stale_ms 는 null(상한 없음), 오래된 읽기가 없으면 0

단계

  1. /root/svccs/cache/ 를 만들고 example_strategies.py/root/svccs/cache/strategies.py 로 복사한 뒤, s1-two-writers 시나리오를 update 전략으로 돌린 기록을 /root/svccs/cache/s1-update.log.json 에 저장합니다.
  2. strategies.pymeasure(log) 를 '측정 규칙' 대로 만들고, 1단계 기록을 잰 결과를 /root/svccs/cache/s1-update.measure.json 에 저장합니다. 채점기는 숨은 시나리오 기록들로 measure 를 다시 부릅니다.
  3. write_delete_after(커밋 → 캐시 삭제)와 write_delete_before(캐시 삭제 → 커밋)를 만듭니다. 쓰기 함수는 (key, value, opts) 를 받는 제너레이터입니다.
  4. is_newer(cached, new)(캐시가 비었거나 new 가 엄격히 클 때만 참), write_cas(커밋 → 받은 버전으로 cache_set_if), read_aside_cas(read_aside 와 같되 채울 때 cache_set_if)를 만듭니다. cache_set_if 의 마지막 인자로 is_newer 를 넘깁니다.
  5. write_double_delete(캐시 삭제 → 커밋 → ("sleep", opts["delay_ms"]) → 캐시 삭제)를 만듭니다.
  6. 시나리오 다섯 × 전략 여섯을 모두 돌려 measure 한 결과를 /root/svccs/cache/results.json{시나리오 name: {전략 이름: measure 결과}} 로 저장합니다.
  7. ttl 전략의 시나리오별 max_stale_ms 를 모아 /root/svccs/cache/ttl.jsonttl_ms(시나리오들의 ttl_ms, 정수)·max_stale_ms_by_scenario·exceeds_ttl(max_stale_ms 가 ttl_ms 보다 큰 시나리오 이름, 가나다순 목록)을 적습니다. 채점기는 여러분의 ttl 전략도 숨은 시나리오에서 다시 돌립니다.
  8. /root/svccs/cache/choice.jsonstuck(전략마다 stuck 이 참이었던 시나리오 이름 목록, 정렬)·safe(어느 시나리오에서도 stuck 이 아닌 전략, 정렬)·chosen(safe 가운데 시나리오별 max_stale_ms 의 최댓값이 가장 작은 것, 같으면 stale_reads 합이 작은 것, 그래도 같으면 이름순)·chosen_worst_ms(그 최댓값)를 적습니다.

참고

시뮬레이터를 한 번 돌린다

example_strategies.py 를 /root/svccs/cache/strategies.py 로 복사하고, s1-two-writers.json 을 update 전략으로 돌린 기록을 /root/svccs/cache/s1-update.log.json 에 저장한다.

sim.py 는 표준 출력으로 기록 JSON 을 냅니다. > 로 파일에 받으세요. 기록의 events 를 훑어 W1 의 늦은 cache_set 이 W2 보다 뒤에 도착하는 줄을 찾아보세요.

오래됨을 커밋 기준으로 잰다

strategies.py 에 measure(log) 를 측정 규칙대로 만들고, 1단계 기록을 잰 결과를 /root/svccs/cache/s1-update.measure.json 에 저장한다. 채점기는 숨은 시나리오 기록들로 measure 를 다시 부른다.

읽기의 seq 와 커밋의 seq 를 비교해 '읽기가 끝나기 전에 커밋된 것' 만 봅니다. 오래됨의 기준은 돌려준 버전 바로 다음 버전이 커밋된 시각입니다. 캐시에 만료 없는 옛 값이 남았다면 관측한 최댓값이 얼마든 상한은 없습니다.

쓰기 후 삭제, 삭제 후 쓰기

strategies.py 에 write_delete_after(커밋 → 캐시 삭제)와 write_delete_before(캐시 삭제 → 커밋)를 만든다. 채점기가 숨은 시나리오에서 두 전략의 연산 기록을 기준과 대조한다.

쓰기 함수는 (key, value, opts) 를 받는 제너레이터이고 연산 두 개를 yield 합니다. 결과를 쓰지 않는 연산도 yield 해야 시뮬레이터가 시간을 흘립니다.

버전을 비교해서만 덮는다

strategies.py 에 is_newer(cached, new)·write_cas·read_aside_cas 를 만든다. 채점기는 is_newer 를 직접 부르고, cas 전략을 숨은 시나리오에서 기준과 대조한다.

is_newer 는 캐시에 있는 버전(cached, 없으면 None)과 넣으려는 버전(new)을 받습니다. 옛 값이 새 값을 덮지 못하게 하려면 어느 쪽이 커야 할까요. write_cas 는 db_write 가 돌려준 버전을 씁니다.

지연 이중 삭제

strategies.py 에 write_double_delete(캐시 삭제 → 커밋 → ("sleep", opts["delay_ms"]) → 캐시 삭제)를 만든다. 채점기가 숨은 시나리오에서 기준과 대조한다.

sleep 은 결과가 없는 연산이지만 yield 해야 그만큼 시간이 흐릅니다. 기다리는 시간은 시나리오마다 다르므로 opts 에서 읽으세요.

다섯 시나리오 × 여섯 전략

시나리오 다섯을 전략 여섯으로 모두 돌려 measure 한 결과를 /root/svccs/cache/results.json 에 {시나리오 name: {전략 이름: measure 결과}} 로 저장한다.

sim.py 를 import 해 sim.run_strategy(시나리오, 전략모듈, 이름) 을 부르면 기록 dict 가 바로 나옵니다. 시나리오 이름은 파일 안의 name 입니다.

TTL 은 오래됨을 어디까지 묶나

ttl 전략의 시나리오별 max_stale_ms 를 모아 /root/svccs/cache/ttl.json 에 ttl_ms·max_stale_ms_by_scenario·exceeds_ttl 을 적는다. 채점기는 여러분의 ttl 전략도 숨은 시나리오에서 다시 돌린다.

TTL 은 캐시에 넣은 시각부터 셉니다. 커밋보다 한참 뒤에 옛 값이 들어오면 무엇이 되나요. ttl 전략은 read_aside 가 opts["ttl_ms"] 를 set 에 넘겨야 동작합니다.

숫자로 전략을 고른다

results.json 으로 /root/svccs/cache/choice.json 에 stuck·safe·chosen·chosen_worst_ms 를 적는다.

한 시나리오에서라도 stuck 이면 그 전략은 오래됨에 상한이 없으므로 safe 가 아닙니다. 어떤 시나리오에서 오래된 읽기가 0 이었다는 것은 안전의 근거가 되지 못합니다.