Mooncake 确实使用了协程,但它并不是把整个系统改造成“全异步架构”。截至本文分析的主分支提交 3d1665a,协程主要出现在控制面 RPC、连接管理和阻塞任务卸载等 I/O 密集路径;对外接口仍大量保留同步调用形式。

这形成了一个很实用的分层:内部用 C++20 协程组织异步流程,边界处再按需要转换成同步返回值或回调。

技术栈

Mooncake 的协程代码主要建立在两层库之上:

  • async_simple::coro::Lazy<T> 表示一个惰性异步任务;
  • yalantinglibscoro_rpccoro_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

  1. 从客户端池取得可用连接并发出请求;
  2. 等待 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(...);

它负责:

  1. 从连接池租用客户端;
  2. 必要时通过 co_await client->connect(...) 建立连接;
  3. 通过 co_await client->call(...) 发起 RPC;
  4. 根据错误类型决定是否清理连接池;
  5. 返回状态和响应数据。

在这个协程内核之上,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_awaitco_return 编写异步核心逻辑;
  • 使用 coro_rpc 管理连接和 RPC 请求;
  • 使用 syncAwait 兼容同步业务接口;
  • 使用 .start(callback) 提供真正的异步回调入口;
  • 使用 coro_io::post 将阻塞工作移出协程执行线程。

理解 Mooncake 的关键不是统计源码里有多少个 co_await,而是识别三种边界:协程与网络事件循环的边界、协程与同步调用者的边界,以及协程与阻塞线程池的边界。

源码索引