TT Lab
开始
学习 学习路径 课程

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

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

在 TT Lab 中继续学习

目标

在确定性模拟器中,把六种缓存写入、读取策略放进同样的交错顺序里,在每个场景下测出陈旧读取数和最大陈旧度(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

步骤

  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.py 中按“测量规则”编写 measure(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)。把 is_newer 作为 cache_set_if 的最后一个参数传入。
  5. 编写 write_double_delete(删除缓存 → 提交 → ("sleep", opts["delay_ms"]) → 删除缓存)。
  6. 把 5 个场景 × 6 种策略全部运行并测量的结果,以 {시나리오 name: {전략 이름: measure 결과}}(占位符依次为场景 name、策略名称、measure 结果)的形式保存到 /root/svccs/cache/results.json。
  7. 汇总 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 策略。
  8. 在 /root/svccs/cache/choice.json 中写入 stuck(每种策略中 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 的结果,以 {场景 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,并不能成为安全的依据。