Mooncake Store 读取路径:GetReplicaList、租约和本地热缓存
沿着 GetReplicaList、副本选择和本地热缓存,梳理一次 Store 读取如何从元数据查询进入数据传输。
Mooncake Store 的 Get 不是“向 Master 要一个值”。Master 只返回可读副本和租约,数据由 Client 自己搬运。
核心链路:
Client::Get / Client::BatchGet
-> Query / GetReplicaList
-> Master 返回 COMPLETE 副本和租约
-> Client 选择优先副本
-> 本地热缓存或 TransferSubmitter
-> 数据写入调用方切片
call path
Mooncake Store Get:Master 返回可读位置,Client 自己搬数据
Get 的数据不经过 Master。Master 给出 COMPLETE 副本和租约;真正的数据由本地热缓存、内存复制或 Transfer Engine 写入调用方切片。
- 1
调用方 / HiCache
Store Client
Get / BatchGet(键、目标切片)
目标缓冲区通常是主机 KV 缓存页指针
- 2
Store Client
Master
Query / GetReplicaList
查询对象元数据和可读副本
- 3
Master
Store Client
COMPLETE 副本 + 租约
元数据有时间边界,不能无限复用
- 4
Store Client
本地热缓存
尝试读取本地热缓存
命中则少一次远端读取
- 5
Store Client
Transfer Engine
READ Segment
未命中本地缓存时,从副本所在的 Segment 读取
- 6
Transfer Engine
调用方切片
填充本地缓冲区
Mooncake 写到上层传入的目标地址
Get 先查询元数据
Get 的第一步是向 Master 查询对象元数据。Master 需要判断:
- 对象键是否存在
- 是否有处于
COMPLETE状态的副本 - 副本所在的 Segment 是否仍然可用
- 是否可以发放租约
- 租户和分组语义是否影响本次查询
返回给 Client 的不是对象数据,而是 QueryResult 一类的元数据:
副本描述
租约有效期
对象元数据
Client 拿到元数据以后,才进入数据面。
为什么只读取 COMPLETE 状态的副本
Put 路径中的副本会经历 PROCESSING 状态。Get 不能读取这种副本,因为数据可能还没有写完。
正确的读路径应该只把 COMPLETE 副本当作候选:
PROCESSING:
写入中,不可读
COMPLETE:
PutEnd 已提交,可读
REMOVED / FAILED:
不可读
这是 Store 保证读取正确性的底线。如果读到了 PROCESSING 副本,就可能拿到只写了一半的数据。
租约限制元数据的有效期
Client 拿到副本列表以后,不能无限期复用,因为 Master 侧的状态可能变化:
- Segment 被卸载
- 副本被清理
- 对象被更新或删除
- Client 与 Master 看到的状态发生变化
租约为这次元数据使用划定了时间边界。Client 应该在租约有效期内使用副本描述符,过期后重新查询。
因此 Get 的正确性不是“元数据查到一次就永久有效”,而是:
元数据查询成功
+ 副本状态为 COMPLETE
+ 租约有效
+ 传输成功
首选副本与数据本地性
Client 拿到多个副本后,需要从中选择一个。选择时会考虑:
- 是否在同一节点
- 是否能走本地 memcpy
- 是否已经进入本地热缓存
- Segment 的协议是否适合当前 Client
- 是否配置了优先选择同节点副本
优先选择本地副本可以减少网络传输。对于 KV 缓存这类大对象,本地性带来的差异可能远大于元数据查询成本。
本地热缓存只负责加速读取,不代表全局状态
本地热缓存保存常用对象的数据,可以减少重复远端读取,但不能代替 Master 元数据。
更准确地说:
Master 元数据:
决定哪些副本可读
本地热缓存:
加速已经判定可读的数据访问
本地热缓存也要借助写入令牌或版本号,防止异步 Put 把过期内容写回缓存。它只是本地优化层,不是分布式一致性的依据。
数据面读取如何执行
如果最终选择的是远端内存副本,Client 会把副本描述转换成 Transfer Engine READ 请求:
TransferRequest
opcode = READ
source = 本地目标地址
target_id = 远端 Segment ID
target_offset = 副本偏移量
length = 切片大小
这里的 source 字段在 Transfer Engine 看来是本地缓冲区地址。READ 表示从远端 Segment 读取到本地地址。
如果是本地副本,可能直接复制内存;如果是文件或卸载副本,则交给对应后端处理。
BatchGet 与 HiCache
HiCache 从 Mooncake 预取时,会批量读取许多缓存页对象。BatchGet 的价值和 BatchPut 类似:减少单个对象的元数据与传输开销。
Mooncake 的 Python 零拷贝 API 最后会暴露为:
batch_get_into(keys, ptrs, sizes)
batch_get_into_multi_buffers(keys, ptrs, sizes)
SGLang 后端把主机内存池缓存页的目标指针传给 Mooncake。Mooncake Store 把对象数据直接写入这些主机缓冲区,随后 HiCache 再把主机缓存页回载到 GPU。
读取失败如何影响上层
Get 失败可以发生在几层:
- Master 查不到对象键
- 没有 COMPLETE 副本
- 租约已过期
- 选中的副本所对应的 Segment 不可用
- Transfer Engine READ 失败
- 批次中某个 K/V 对象读取失败
对于 SGLang HiCache,这些对象读取结果最终要按缓存页汇总。如果某个缓存页的 K 对象成功而 V 对象失败,这一页就不能算命中。
因此,后端需要按缓存页汇总对象状态,再计算连续命中的页数。
从 HiCache 视角看读取
HiCache 读取 Mooncake 时,目标不是把对象放进 Python,而是把远端 KV 缓存页直接填回主机 KV 池。batch_get_into 中的指针指向主机内存池缓存页;Mooncake Store 负责把远端副本的字节写到这些地址;之后 HiCache 还要执行从主机内存到 GPU 的回载。
因此,一次远端命中至少跨过三道边界:Master 元数据必须返回可读副本,数据面必须把对象读回主机缓冲区,HiCache 必须把主机缓存页提交回 GPU KV 池。前两步成功只能说明 Mooncake Store 工作正常,不能自动说明注意力计算已经能够复用这段 KV。
对象读取结果和缓存页命中结果也要分清。Mooncake 只能告诉你某个对象读取成功;SGLang 还要汇总 K/V 对象、按注意力头拆分的对象和辅助内存池对象,判断整个缓存页是否读取成功。只要这组对象中有一个失败,这一页就应该按未命中处理。