Mooncake Store 写入路径:PutStart、TransferSubmitter 和 PutEnd
沿着 PutStart、TransferSubmitter 和 PutEnd,梳理一次 Store 写入如何完成副本分配、数据搬运与可见性提交。
Mooncake Store 的 Put 不是一次 RPC。它是一段控制面和数据面协作的状态机。
这篇文章追踪的问题是:一个对象从调用方进入 Store 后,什么时候只是被 Master 预留,什么时候才真正对读取路径可见。 这个边界决定了 HiCache 写入 Mooncake 后,能不能被后续请求视为一次完整的远端命中,而不能只看 PutStart 是否成功。
源码版本与范围
本文参考 kvcache-ai/Mooncake 的稳定发布标签 v0.3.11.post1(提交 e9c61075720039bcfc5fffd19f847608402be3d0),以 Mooncake Store 的源码路径和调用链为阅读边界。
核心链路:
Client::Put / Client::BatchPut
-> MasterClient::PutStart / BatchPutStart
-> master 分配 PROCESSING replica
-> Client::SubmitTransfers
-> TransferSubmitter 写数据
-> MasterClient::PutEnd / BatchPutEnd
-> replica 变成 COMPLETE
对应源码入口:
mooncake-store/include/client_service.h
mooncake-store/src/client_service.cpp
mooncake-store/src/master_service.cpp
mooncake-store/include/transfer_task.h
mooncake-store/src/transfer_task.cpp
本文只分析 Store Put / BatchPut 的元数据和数据面边界;不覆盖完整的副本放置策略和所有传输后端,也不提供页大小或批次大小的基准测试结论。
flow
写入路径总览:预留元数据、写入数据、提交可见性
Put 的关键不是 API 名字,而是哪一步只预留、哪一步搬数据、哪一步让 Get 能读。
- 1
预留空间
PutStart
Master 为对象分配副本位置,并将其置于 PROCESSING 状态。
对象键切片副本描述 - 2
写入数据
SubmitTransfers
Client 把切片写入本地或远端 Segment,大块数据不经过 Master。
TransferSubmitterSegmentWRITE - 3
提交可见性
PutEnd
数据写入成功后,Master 才把副本推进到 COMPLETE。
COMPLETE读取候选BatchPutEnd - 4
汇总为缓存语义
HiCache 后端
上层还要汇总多个对象的写入结果,判断整个缓存页是否完整。
K / V 对象辅助内存池每页汇总结果
call path
Mooncake Store Put 的提交点不在 PutStart,而在 PutEnd
PutStart 只让 Master 预留副本;数据写完后,PutEnd 才把副本推进到 COMPLETE。
- 1
调用方
Store Client
Put / BatchPut(键、切片)
上层传入对象键和本地缓冲区
- 2
Store Client
Master
PutStart / BatchPutStart
申请副本位置和写入配额
- 3
Master
Store Client
PROCESSING 副本
返回 Segment、偏移量和长度,但还不可读
- 4
Store Client
TransferSubmitter
SubmitTransfers
把切片和副本描述转换成传输任务
- 5
TransferSubmitter
Segment
WRITE 数据
本地内存复制、Transfer Engine 或卸载后端
- 6
Store Client
Master
PutEnd / BatchPutEnd
数据写完后提交可见性
PutStart 只是开始,不是完成
PutStart 的作用是让 Master 为对象分配写入位置。Master 会根据对象键、切片大小、副本配置、租户配额、Segment 状态和分配策略选择副本。
这个阶段结束后,副本通常处于:
PROCESSING
这表示“空间已经被元数据预留,但数据面还没有确认写完”。它不能被正常 Get 当作可读副本。
如果把 PutStart 理解成 Put 成功,后面的读路径就会错。Store 的写入提交点在 PutEnd。
为什么需要 PROCESSING
Store 的大块数据不经过 Master。PutStart 之后,Client 需要自己把数据切片写到对应副本所在的 Segment。
这个过程可能失败:
- Client 本地缓冲区不可读
- Transfer Engine 没有注册对应内存
- 远端 Segment 打不开
- RDMA、TCP 或 NVLink 传输出错
- 只写入了部分副本
- Client 在 PutEnd 前崩溃
PROCESSING 为系统提供了一个中间状态:元数据中已经有一批候选副本,但它们还不能对读取方可见。
TransferSubmitter 根据元数据生成传输任务
PutStart 返回副本描述后,Client 会把每组切片和副本转换成具体的传输任务。
大致路径:
对象切片
-> 副本描述
-> TransferSubmitter
-> 本地内存复制 / Transfer Engine WRITE / 文件或卸载路径
如果副本位于同一节点,可能可以直接复制内存;如果副本位于远端内存 Segment,就要交给 Transfer Engine:
TransferRequest
opcode = WRITE
source = 本地切片地址
target_id = 远端 Segment ID
target_offset = 副本偏移量
length = 切片大小
Store 本身不关心底层使用 RDMA 还是 TCP。TransferSubmitter 只需要把 Store 副本描述转换成 Transfer Engine 能执行的请求。
PutEnd 是可见性边界
所有数据写入成功以后,Client 才调用 PutEnd。Master 在 PutEnd 中把对应副本从 PROCESSING 推进到 COMPLETE。
从读语义上看:
PutStart 成功:
说明位置已分配,不说明数据可读
Transfer 成功:
说明数据写入完成,但 Master 还没提交可见性
PutEnd 成功:
说明副本可以成为 Get 候选
这个边界非常关键。它让 Store 能够处理部分写入失败和 Client 故障,而不是把半成品暴露给读取方。
PutRevoke 处理失败
如果数据传输失败,Client 应该撤销这次写入。撤销路径会告诉 Master:这些 PROCESSING 副本不应该转成 COMPLETE。
简化状态机:
INITIALIZED
-> PROCESSING
-> COMPLETE on PutEnd
-> REMOVED / FAILED on PutRevoke or cleanup
state flow
Put 路径中的副本状态变化
PROCESSING 是隔离半成品的中间态。只有 PutEnd 成功之后,GetReplicaList 才应该把这个副本返回给读者。
INITIALIZED
对象或副本记录刚创建
PROCESSING
元数据预留完成,数据写入中
COMPLETE
PutEnd 提交后可读
REMOVED / FAILED
PutRevoke、清理操作或传输失败
这也是 Store 不是普通 KV 映射的原因。每次写入都要让元数据状态和数据面结果保持一致。
BatchPut 的价值
HiCache 接入 Mooncake 时会产生大量缓存页对象。每个缓存页可能对应多个 Store 对象。如果逐个写入对象,RPC 和调度开销会很高。
BatchPut 可以把多个对象的写入合并成一批:
BatchPutStart
-> 批量分配副本
SubmitTransfers
-> 批量数据面传输
BatchPutEnd
-> 批量提交可见性
这和 SGLang 的 batch_set_v1/v2 相对应。HiCache 以页为单位提交,Mooncake Store 以对象为单位批量写入。
分组语义是与版本相关的可选能力
SGLang 的一个逻辑页在 Mooncake 中可能对应:
page_key_rank_k
page_key_rank_v
SGLang 的 Mooncake 后端会探测当前安装的 Mooncake 包是否支持 ReplicateConfig.group_ids。如果支持,后端可以给同一个缓存页映射出的多个对象设置相同的分组编号:
sglang-hicache:<logical_page_key>
这样 Store 看到多个对象键时,仍然可以知道它们属于同一个逻辑缓存单元。后续元数据路由、租约续期和淘汰等行为才有机会按组优化。
但这不是本文参考的 Mooncake 稳定标签 v0.3.11.post1 中的基础字段:该版本的 ReplicateConfig 没有 group_ids。因此,在这个版本边界下,HiCache 仍然按对象批量写入,再按缓存页汇总结果;分组信息只能作为适配新版本包的可选优化,而不是保证写入正确性的前提。
写路径的性能边界
Put 路径的成本来自三部分:
- 元数据 RPC:PutStart/PutEnd
- 数据传输:本地内存复制或 Transfer Engine
- 内存注册和打开 Segment 的前置成本
因此性能敏感场景通常需要重点检查:
- 批量写入,而不是单对象 RPC。
- 预先注册主机缓冲区,减少写入时的注册开销。
- 使用稳定的页粒度,避免产生大量小对象。
- 数据传输和控制面分离,大块数据不经过 Master。
如果缓存页太小,元数据和传输开销可能超过复用收益;如果缓存页太大,前缀复用粒度又会变粗。这是一项设计取舍,不是本文给出的基准测试结论;真实收益仍要结合负载、页大小、批次大小、协议和设备拓扑验证。
从 HiCache 视角看写入
HiCache 写入 Mooncake 时,真正要完成的不是“调用一次 put”,而是让一个逻辑页的所有对象都变得可读。对于 MHA,K 对象和 V 对象必须一起成功;对于混合内存池,辅助状态也要进入同一逻辑边界。
所以写入路径要分两层理解:Store 负责把每个对象的副本从 PROCESSING 推进到 COMPLETE;HiCache 后端负责汇总同一缓存页中各对象的写入结果。只要其中一个对象写入失败,这个缓存页就不能被后续请求视为完整的远端命中。
这条路径能证明什么
evidence boundary
Put 路径能说明什么
Store Put 能证明对象的可见性边界;完整 KV 缓存复用还需要上层页语义和读回链路闭环。
能够确认
- PutStart 之后,副本只是 PROCESSING 状态,不能当成可读副本。
- TransferSubmitter 负责把 Store 副本描述转换成数据写入任务。
- PutEnd / BatchPutEnd 是 Store 层可读性的提交点。
- BatchPut 更接近 HiCache 大量缓存页对象的使用粒度;分组编号只能在 Mooncake 包支持时作为可选优化。
不能单独证明
- 不能证明 SGLang 的后续请求一定会命中并复用这段前缀。
- 不能证明 K/V 对象和辅助内存池已经能按逻辑页被完整读回并复用。
- 不能证明 Get、租约续期、副本淘汰或 `load_back` 回载流程全部正常。
- 不能替代端到端远端命中验证和真实负载下的基准测试。
结论:PutEnd 才是可读性的提交点
这条写路径的关键结论是:Mooncake Store 的写入正确性不靠一次“大 Put RPC”,而靠控制面和数据面的明确分工。
PutStart 预留元数据
-> TransferSubmitter 写入数据
-> PutEnd 发布可读副本
-> HiCache 汇总各对象的写入结果,判断缓存页是否完整
这也是 BatchPut 的意义:它不是为了让接口更好看,而是为了让大量缓存页对象的控制面开销和状态提交更接近 HiCache 的真实使用粒度。如果安装的 Mooncake 包支持分组编号,它还可以帮助后续租约和淘汰等逻辑按组理解这些对象;但对上层推理引擎来说,最基本的正确性仍然是:只要一个逻辑页中的必要对象没有全部从 PROCESSING 进入 COMPLETE,这段前缀就不应该被视为完整的远端命中。