LabHub
Get started
배우기 러닝패스 코스

CS for Building Good Services — Relearning Textbook Ideas by Measuring

Measure the Cache's Lies by Interleaving Order

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 이었다는 것은 안전의 근거가 되지 못합니다.