Mooncake Classic vs TENT Engine
导言
看到“应用必须持有 Transport*”时,我真正卡住的是:持有到底是什么意思?为什么直接保存一个后端指针就能工作,统一的 TransferEngine 反而走不通?
把这个词拆开后,Classic 与 TENT 的差异就不再只是两套接口。Classic 的公共 Batch 先于后端选择而创建,后端私有状态缺少稳定的安放位置;TENT 则在选定后端后,为每个 transport 创建独立 SubBatch,再由统一运行时协调提交、状态与回收。
因此,判断一个 Engine 是否容易扩展多个后端,不能只数有多少个 Transport 子类。真正要问的是:后端的私有状态放在哪里,由谁创建,又由谁保证它完成整个异步生命周期。
本文承接 Mooncake Classic NVMeoF Transport 与 Mooncake TENT GDS。前者展示旧后端怎样直接调用 cuFile,后者展示 TENT 怎样管理统一运行时;这里不再重复两篇文章的接口细节,只比较它们的对象边界。
Classic 不是 C++ class
本文的 Classic 指 Mooncake 经典 Transfer Engine 路径,不是 C++ 的 class 关键字。问题也不是“面向对象继承一定难扩展”,而是这套公共 Batch 与后端私有生命周期之间存在错位。
“持有”究竟是什么¶
这里的“持有 Transport*”不是说应用拥有或负责销毁 Transport 对象,而是说:应用保存 installTransport 返回的后端指针,并绕过统一 Engine,直接在这个对象上完成 Batch 的分配、提交、查询与释放。
Transport* xport = engine.installTransport("nvmeof", args);
auto batch_id = xport->allocateBatchID(1);
xport->submitTransfer(batch_id, requests);
xport->getTransferStatus(batch_id, 0, status);
xport->freeBatchID(batch_id);
“保存指针”本身不是关键。关键是四个操作始终落在同一个 NVMeoFTransport 对象上:
- 分配时创建 NVMeoF 私有 descriptor;
- 提交时向 descriptor 填入 cuFile 参数;
- 查询时用 descriptor 中的 slice 映射聚合事件;
- 释放时回收 descriptor、BatchHandle 与私有上下文。
因此,更准确的说法是:这个 Batch 从创建到回收都由 NVMeoF 后端直接管理。 “持有后端指针”只是调用者进入这条专用生命周期的手段。
Classic 为什么直接调用能跑¶
可以先把 Transfer Engine 想成物流平台,把不同 transport 想成不同承运商。
公共 BatchDesc 是平台统一的订单,记录容量和 task 列表。NVMeoF 后端还需要一只专用文件袋,保存 cuFile descriptor、task 到 slice 的映射以及异步完成事件。代码里的这只“文件袋”就是 NVMeoFBatchDesc。
直接调用 xport->allocateBatchID 时,执行的是 NVMeoFTransport::allocateBatchID。它先创建公共 Batch,再把私有对象放进 context:
auto* nvme_batch = new NVMeoFBatchDesc();
auto batch_id = Transport::allocateBatchID(batch_size);
auto& batch = *reinterpret_cast<BatchDesc*>(batch_id);
nvme_batch->desc_idx_ = desc_pool_->allocCUfileDesc(batch_size);
batch.context = nvme_batch;
后面的接口都能沿 batch.context 找回同一个 NVMeoFBatchDesc。cuFile handle、切片区间、事件缓存和资源回收因此连成了闭环。
这条专用路径能够工作,但代价也很直接:调用者必须知道自己正在使用 NVMeoF,并牢记这个 Batch 只能继续交给同一个后端。
统一 Engine 为什么反而断了¶
如果 Batch 由 engine->allocateBatchID 创建,执行者变成 MultiTransport。此时运行时还没有为 NVMeoF 建立私有上下文:
等到 engine->submitTransfer 解析请求后,它确实可以根据 Segment protocol 选中 NVMeoFTransport。但 transport 选择解决的只是“任务应该交给谁”,没有补上“这个后端的批次状态放在哪里”。
engine->submitTransfer
→ MultiTransport 选中 NVMeoFTransport
→ 调用 NVMeoFTransport::submitTransferTask
→ NotImplemented
旧实现只在专用的 NVMeoFTransport::allocateBatchID 中创建 descriptor;通用路径不会调用这个函数,而 submitTransferTask 又没有实现等价的私有批次分配、状态映射和回收逻辑,所以只能返回 NotImplemented。
选中后端不等于接入运行时
installTransport("nvmeof") 成功、selector 能找到 NVMeoF、专用测试能够读写,这三件事都不能证明通用 Engine 路径已经接通。真正的集成必须同时覆盖分配、提交、状态推进、失败清理和释放。
Classic 难扩展在哪里¶
Classic 并不是不能装载多个 transport。困难出现在新后端需要自己的批次级私有状态时:公共 Engine 与后端必须临时商量“这份状态由谁创建、挂在哪里、什么时候可以释放”,但 Classic 没有把这套协商变成统一契约。
Batch 创建得太早¶
公共 Batch 在 transport 选择前已经存在。后端真正收到 task 时,Batch 的对象形态和所有权已经确定。
对只需逐 task 提交、几乎没有批次私有状态的后端,这未必构成问题。但 NVMeoF、GDS 这类异步批量后端还要维护:
- 底层 BatchHandle;
- 一个 task 展开后的多个物理 slice;
- slice completion 到公共 task 的映射;
- 取消后仍被设备引用的参数;
- 只有物理终态后才能回收的资源。
这些对象不能在提交函数的栈上临时创建,也不能在 API 返回错误后立即删除。它们需要一个与异步 Batch 同寿命的私有容器。
单个 context 难表达多个后端¶
Classic 的公共 BatchDesc 只有一个泛型 context。如果整个 Batch 永远只交给一个后端,这个槽位尚可保存私有对象;一旦公共 Batch 中的 task 需要按 transport 分组,问题就出现了:
一个 context 无法自然表达多组彼此独立的 handle、队列和完成事件。理论上可以再增加 map、外部 side table 或新的特殊分支,但每接一个有状态后端,公共层就更了解一种后端细节,扩展成本会继续向中心聚集。
后端知识泄漏给调用者¶
专用 xport 路径把本该由运行时处理的问题交给了业务代码:
- 应用要提前知道应该选择
nvmeof; - 应用要保存后端指针;
- Batch 必须由这个后端分配,也必须由它释放;
- 误用
engine->freeBatchID可能绕开私有资源回收; - 更换 transport 会改变业务代码的调用路径。
这使后端从“可替换的执行器”变成了“业务必须显式理解的对象”。后端数量增加后,统一 Engine 名义上仍在,真正的调度边界却散落到了调用者手中。
特殊路径会逐渐增多¶
如果每个新后端都通过专用分配接口解决自己的状态问题,系统最终容易形成多套彼此相似但不能互换的调用链:
这并不表示 Classic 无法继续增加代码,而是说每增加一个后端,都可能同时增加一套调用约定和生命周期例外。类数量在增长,统一抽象却没有同步变强。
TENT 改变了什么¶
TENT 没有要求所有后端把私有状态塞进同一个公共 Batch,也没有让应用直接保存 GdsTransport*。它在公共 Batch 与具体 transport 之间增加了明确的 SubBatch 层:
应用提交公共 Request
→ TENT 解析 Segment 与本地内存
→ selector 选择 transport
→ 按 transport 给 task 分组
→ transport->allocateSubBatch(...)
→ transport->submitTransferTasks(...)
→ transport->getTransferStatus(...)
→ transport->freeSubBatch(...)
对象关系也随之改变:
TENT 公共 Batch
├── 公共 TaskInfo
├── GDS SubBatch
│ ├── CUfile BatchHandle
│ ├── IOParamRange
│ └── completion cache
└── IOUring SubBatch
└── IOUring 私有队列与状态
每个 transport 可以定义自己的 SubBatch 类型,公共运行时只负责在正确时间调用统一生命周期接口。这样一来:
- 选择与分配顺序正确:先知道使用哪个后端,再让该后端创建私有状态;
- 私有状态彼此隔离:GDS 与 IOUring 不争用一个
context; - 调用者保持统一:应用只面对 Engine,不直接保存某个 transport 指针;
- 生命周期成为契约:分配、提交、查询与释放不再是后端的隐藏约定;
- 一个公共 Batch 可以容纳多个执行后端:每组 task 进入自己的 SubBatch,状态再聚合回公共 task。
TENT 的关键变化不是把 Transport 类改了一个名字,而是把“后端私有批次”提升成运行时的一等对象。
两套 Engine 放在一起看¶
| 维度 | Classic Engine | TENT Engine |
|---|---|---|
| 公共 Batch 创建时机 | 通常先创建,再在提交时选择 transport | 先准备请求和选择 transport,再创建私有 SubBatch |
| 后端私有状态 | 依赖公共 context、外部映射或特殊路径 |
每个 transport 拥有独立 SubBatch |
| 应用是否知道后端 | 专用路径需要保存 Transport* |
应用只调用统一 Engine |
| 多后端分组 | 后端私有批次缺少统一落点 | 公共 Batch 下可挂多个 transport SubBatch |
| 状态聚合 | 各后端自行适配公共 task,容易形成例外 | runtime 根据 SubBatch 与 task 映射统一推进 |
| 资源回收 | 调用者可能必须找回原后端 | runtime 协调 freeSubBatch |
| 新后端接入重点 | 容易先实现数据搬运,再补生命周期 | 接口直接要求完整异步生命周期 |
| 典型失败 | selector 选中后端,但提交接口 NotImplemented |
transport 未实现契约时不会形成完整可用路径 |
Classic 的优势是直接:后端掌握自己的对象,专用测试可以很快打通底层 I/O。TENT 付出的代价则是运行时更复杂,需要维护 selector、公共 task、SubBatch 和状态映射。
因此,这不是“旧架构一无是处、新架构天然正确”的比较。二者优化的阶段不同:Classic 更容易先证明某个 transport 能搬数据;TENT 更重视多个 transport 怎样长期共存、替换和回收。
新后端应先回答什么¶
接入一个新 NDS transport 时,可以先不看具体 SDK,而是检查下面五个问题:
- 谁选择后端:业务代码硬编码,还是 runtime 根据 Segment、内存与 capability 选择?
- 私有状态放在哪里:公共 Batch 的临时槽位,还是后端自己的稳定 SubBatch?
- 谁管理生命周期:调用者记住专用顺序,还是 Engine 统一调用分配、提交、查询与释放?
- 怎样与其他后端共存:一个 Batch 是否能够按 transport 分组,并保留多套私有状态?
- 失败后何时安全释放:API 返回错误时,设备是否仍可能引用参数与用户 buffer?
如果前三个问题的答案仍是“让业务代码拿着某个 backend 指针自行处理”,那么新增的只是一个能单独运行的后端,还没有成为统一运行时中的可替换能力。
最终判断¶
回到开头,“持有 Transport*”真正暴露的不是一个 C++ 指针问题,而是一条架构边界:Classic 的 NVMeoF 私有生命周期只存在于后端专用调用链中,没有成为公共 Engine 能够创建和管理的对象。
这解释了为什么专用测试能够成功,MultiTransport 也能选中 nvmeof,但通用提交仍然返回 NotImplemented。选择后端与管理后端生命周期,是两件不同的事。
TENT 用 SubBatch 把这两件事重新接了起来。现在可以确认的是,这种对象边界更适合多个有状态异步后端共存;但它并不消除后端实现本身的复杂度,每个 transport 仍必须正确处理切片、完成事件、取消和安全回收。
因此,新后端接入时最值得复制的不是某个 submit 函数,而是这条完整关系:公共请求由 runtime 选择和分组,私有状态由 transport 创建,最终状态再由 runtime 聚合和回收。
参考文献¶
- Mooncake Classic NVMeoF Transport
- Mooncake TENT GDS
- Mooncake TENT Request Path
- Mooncake classic MultiTransport
- Mooncake classic NVMeoF transport
- Mooncake TENT Transport interface
- Mooncake TENT Transfer Engine