按交错顺序测量缓存的谎言
目标
在确定性模拟器中,把六种缓存写入、读取策略放进同样的交错顺序里,在每个场景下测出陈旧读取数和最大陈旧度(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 表中(ttl 是给 write_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
步骤
- 创建
/root/svccs/cache/,把example_strategies.py复制为/root/svccs/cache/strategies.py,然后把s1-two-writers场景用update策略运行得到的记录,保存到/root/svccs/cache/s1-update.log.json。 - 在
strategies.py中按“测量规则”编写measure(log),把对第 1 步的记录进行测量的结果保存到/root/svccs/cache/s1-update.measure.json。评分器会用隐藏场景的记录重新调用 measure。 - 编写
write_delete_after(提交 → 删除缓存)和write_delete_before(删除缓存 → 提交)。写入函数是接收(key, value, opts)的生成器。 - 编写
is_newer(cached, new)(仅当缓存为空,或 new 严格更大时为真)、write_cas(提交 → 用收到的版本执行cache_set_if)、read_aside_cas(与 read_aside 相同,但在填充时使用cache_set_if)。把is_newer作为cache_set_if的最后一个参数传入。 - 编写
write_double_delete(删除缓存 → 提交 →("sleep", opts["delay_ms"])→ 删除缓存)。 - 把 5 个场景 × 6 种策略全部运行并测量的结果,以
{시나리오 name: {전략 이름: measure 결과}}(占位符依次为场景 name、策略名称、measure 结果)的形式保存到/root/svccs/cache/results.json。 - 汇总
ttl策略在各场景下的 max_stale_ms,把ttl_ms(各场景的 ttl_ms,整数)、max_stale_ms_by_scenario、exceeds_ttl(max_stale_ms 大于 ttl_ms 的场景名称,按字典序排列的列表)写入/root/svccs/cache/ttl.json。评分器也会在隐藏场景上重新运行你的ttl策略。 - 在
/root/svccs/cache/choice.json中写入stuck(每种策略中 stuck 为真的场景名称列表,已排序)、safe(在任何场景中 stuck 都不成立的策略,已排序)、chosen(safe 中各场景 max_stale_ms 的最大值最小的那个;相同时取 stale_reads 之和较小的;仍相同时按名称顺序)、chosen_worst_ms(那个最大值)。
参考
- 在生成器中写
x = yield ("db_read", key),x 中就会放进操作的结果。读取策略用return返回读到的值。 - 评分器会在隐藏场景上运行你的策略,并把操作记录逐行与基准对照。请严格遵守步骤中写明的顺序和参数——多一个或少一个操作,记录都会不同。
strategies.py由评分器导入。生成结果文件的代码,请放在单独的脚本或if __name__ == "__main__":之下。- 常见错误:把陈旧度从写入开始的时刻算起;stuck 时却在 max_stale_ms 中写下观测到的最大值(它没有上限);把 is_newer 的比较方向弄反。
运行一次模拟器
把 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 的结果,以 {场景 name: {策略名称: measure 结果}} 的形式保存到 /root/svccs/cache/results.json。
import sim.py,调用 sim.run_strategy(场景, 策略模块, 名称),就会直接得到记录 dict。场景名称是文件里的 name。
TTL 能把陈旧度限制到多远
汇总 ttl 策略在各场景下的 max_stale_ms,把 ttl_ms、max_stale_ms_by_scenario、exceeds_ttl 写入 /root/svccs/cache/ttl.json。评分器也会在隐藏场景上重新运行你的 ttl 策略。
TTL 从放进缓存的时刻开始计算。如果旧值在提交很久之后才进来,会怎样?ttl 策略要让 read_aside 把 opts["ttl_ms"] 传给 set 才能工作。
用数字选出策略
用 results.json 把 stuck、safe、chosen、chosen_worst_ms 写入 /root/svccs/cache/choice.json。
只要在某一个场景中 stuck,这种策略的陈旧度就没有上限,所以不是 safe。在某个场景里陈旧读取为 0,并不能成为安全的依据。