Mooncake 确实使用了协程,但它并不是把整个系统改造成“全异步架构”。截至本文分析的主分支提交 3d1665a,协程主要出现在控制面 RPC、连接管理和阻塞任务卸载等 I/O 密集路径;对外接口仍大量保留同步调用形式。
这形成了一个很实用的分层:内部用 C++20 协程组织异步流程,边界处再按需要转换成同步返回值或回调。
技术栈
Mooncake 的协程代码主要建立在两层库之上:
async_simple::coro::Lazy<T>表示一个惰性异步任务;yalantinglibs的coro_rpc和coro_io提供异步 RPC、网络连接与线程池调度。
典型代码形态如下:
async_simple::coro::Lazy<Result> request() {
auto response = co_await client.send_request(...);
co_return response;
}
Lazy<T> 创建后通常不会立刻执行。它需要被另一个协程 co_await,通过 .start(...) 启动,或者由 syncAwait(...) 驱动至完成。
路径一:Mooncake Store 的 Master RPC
mooncake-store/src/master_client.cpp 中的 MasterClient::invoke_rpc 是最清晰的例子。它的外部签名是同步的:
tl::expected<ReturnType, ErrorCode>
MasterClient::invoke_rpc(Args&&... args);
函数内部却先构造 Lazy,再连续等待两个异步阶段:
return async_simple::coro::syncAwait(
[&]() -> async_simple::coro::Lazy<
tl::expected<ReturnType, ErrorCode>> {
auto pending = co_await pool->send_request(
[&](coro_io::client_reuse_hint,
coro_rpc::coro_rpc_client& client) {
return client.send_request<ServiceMethod>(...);
});
if (!pending.has_value()) {
co_return tl::make_unexpected(ErrorCode::RPC_FAIL);
}
auto result = co_await std::move(pending.value());
co_return result->result();
}());
这里有两次 co_await:
- 从客户端池取得可用连接并发出请求;
- 等待 RPC 响应返回。
最外层使用 syncAwait,所以调用者看到的仍是普通阻塞函数。换句话说,协程在这里主要用于简化内部异步控制流,而不是把异步类型传播给所有上层调用者。
同步业务代码
│
▼
invoke_rpc()
│ syncAwait
▼
Lazy 协程
├── co_await 获取连接并发送
└── co_await 等待响应
│
▼
tl::expected 返回给调用者
这种设计减少了接口改造范围,但 syncAwait 所在线程仍会等待结果。因此,不能仅凭内部出现 co_await,就断言整个调用链不会阻塞线程。
路径二:Transfer Engine 同时提供三种调用方式
mooncake-transfer-engine/tent/src/rpc/rpc.cpp 中的 CoroRpcAgent 更完整地展示了协程如何作为统一内核。
核心实现 callCoroutine 返回:
Lazy<std::pair<Status, std::string>>
CoroRpcAgent::callCoroutine(...);
它负责:
- 从连接池租用客户端;
- 必要时通过
co_await client->connect(...)建立连接; - 通过
co_await client->call(...)发起 RPC; - 根据错误类型决定是否清理连接池;
- 返回状态和响应数据。
在这个协程内核之上,Mooncake 暴露了三种接口。
同步接口
auto [status, response] = async_simple::coro::syncAwait(
callCoroutine(server_addr, func_id, request));
适合已有同步调用链,代价是当前线程需要等待。
所有权友好的同步接口
callOwned 接收可移动的 std::string,随后将其移动进协程,避免异步生命周期依赖调用者的 string_view。
这和 Rust 异步代码里先把 &[u8] 转成 Vec<u8> 的动机相似:异步任务可能暂停,任务内部持有的数据必须活得足够久。
回调接口
callCoroutine(server_addr, func_id, request)
.start([callback = std::move(callback)](auto&& result) {
// 把协程结果转换为回调
});
这里没有 syncAwait。.start(...) 启动协程,完成后执行回调,调用线程无需同步等待结果。
因此,CoroRpcAgent 的结构可以概括为:
┌─ syncAwait ─→ call()/callOwned()
callCoroutine ─┤
└─ start(callback) ─→ callAsync()
协程承担一次 RPC 的真实状态机,同步和回调 API 只是不同适配层。这避免了为不同调用风格各写一套连接、错误处理和响应解析逻辑。
路径三:Mooncake-PG 的异步控制面
Mooncake-PG 的 RpcClient 更进一步,直接提供 fire-and-forget 风格的异步调用。
getOrCreateClient 是一个协程:
async_simple::coro::Lazy<std::shared_ptr<coro_rpc::coro_rpc_client>>
RpcClient::getOrCreateClient(...)
它先查询连接缓存;没有可用客户端时创建新客户端,并执行:
auto error = co_await client->connect(address);
异步调用路径随后等待三个阶段:
auto client = co_await getOrCreateClient(...);
auto send_result = co_await std::move(send_operation).coAwaitTry();
auto reply = co_await std::move(receive_operation).coAwaitTry();
这里的 coAwaitTry() 很重要:异常被包装进结果对象,而不是直接穿过协程边界。代码可以分别处理连接、发送和接收错误,并保证回调只收到明确的成功或失败结果。
任务最终通过执行器启动:
std::move(task).via(executor).start([](auto&&) {});
via(executor)指定协程在哪个执行器上推进;start(...)启动惰性任务;- 空完成回调用于结束 detached 风格任务。
这说明协程本身不等于线程。协程保存暂停点和局部状态,真正执行它的仍是底层 executor 线程。
协程也用于卸载阻塞工作
协程最怕的一件事,是在 executor 线程上直接执行耗时阻塞操作。Mooncake 使用 coro_io::post 将部分同步工作提交到线程池:
auto result = co_await coro_io::post([&] {
return blocking_operation();
});
在 Transfer Engine 的 RPC 服务端,处理函数通过 coro_io::post 执行;Mooncake Store 的部分卸载读取和传输处理也使用同样模式。
其运行过程是:
RPC 协程运行
│
├── post 阻塞任务到线程池
│ └── 工作线程执行同步函数
│
├── 当前协程暂停
│
└── 任务完成后恢复协程
这不是让阻塞函数突然变成非阻塞函数,而是把阻塞从承载网络事件循环的线程转移到工作线程,避免卡住其他 RPC 协程。
协程解决了什么问题
让异步状态机保持顺序代码形态
如果完全使用回调,一次 RPC 的连接、发送、接收和错误处理容易形成多层嵌套。co_await 让逻辑仍按执行顺序书写,同时允许等待期间让出执行权。
复用连接和执行线程
客户端池配合协程,可以在请求等待网络时让 executor 处理其他任务,而不需要为每个并发请求创建一个线程。
统一多种 API 风格
同一个 Lazy 内核可以通过:
co_await组合到另一个协程;syncAwait暴露同步接口;.start(callback)暴露回调接口。
这也是 Mooncake 中最值得借鉴的设计点。
使用时需要注意的边界
syncAwait 仍然会阻塞调用线程
它只是驱动协程并同步取得结果,不会自动把同步调用者变成非阻塞调用者。判断性能模型时,必须继续向外追踪 syncAwait 运行在哪类线程上。
Lazy 是惰性的
只创建 Lazy 而不 co_await、.start() 或 syncAwait(),任务通常不会执行。返回 Lazy 的函数更像生成一个“异步执行计划”。
参数生命周期必须覆盖暂停期
协程可能在 co_await 处暂停,因此引用或 string_view 不能指向提前销毁的数据。Mooncake 的 callOwned 通过把字符串所有权移进协程来处理这一问题。
阻塞任务要主动隔离
协程只能在遇到可挂起操作时让出线程。如果在协程体里直接执行长时间同步 I/O 或 CPU 重任务,仍然会堵塞 executor。coro_io::post 正是为这一边界服务。
总结
Mooncake 对协程的使用不是“所有函数都异步化”,而是围绕 I/O 路径建立了一套分层方案:
- 使用
Lazy<T>、co_await和co_return编写异步核心逻辑; - 使用
coro_rpc管理连接和 RPC 请求; - 使用
syncAwait兼容同步业务接口; - 使用
.start(callback)提供真正的异步回调入口; - 使用
coro_io::post将阻塞工作移出协程执行线程。
理解 Mooncake 的关键不是统计源码里有多少个 co_await,而是识别三种边界:协程与网络事件循环的边界、协程与同步调用者的边界,以及协程与阻塞线程池的边界。