Mooncake Store 写入路径:PutStart、TransferSubmitter 和 PutEnd

沿着 PutStart、TransferSubmitter 和 PutEnd,梳理一次 Store 写入如何完成副本分配、数据搬运与可见性提交。

Code walkthroughsMooncake and HiCache internalsLLM servingKV cacheMooncakeDistributed systems

Mooncake Store 的 Put 不是一次 RPC。它是一段控制面和数据面协作的状态机。

这篇文章追踪的问题是:一个对象从调用方进入 Store 后,什么时候只是被 Master 预留,什么时候才真正对读取路径可见。 这个边界决定了 HiCache 写入 Mooncake 后,能不能被后续请求视为一次完整的远端命中,而不能只看 PutStart 是否成功。

源码版本与范围

本文参考 kvcache-ai/Mooncake 的稳定发布标签 v0.3.11.post1(提交 e9c61075720039bcfc5fffd19f847608402be3d0),以 Mooncake Store 的源码路径和调用链为阅读边界。

核心链路:

text
Client::Put / Client::BatchPut
  -> MasterClient::PutStart / BatchPutStart
  -> master 分配 PROCESSING replica
  -> Client::SubmitTransfers
  -> TransferSubmitter 写数据
  -> MasterClient::PutEnd / BatchPutEnd
  -> replica 变成 COMPLETE

对应源码入口:

text
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. 1

    预留空间

    PutStart

    Master 为对象分配副本位置,并将其置于 PROCESSING 状态。

    对象键切片副本描述
  2. 2

    写入数据

    SubmitTransfers

    Client 把切片写入本地或远端 Segment,大块数据不经过 Master。

    TransferSubmitterSegmentWRITE
  3. 3

    提交可见性

    PutEnd

    数据写入成功后,Master 才把副本推进到 COMPLETE。

    COMPLETE读取候选BatchPutEnd
  4. 4

    汇总为缓存语义

    HiCache 后端

    上层还要汇总多个对象的写入结果,判断整个缓存页是否完整。

    K / V 对象辅助内存池每页汇总结果

call path

Mooncake Store Put 的提交点不在 PutStart,而在 PutEnd

PutStart 只让 Master 预留副本;数据写完后,PutEnd 才把副本推进到 COMPLETE。

调用方Store ClientMasterTransferSubmitterSegment
  1. 1

    调用方

    Store Client

    Put / BatchPut(键、切片)

    上层传入对象键和本地缓冲区

  2. 2

    Store Client

    Master

    PutStart / BatchPutStart

    申请副本位置和写入配额

  3. 3

    Master

    Store Client

    PROCESSING 副本

    返回 Segment、偏移量和长度,但还不可读

  4. 4

    Store Client

    TransferSubmitter

    SubmitTransfers

    把切片和副本描述转换成传输任务

  5. 5

    TransferSubmitter

    Segment

    WRITE 数据

    本地内存复制、Transfer Engine 或卸载后端

  6. 6

    Store Client

    Master

    PutEnd / BatchPutEnd

    数据写完后提交可见性

PutStart 只是开始,不是完成

PutStart 的作用是让 Master 为对象分配写入位置。Master 会根据对象键、切片大小、副本配置、租户配额、Segment 状态和分配策略选择副本。

这个阶段结束后,副本通常处于:

text
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 会把每组切片和副本转换成具体的传输任务。

大致路径:

text
对象切片
  -> 副本描述
  -> TransferSubmitter
  -> 本地内存复制 / Transfer Engine WRITE / 文件或卸载路径

如果副本位于同一节点,可能可以直接复制内存;如果副本位于远端内存 Segment,就要交给 Transfer Engine:

text
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

从读语义上看:

text
PutStart 成功:
  说明位置已分配,不说明数据可读

Transfer 成功:
  说明数据写入完成,但 Master 还没提交可见性

PutEnd 成功:
  说明副本可以成为 Get 候选

这个边界非常关键。它让 Store 能够处理部分写入失败和 Client 故障,而不是把半成品暴露给读取方。

PutRevoke 处理失败

如果数据传输失败,Client 应该撤销这次写入。撤销路径会告诉 Master:这些 PROCESSING 副本不应该转成 COMPLETE。

简化状态机:

text
INITIALIZED
  -> PROCESSING
       -> COMPLETE   on PutEnd
       -> REMOVED / FAILED on PutRevoke or cleanup

state flow

Put 路径中的副本状态变化

PROCESSING 是隔离半成品的中间态。只有 PutEnd 成功之后,GetReplicaList 才应该把这个副本返回给读者。

INITIALIZED

对象或副本记录刚创建

PutStart 分配空间

PROCESSING

元数据预留完成,数据写入中

TransferSubmitter 完成 WRITE

COMPLETE

PutEnd 提交后可读

成为 GetReplicaList 候选

REMOVED / FAILED

PutRevoke、清理操作或传输失败

这也是 Store 不是普通 KV 映射的原因。每次写入都要让元数据状态和数据面结果保持一致。

BatchPut 的价值

HiCache 接入 Mooncake 时会产生大量缓存页对象。每个缓存页可能对应多个 Store 对象。如果逐个写入对象,RPC 和调度开销会很高。

BatchPut 可以把多个对象的写入合并成一批:

text
BatchPutStart
  -> 批量分配副本
SubmitTransfers
  -> 批量数据面传输
BatchPutEnd
  -> 批量提交可见性

这和 SGLang 的 batch_set_v1/v2 相对应。HiCache 以页为单位提交,Mooncake Store 以对象为单位批量写入。

分组语义是与版本相关的可选能力

SGLang 的一个逻辑页在 Mooncake 中可能对应:

text
page_key_rank_k
page_key_rank_v

SGLang 的 Mooncake 后端会探测当前安装的 Mooncake 包是否支持 ReplicateConfig.group_ids。如果支持,后端可以给同一个缓存页映射出的多个对象设置相同的分组编号:

text
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”,而靠控制面和数据面的明确分工。

text
PutStart 预留元数据
  -> TransferSubmitter 写入数据
  -> PutEnd 发布可读副本
  -> HiCache 汇总各对象的写入结果,判断缓存页是否完整

这也是 BatchPut 的意义:它不是为了让接口更好看,而是为了让大量缓存页对象的控制面开销和状态提交更接近 HiCache 的真实使用粒度。如果安装的 Mooncake 包支持分组编号,它还可以帮助后续租约和淘汰等逻辑按组理解这些对象;但对上层推理引擎来说,最基本的正确性仍然是:只要一个逻辑页中的必要对象没有全部从 PROCESSING 进入 COMPLETE,这段前缀就不应该被视为完整的远端命中。