Qwen3.8: 从 0 到 1 用 Rust 重写 Mooncake: Suncake

Posted by w@hidva.com on September 5, 2026

如之前的系列文章所示, 我们在 Mooncake 上做了大量定制化改造: local masterunified memory poolevict、offload 等等. 改到今天, 我们和开源社区的 Mooncake 已经完全是两条发展路径了, 社区版本里那些 snapshot / restore / 多副本之类的功能我们既不需要, 行为也和社区版越差越远. 之前和同事开玩笑, 说或许我们应该改个名字, 不叫 mooncake 了, 叫 suncake.

为什么 Rust?

在讲 suncake 之前, 先把我的立场摆出来, 这样后面的故事才好理解. 如我在 Mooncake 统一内存池: AI Vibe Coding 与 Rust 里写过的, 我一直觉得 Rust 更适合 AI vibe coding, 也更适合 mooncake 这种重 IO 应用. 理由大概三条:

  1. Rust 更能显式表达意图. enum + matchResult + ?、trait 这些设施能把”有哪些状态”、”错误怎么传播”、”谁拥有这块数据”写得非常直接. 代码里显式表达出来的信息越多, AI 越不容易靠猜.
  2. 人类最烦 Rust 的地方, 往往恰好是 AI 最不怕烦的地方. 比如 lifetime, 对人来说写着写着就烦了, 但对 AI 来说, 根据编译器报错去缩短 borrow、拆 owned/borrowed、调整签名, 这类机械但繁琐的对齐过程正好适合交给它.
  3. Rust 编译通过之后, 可信度就是更高. C++ 编译通过, 很多时候只是说明模板和语法终于拧过去了, 绝不意味着内存没问题. dangling pointer、use-after-free、迭代器失效, 完全可能在”编译成功”的前提下继续往线上跑.

至于对 C++ 的吐槽, 我历史文章里也没少写. 最近的一次是今年 4 月: 一次 std::make_pair 让 iter_ 悄悄失效. 当时 review AI 生成的 SwapOut(), 循环里每个元素确实都是通过 splice 挪走的, 理直气壮地觉得这段代码肯定没问题. 结果真正把 iter_ 搞坏的不是循环, 而是最后那句 return std::make_pair(ok_list, err_list) —— 两个左值 list 被 copy 进返回值, 新 list 重新分配了一批 node, iter_ 里记着的还是 callee 栈上那批旧 node 的 iterator, 函数一返回就当场悬空. 我一路追到反汇编才确认这件事. 这类问题的麻烦不在于”做不到对”, 而在于它太细、太隐式、太依赖经验, 人尚且容易忘, AI 在上下文不完整时更容易踩坑.

所以我眼里的结论很朴素: C++ 里靠 review 红线、靠经验、靠文档维持的正确性, 在 Rust 里往往是类型系统和 borrow checker 直接保证的. 这个判断对不对, 需要一个足够大的实验来验证. suncake 就是这个实验.

试一试 Rust

如标题所示, 这次我用的工具是 Qoder, 模型是 Qwen3.8. 整个 suncake 项目, 从空目录开始, 人工只输入了下面这一个 prompt:

基于当前项目 mooncake 的代码库,请全面分析项目结构,特别是读取 AGENTS.md 以及其中提到的所有相关文件。根据项目现状可知,我们对 mooncake store 部分进行了大量定制化改造,包含了 local master、unified memory pool、evict、offload 等核心功能,这与开源社区版本的 mooncake store 已经是完全不同的发展路径。现在需要从零开始构建一个新的 Rust 项目 suncake,具体要求如下:

1. **功能范围限制**:
   - suncake 项目只需包含当前 mooncake store 模块的核心功能,仅实现 local master、unified memory pool、evict、offload 等指定逻辑
   - 完全排除社区版的其他功能(如 snapshot、restore 等)
   - 依赖现有的 mooncake transfer engine 的 Rust 绑定(通过 Cargo 依赖),无需重新从零构建 transfer engine,因为现有实现已经令人满意。transfer engine Rust bind 能否也使用 USE_EVENT_DRIVEN_COMPLETION 特性,我不太喜欢 busy poll 的方式。

2. **API 兼容性要求**:
   - suncake 必须提供与当前 mooncake store 完全相同的 pybind 接口
   - 目标是在不进行任何修改的情况下,能够成功运行 mooncake-wheel/tests/test_local_master_unified_pool.py 测试用例

3. **架构和技术要求**:
   - 由于 mooncake store 是 IO 密集型应用,应该在合适地地方使用 Rust 的 Tokio 异步运行时
   - 在合适的地方采用 async/await 异步编程模型编写,充分利用 Rust 的异步特性来处理高并发 IO 操作
   - 充分利用 Rust 安全抽象,仅在不得不的时候使用 unsafe Rust。

suncake 工作目录:/mnt/workspace/zhanyi/suncake。在实现过程中应保留必要的信息,我准备在此之后写一篇文章,大概主题是 Qwen3.8 从 0 到 1 构建 suncake,你保留的信息将作为文章的素材。

你在实现中应该意识到当前 Mooncake 库中很多现状只是为了避免在社区版本上引入太多的改动。比如 local master 模式下 ObjectMetadata.replicas_ 总是只有 1 个 replica,之所以是现状是 Vec 是因为社区用了 Vec,如果去掉 Vec 会引入太多的改动。

之后的过程就没什么人工介入了: Qoder 先在 plan mode 里通读 mooncake 代码库和我们内部的设计文档, 产出了一份 spec; 确认之后进入 goal 执行, 按 M1(store core) → M2(offload 数据面) → M3(Redis MetaSyncer) 三个里程碑逐步落地. 每个里程碑的验收判据都是硬的, 直接写死在脚本里: M1 要求 test_local_master_unified_pool.py 零修改跑通, Ran 17 tests / OK / skipped == 0; M2 要求 offload 测试 failures=0, 连 7 条 skip 的原因字符串都逐条核对, 出现第 8 种原因立刻 FAIL; M3 要求 Redis 元数据跨节点可见, 且收尾 DBSIZE == 0.

最终验收数字: M1 墙钟 suncake 108.8s vs C++ 209.2s; M2 墙钟 suncake 81.9s vs C++ 156.6s, 两个里程碑都恰好是 C++ 的 52% 左右. git log 里从 2026-09-03 的第一个提交 feat: suncake M1 — Rust rewrite of pai-mooncake's local-master store 到 09-05, 一共 37 个提交, 每步一个提交, message 里带着该步的实测证据. 最后列一下这个项目的体量:

  • 11 个 crate, 生产代码(crates/*/src)约 2.26 万行; 7 个 smoke example 约 8.4k 行; 文档 + 15 份 ADR 约 4 千行.
  • 生产代码 0 个 unwrap(), expect 仅 3 处, unsafe 约 80 处, 全部集中在 mmap、SCM_RIGHTS、O_DIRECT、buffer protocol 这些 FFI/裸内存的必然位置, 每个 unsafe 块配 // SAFETY: 注释.
  • 开发周期 2026-09-03 到 09-05, 37 个提交.

还得是 Rust!

重写完之后做了一次认真的 A/B 性能对比: 同一台 192 核机器, tcp loopback, 64 路并发, 1 MB 对象, 两边各跑 3 次取中位数. 为了可比性, 两边用同一份 benchmark 脚本、同一份 master 配置、同一份预编译的 Transfer Engine 静态库. 结论一句话: put 平手, get 的中位数平手(C++ 略好), 但 get 的尾部和可用性是 suncake 压倒性胜出, 而且把 C++ 的线程预算翻倍也救不回来.

  • put: 稳态 p50 差 1.2%, 落在 C++ 自己 27% 的轮间极差之内, 平手.
  • get 中位数: C++ 8.98 ms vs suncake 12.71 ms, C++ 略好.
  • get 尾部: C++ 稳态 p99 是 34,045 ms、max 68,114 ms; suncake 对应 82 ms / 153 ms. 差 339 到 445 倍.
  • 整阶段: 同一批 3,840 个跨 client 读, suncake 1.45 秒跑完, C++ 要 163 秒(8+8 线程)或 308 秒(16+16 线程) —— 线程翻倍反而更差.
  • 资源: real client 单进程峰值线程数 C++ 266 vs suncake 21.

这里要特别说明, 这不是一个”async 比 sync 快”的故事 —— 两边其实都是 async, C++ 的 RPC 框架本身就是协程 + asio. 真正的差异在于: C++ 侧是一个 RPC 线程同步占住、直到跨 client 传输结束才释放, 而 suncake 侧 IO 编排全部 async 化, worker 不会被单个传输占住. 高并发下这个差异不体现在中位数上, 全部体现在尾部上. 同步占住 RPC 线程还让我吃过大亏:一行代码引起的分布式死锁. 这恰好验证了我在 vibe coding 那篇文章里的判断: mooncake store 是 IO 密集型应用, Rust + Tokio 的 async/await 就是更适合它的表达方式. 当时这句话多少带点直觉成分, 现在有了对照实验数据.

kimi-k3 的评价

最后让 kimi-k3 读了一遍 suncake 的 docs/README.md, 然后评估项目完成度和代码质量, 算是请了个独立第三方 review:

它的总评是: “这是一个完成度高、过程证据链罕见地扎实的重写项目: 验收判据全部硬编码、刻意差异全部登记、性能数字区分实测与推断. “ 同时也挑出了三个实质性弱点: clippy 没清零(有一个已升 hard error 的 dead API)、全仓零单元测试(回归保障全挂在 smoke example 和 Python e2e 上)、以及 2Q 驱逐仍是 ADR-004 里自认的”降级形态”尚未收敛. 这几个批评都是对的, 尤其零单测这条, 确实是当时为了对齐”Python 测试即契约”策略做的取舍.