Mooncake Store 状态管理:副本生命周期、租约和本地热缓存
沿着副本状态机、元数据租约和本地热缓存的校验令牌,说明 Mooncake Store 如何维护读写可见性并防止旧写覆盖。
Mooncake Store 的主路径是 Put/Get,但真正容易出问题的是状态管理。大对象缓存系统最怕的不是“没有数据”,而是“读到一份看起来存在但其实不该读的数据”。
这篇只看三个状态边界:
副本生命周期
元数据租约
本地热缓存的世代号或令牌
副本的生命周期
副本不是一创建就可读,而是要经过一套状态机:
INITIALIZED
-> PROCESSING
-> COMPLETE
-> REMOVED / FAILED
state flow
副本状态决定读路径能不能使用这份数据
Transfer Engine 传输成功只是数据传输结果;Store 读路径只应使用已经提交为 COMPLETE 的副本。
INITIALIZED
Master 分配元数据位置,此时尚未写入数据
PROCESSING
Client 正在写入数据,Get 不能读取
COMPLETE
元数据已经提交,可以进入 GetReplicaList
REMOVED / FAILED
不再参与全局可读事实
写入时,Master 先为对象分配副本。此时副本只有位置,数据尚未写完。Client 完成数据面传输后,PutEnd 才把副本提交为 COMPLETE。
读路径只应该使用处于 COMPLETE 状态的副本。这条规则看起来简单,却是 Store 保证读取正确性的核心。
PROCESSING 的意义
如果没有 PROCESSING 状态,PutStart 后元数据中会立刻出现新副本,Get 就可能读到尚未写完的数据。
PROCESSING 把写入拆成两步:
控制面:
我预留了一个位置
数据面:
我把数据写过去了
提交点:
我确认这份副本可读
这个中间状态允许 Store 处理部分失败。例如批量写入 100 个对象,其中 3 个远端传输失败,Master 就不应该把这 3 个对象也标记为 COMPLETE。
租约的作用
Master 返回副本列表时,会给 Client 一份租约。租约表示这份元数据视图只能在有限时间内使用。
租约不是为了提升性能而添加的装饰字段,它限制了 Client 复用旧元数据的时间。
没有租约,Client 可能一直沿用很早以前查到的副本地址,即使 Segment 已经卸载、副本已经删除或 Master 已经完成故障切换。这类问题很难复现,在分布式缓存里却非常危险。
因此读路径应该理解成:
GetReplicaList
-> COMPLETE 副本和租约
-> Client 在租约有效期内执行数据面读取
-> 租约过期后重新查询
本地热缓存的边界
本地热缓存是 Client 侧的加速层,可以把常用对象保留在本地内存中,减少远端读取。
但它不能绕过 Master 元数据。本地热缓存的准确定位是:
全局可读性:
Master + 副本状态 + 租约
本地加速:
热缓存
如果一个对象仍在本地热缓存中,但 Master 已经认为它不可读,就不能仅凭本地缓存命中直接返回。热缓存只能加速已经确认可读的数据路径。
写入令牌防止旧写覆盖新写
本地热缓存里常见的风险是异步写入乱序。
想象两个 put 几乎同时发生:
Put A 获取数据块,准备写入 key=x
Put B 随后也写入 key=x
Put B 先完成
Put A 后完成
如果没有写入令牌或版本号保护,A 可能把旧内容覆盖到热缓存中,导致后续读取命中过期数据。
Mooncake 的热缓存会使用类似写入令牌的机制:写入前先获取令牌,真正放入缓存时再检查它是否仍然有效。这样,过期写入就不会覆盖新状态。
缓存淘汰与可见性
Store 中的对象可能被淘汰或移除。对于远端缓存,淘汰不只是释放内存,还会影响元数据可见性。
应该区分:
数据内容:
某段 Segment 上的内容
元数据:
对象 -> 副本的可读关系
本地缓存:
Client 侧的加速副本
只有元数据承认的 COMPLETE 副本才是全局读取路径的一部分。字节仍然留在某块内存中,并不代表读取方可以使用。
和 HiCache 的关系
SGLang HiCache 只需要知道某个逻辑页是否可用。Mooncake Store 则要在内部保证这一页对应的所有对象都满足可读条件。
例如,一个 MHA 页对应 K/V 两个对象:
page_k:COMPLETE + 租约有效 + 传输成功
page_v:COMPLETE + 租约有效 + 传输成功
只有两个对象都成功,HiCache 才能把这个逻辑页视为命中。
这就是 Mooncake 后端需要按缓存页汇总各对象结果的原因。上层不应该看到“半页命中”。
排障时看什么
如果怀疑 Mooncake Store 读到了旧数据或出现异常未命中,可以先按状态分层排查:
- PutStart 是否成功但 PutEnd 没有成功。
- 副本是否仍处于 PROCESSING。
- GetReplicaList 返回的副本是否为 COMPLETE。
- 租约是否过期。
- 本地热缓存是否命中过期内容。
- K/V 对象是否只有部分存在。
- 如果使用的 Mooncake 包支持
group_ids,再看分组语义是否让一组对象拥有一致的管理边界。
不要一上来只查传输层。传输成功只能证明数据搬过了,不能证明这份数据应该被读取。
排查可见性时看哪里
PROCESSING和COMPLETE的差别为什么是可见性边界?- 为什么不能省略租约?
- 本地热缓存为什么不能绕过 Master 元数据?
- 写入令牌防止的是哪类乱序?
- 为什么 HiCache 缓存页命中要求多个 Store 对象都成功?