RPC 框架实验 · 元戎 × Mooncake

bRPC 与 coro_rpc

追踪 Linux TCP 收发、线程调度和编解码,并在 xqyun 与 tiantiyun 各自复测同机 RPC。

验证环境
实验主机
实验日期
—
结果轮次
—

01实验结论

1.1 双路物理机复测

在 tiantiyun-80c128g 的同 NUMA CPU 放置下,48 轮全部通过,26,228,060 次回包内容正确,RPC 错误和响应不匹配均为 0。实验角色分配在不同物理核上。计时窗覆盖的 CPU steal 计数增量均为 0。

配置 bRPC 成功 RPC/s · P99 coro_rpc 成功 RPC/s · P99
64 B · 1 并发 · 每端 4 worker 27,193 · 70 µs 31,667 · 50 µs
64 B · 32 并发 · 每端 4 worker 59,181 · 1,887 µs 301,821 · 135 µs
64 B · 128 并发 · 每端 4 worker 57,706 · 7,183 µs 319,690 · 455 µs
64 KiB · 32 并发 · 每端 4 worker 18,534 · 4,096 µs 71,409 · 587 µs
64 B · 32 并发 · handler 忙等 25 µs 24,093 · 2,048 µs 106,046 · 333 µs
64 B · 32 并发 · 每端 8 worker 57,270 · 2,024 µs 504,761 · 75 µs

表中为 3 轮中位数。P99 是成功请求直方图的上界估计,超过 1,024 µs 后桶会变宽,并可能按实测最大值截断。查看逐轮数据时应连同分布一起读。

1.2 线程利用率与单次调用成本

  • 小包 32 并发时,吞吐中位数相差 5.10 倍。 bRPC 服务端使用约 0.80 核,coro_rpc 使用约 3.20 核。按每轮起止快照可匹配的服务端 TID 记账,CPU 时间最多的两条线程占比中位数分别为 99.80% 和 50.08%。bRPC 三轮的该占比为 75.16%–99.84%。三轮分别配对的吞吐比为 5.03–5.23 倍。
  • 每次调用的 CPU 也有差异。 两端合计为 27.40 对 21.20 µs/RPC。服务端 13.64 对 10.61 µs/RPC,其中用户态约 6.13 对 3.54 µs,内核态约 7.50 对 7.07 µs。这组小包中,用户态时间占服务端 CPU 成本差的大部分。吞吐倍数同时受每次调用成本和实际并行利用率影响。
  • 增加 worker 与 CPU 预算时,两个框架的扩展曲线分开。 4→8 worker 时,coro_rpc 服务端实际用量从 3.20 增到 6.27 核,吞吐增加约 67%。bRPC 服务端约 0.80→0.79 核,吞吐从 5.92 万变为 5.73 万 RPC/s。这是 64 B / 32 并发固定连接配置下的观测。
  • 大包时单次调用总 CPU 成本更接近。 64 KiB / 32 并发两端合计 94.15 对 87.44 µs/RPC。coro_rpc 的服务端内核 CPU 成本在这一场景较高(32.55 对 26.80 µs/RPC),服务端用户态成本较低(12.11 对 20.15 µs/RPC)。25 µs 忙等带来的服务端 CPU 增量分别为 24.53 和 23.86 µs/RPC。

1.3 与 xqyun 原始实验对照

同样是 64 B / 32 并发,tiantiyun 对 xqyun 的成功吞吐中位数变化为 bRPC +57.4%、coro_rpc +19.4%。表中各项先按主机和框架分别取三轮中位数。

框架 服务端忙核 xqyun → tiantiyun 服务端 CPU µs/RPC 客户端 CPU µs/RPC 两端合计 CPU µs/RPC
bRPC 0.73 → 0.80 19.37 → 13.64 20.28 → 13.85 39.65 → 27.40
coro_rpc 2.64 → 3.20 10.46 → 10.61 11.59 → 10.59 22.05 → 21.20

bRPC 的服务端每次调用 CPU 下降约 29.6%,实际忙核增加约 10%。coro_rpc 的服务端每次调用 CPU 接近原值,忙核增加约 21%。吞吐约等于忙核除以每次调用的服务端 CPU 秒数。这是一组记账分解,各项中位数独立计算,两端合计取逐轮合计的中位数。

64 KiB / 32 并发时,tiantiyun 上 bRPC 和 coro_rpc 的吞吐中位数分别为 xqyun 的 2.72 倍和 1.84 倍。coro_rpc 的三轮范围从约 1.83–4.12 万收窄到 6.94–7.18 万 RPC/s。xqyun 最低吞吐轮和测量 CPU steal 峰值 23.09% 同轮出现。硬件、SMT、内核和宿主机条件共同变化,网页按相同配置展示各主机结果,便于保留这些边界。

1.4 跨 NUMA 的 CPU 放置对照

固定服务端 CPU,只将客户端切到另一插槽。18 轮全部通过,校验成功 7,816,998 次,0 错误。下表为跨 NUMA 相对同 NUMA 的中位数变化。

场景 bRPC 吞吐 · 两端 CPU/RPC coro_rpc 吞吐 · 两端 CPU/RPC
64 B · 32 并发 −20.0% · +17.6% −19.8% · +17.9%
64 B · 128 并发 −19.7% · +19.1% −21.4% · +20.6%
64 KiB · 32 并发 −30.5% · +44.9% −29.6% · +46.6%

64 KiB 的服务端内核 CPU 增量最突出。bRPC 从 26.80 增到 47.51 µs/RPC,coro_rpc 从 32.55 增到 53.30 µs/RPC,两边均增加约 20.7 µs/RPC。用户态分别从 20.15 到 20.71、从 12.11 到 12.12 µs/RPC。两组 64 B 场景的服务端内核增量约 2.0–2.5 µs/RPC。

这里固定的是线程的 CPU 归属,内存采用系统默认 NUMA 策略。数据量化进程的 CPU 记账变化和 CPU 放置敏感性。要区分内存驻留、跨插槽流量和内核 TCP 各阶段,需要分别采集相应证据。

1.5 双端 CPU 采样和队头阻塞验证

64 B / 32 并发、每端 4 worker 的独立 perf 轮分别附着服务端和客户端。以下为各自 4 秒窗口的结果,四份记录的丢失样本数均为 0。

角色 cpu-clock 样本数 内核样本周期权重 IPC
bRPC 服务端 577 约 51.82% 0.80
bRPC 客户端 581 约 55.77% 0.81
coro_rpc 服务端 2,386 约 66.93% 0.79
coro_rpc 客户端 2,388 约 59.59% 0.89

内核权重合计包含 kallsyms 和单独列出的内核模块。bRPC 服务端有约 8.15% 的周期权重列在 nf_tables、nf_conntrack 等模块下,另外三份记录也在 kallsyms 下采到 nftables 与连接跟踪符号。bRPC 双端可见 readv、writev,服务端另有 ProcessRpcRequest,客户端可见 bthread 调度和 Protobuf 解码。coro_rpc 可见 recv、sendmsg、epoll、Asio 与协程请求路径。

平铺样本给出执行中的叶节点。计数器与完整负载的窗口不同,逐次调用 CPU 成本以主矩阵的 cgroup 差分为准。阶段排队和空闲等待需另外观测。

主矩阵的忙核来自进程 cgroup。64 B / 32 并发时,另计两端所绑 CPU 的主机 IRQ/softirq,双端每次调用约为 33.18 与 26.49 µs。主机计数可能包含同机背景中断,进程成本仍采用上节的 27.40 与 21.20 µs/RPC。内核的调度任务时钟与 perf 任务时钟采用不同的中断记账路径。调度时钟源码 · perf 任务时钟源码

同机再次执行 coro_rpc 两连接探针,5 组中慢 handler 内联时快请求在放行前返回 0/5,移到独立 worker 后为 5/5。两种模式的请求均成功,线程 ID 与执行路径一致。

1.6 bRPC dispatcher 启动与预热对照

单独构建的诊断适配器增加了 dispatcher 配置入口及回读。固定 64 B / 32 并发、每端 8 worker 和 8 个物理核,按服务端 × 客户端比较。每个组合尝试三轮。

dispatcher 组合 完成计时轮次 预热结果和计时表现
1 × 1 3/3 685,701 次正确回包,吞吐中位数 57,030 RPC/s,三轮范围为 56,434–57,953 RPC/s
2 × 2 0/3 3 轮均未通过预热
4 × 4 0/3 3 轮均未通过预热
4 × 1 0/3 3 轮均未通过预热
1 × 4 0/3 3 轮均未通过预热

12 轮失败均发生在正式计时前。运行器未收到 WARMUP_DONE,客户端日志记录预热错误或至少一条 lane 没有成功请求。仅 1 × 1 有吞吐与延迟数据。

另一次独立诊断对照用相同亲和性观察连接与 epoll。1 × 1 正常通过预热。2 × 2 首个记录的失败为 E1008,RPC 达到 1,000 ms 超时。该诊断有 6 个快照同时看见两端各 32 条已建立连接均被 epoll 监视,服务端分布为 19 + 13,客户端为 17 + 15,未见同一 socket 被重复监视。

这些快照核对了配置回读及 socket 的注册关系。要确定超时发生在哪一段,还需关联事件送达、回调执行和请求往返时序。该组结果记录了当前实验适配器上的预热失败,性能对照仍以完成计时的基线场景为准。

1.7 xqyun-32c32g 原始结果

在 xqyun-32c32g 这组固定 CPU、单机 TCP 原生 Echo 测试里,coro_rpc 随连接增加取得了更高吞吐。 64 B 单连接时,成功吞吐相差约 1.30 倍。到 32 条连接,coro_rpc 达到 25.29 万 RPC/s,bRPC 为 3.76 万 RPC/s。两边合计校验通过 19,651,332 次回包,RPC 错误和内容不匹配均为 0。

配置 bRPC 成功 RPC/s · P99 coro_rpc 成功 RPC/s · P99
64 B · 1 并发 · 每端 4 worker 16,169 · 241 µs 21,027 · 206 µs
64 B · 32 并发 · 每端 4 worker 37,592 · 2,048 µs 252,887 · 344 µs
64 B · 128 并发 · 每端 4 worker 30,275 · 16,384 µs 232,509 · 2,048 µs
64 KiB · 32 并发 · 每端 4 worker 6,804 · 16,384 µs 38,902 · 4,096 µs
64 B · 32 并发 · handler 忙等 25 µs 18,289 · 4,096 µs 90,179 · 808 µs
64 B · 32 并发 · 每端 8 worker 42,993 · 2,048 µs 391,704 · 334 µs

表中为 3 轮中位数。超过 1,024 µs 的延迟使用较宽直方图桶,例如 2,048 µs 表示估算上界。逐轮分布和观察值可在第 4 节或 CSV 复核。

1.7.1 并行利用率和每次 RPC 的 CPU 需分开看

  • 在 64 B / 32 并发下,服务端 CPU 分别为 bRPC 19.37 µs/RPC、0.73 核,coro_rpc 10.46 µs/RPC、2.64 核。两端合计分别约 39.65 与 22.05 µs/RPC。吞吐差距同时包含更高的 CPU 并行利用率和更低的每次调用成本。
  • 该配置第一轮,bRPC 可见的 5 个 worker 中只有 2 个累计约 1.53 和 1.52 CPU 秒,coro_rpc 的 4 个 I/O 线程分别累计约 2.56–2.69 CPU 秒。8 worker/8 vCPU 场景中,coro_rpc 服务端中位使用 4.57 核,bRPC 为 0.77 核。bRPC 该路径的单 tag dispatcher 与 urgent 调度是源码中可定位的执行边界。本轮未改变 dispatcher 数,现有数据支持线程集中现象,无法把 6.73 倍差距单独归给 dispatcher。
  • 参数 4 表示配置值。32 并发小包下,服务进程实测线程总数为 bRPC 8、coro_rpc 6。bRPC 线程数随启动与轮次波动,快照也包含主线程、timer 和 bvar sampler。bRPC 启动时按 tag 等待至少一个 group 后继续,Server 又按 tag 配置线程数,这一启动行为有源码依据。线程数与 CPU 预算分别读取。 bRPC TaskControl 初始化 · Server 线程设置

1.7.2 TCP、编解码和 handler 工作量

  • 同一 64 B / 32 并发下,cgroup 记录服务端内核 CPU 为 10.66 对 7.50 µs/RPC。独立的 149 Hz 服务端采样中,bRPC 的 542 个样本约 52.4% 落在内核,coro_rpc 的 1,497 个样本约 68.9% 落在内核。后者的内核样本占比更高,每成功 RPC 的内核 CPU 时间更低,这两个指标的分母不同。
  • 两边都采到 syscall 入口、TCP 发送和内核锁路径。bRPC 用户态可见 SendRpcResponse、Socket 以及 Protobuf 符号,coro_rpc 可见 send_data、start_impl、Asio reactor 和 malloc。符号样本表示 CPU 执行位置。队列等待、分配次数和单个请求的各阶段耗时需要对应埋点。
  • 载荷从 64 B 扩到 64 KiB,服务端 CPU/RPC 从 19.37 升到 100.48 µs、从 10.46 升到 73.72 µs。增加 25 µs 忙等时,服务端 CPU/RPC 升至 46.57 与 35.80 µs,增量约 27.2 与 25.3 µs。较大的 payload 及人为 handler CPU 工作量都进入端到端成本。

1.7.3 结果适用范围

测试保留了各自的原生编解码和客户端调度。结论适用于这套 Echo 适配器、连接分布、loopback 和所选 CPU 预算。xqyun 是共享 KVM 来宾,测量 CPU 的 steal 在各轮约 0.06%–23.09%。64 KiB 的 coro_rpc 单轮吞吐为 1.83–4.12 万 RPC/s,宜与重复范围一起看。元戎默认 pooled channel、产品认证和 Trace,以及 Mooncake Store 的 handler 池会增加不同的工作。产品吞吐、跨机链路、RDMA、故障恢复与峰值负载需各自的产品测试。

02线程运行视图

bRPC · M:N stackful bthread

  1. 就绪按 fd 分配 EventDispatcher,epoll 等待事件。
  2. 读与拆帧Socket 启动 urgent bthread,InputMessenger 从 IOBuf 切帧。
  3. 派发默认多帧时前面的消息另起 bthread,末条在读 bthread 内处理。
  4. 执行校验并发与元数据,解码 Protobuf,调用服务。
  5. 回包done 序列化并写入 socket。必要时由 KeepWrite 续写。
图 2-1 读与业务阶段可运行在同一 pthread,也可随 bthread 调度迁移。

coro_rpc · Asio 与 C++20 协程

  1. 接入accept 循环接收连接,连接分配到 Asio I/O 执行器。
  2. 读与恢复异步读请求头和 body,I/O 完成后恢复连接协程。
  3. 派发按函数 ID 找 handler,以 struct_pack 解码参数。
  4. 执行同步 handler 在当前执行器执行。协程 handler 可挂起等待。
  5. 回包编码响应,交由连接上的异步写链发送。
图 2-2 连接执行器与协程恢复决定线程归属。阻塞的同步 handler 会占住执行线程。

03接入与开销

元戎与 Apache bRPC

元戎 bRPC 源码核对

版本与边界

  • 元戎基线取工作区 v0.9.2.rc40 的 c0bbb1582e49ecc2d2d925836d1288633d322012。本地缓存的上游 main/master 另核对到 a128d452510181622c69440783c9a0611f08d82e(2026-11-05),两者均依赖 Apache bRPC 1.15.0,发布 commit 50a9075de62f1d2b825b902acbfecfddb3d9f314。CMake Apache 源码包 SHA-256 0bc8c2aee810c96e6c77886f828fbfdf32ae353ce997eb46f2772c0088010c35。Bazel GitHub tag 包 SHA-256 f674b753af71dc313d9d2dcf34f574f0a3438c9f9bb9e7e6ca500a3b0ca7ddfb。两个哈希对应不同打包。CMake 链接工程自己的 gflags、leveldb、protobuf、OpenSSL、zlib,并应用四个适配补丁(BoringSSL、glog flags、显式 pthread 链接、zlib)。Linux 构建关闭 bRPC pthread mutex hook。独立仓采用的依赖及编译开关应以构建日志为准。源证据 cmake/external_libs/brpc.cmake:3,87,135,161、bazel/sdk/repositories.bzl:154。Apache bRPC 发布 commit
  • WorkerOCServer 和 Coordinator 调用 RpcServer::StartBrpcServer,由生成的 service adapter 承接 google::protobuf::Service::CallMethod。RpcStubCacheMgr 为 Worker↔Worker、Worker↔Master、Coordinator、跨 Worker transport 等创建 _BrpcGenericStub 与 Channel。内部 Channel 的 RPC 与 connect 初始默认取 node_timeout_s × 1000(flag 默认 60s),关闭 bRPC 盲重试与熔断,各方法还可传入更紧的预算。SDK ClientWorkerRemoteApi 的对象 Channel 也关闭盲重试,实际 retry/routing 属于上层 API 语义。CMake 某些库仍名 common_rpc_zmq,该名称不能用来辨识此基线的实际 RPC transport。源证据 src/datasystem/worker/worker_oc_server.cpp:2541、src/datasystem/coordinator/coordinator_service_impl.cpp:718、src/datasystem/common/rpc/rpc_stub_cache_mgr.cpp:260、src/datasystem/client/object_cache/client_worker_api/client_worker_remote_api.cpp:209、src/datasystem/common/rpc/CMakeLists.txt。
  • rc40 的 RpcServer 默认 worker 64、并发上限 128(设 0 关闭上限,超限 RPC 由 bRPC 以 ELIMIT 拒绝),dispatcher 配置 0 继承 upstream 每 tag 一个,内置 HTTP 诊断服务默认关闭。Channel 默认 pooled,同 endpoint 最多保留 8 条空闲连接,rc40 可切 single。缓存的 main/master 仍是同版 bRPC 及 64/128/0/8,但 Channel 固定 pooled,转发 connection_group,服务端增加原始请求认证捕获,过期拦截器去掉开关。源证据 rc40 src/datasystem/common/flags/common_flag_define.cpp:314、src/datasystem/common/rpc/brpc_factory.cpp:119,160、src/datasystem/common/rpc/rpc_server.cpp:114,缓存上游 brpc_factory.cpp:162、rpc_server.cpp:132、brpc_expired_request_interceptor.h:49。
  • 元戎 unary 有 plain protobuf 与 attachment payload 变体,也有 stream StreamCreate/StreamAccept/StreamWrite,SHM、RDMA/URMA 等数据路径另有生命周期。独立 echo 的对象是 TCP unary 语义,产品 adapter、Auth、Trace、deadline、控制面与数据面快路径分别看源码或产品实验。源证据 src/datasystem/common/rpc/plugin_generator/brpc_service_generator.cpp:269,406,455、brpc_client_stream_impl.h:124、brpc_server_stream_impl.h:94。

bRPC 1.15.0 线程与 RPC 路径

  1. Linux EventDispatcher 由长期运行的 bthread 执行 epoll_wait,默认每 tag 一个,按 tag 和 fd hash 选择。事件通过 Socket::OnInputEvent 更新每 socket 的 _nevent,首个待处理事件通常用 bthread_start_urgent 交给 Socket::ProcessEvent,read/drain 走 InputMessenger::OnNewMessages。urgent 可在当前 worker 立即切换,不必然跨 pthread;工作窃取也使同一条 RPC 的执行点不保证固定 TID。默认 flags usercode_in_pthread=false、usercode_in_coroutine=false。Dispatcher 初始化与分片 · Linux epoll 循环 · Socket 输入事件
  2. InputMessenger 从 IOBuf 拆 baidu_std 帧,12B PRPC 前缀外还有 protobuf RpcMeta 与请求 body。默认普通 TCP 一次切出多条消息时,前面的消息走其他 bthread(批量 BTHREAD_NOSIGNAL + bthread_flush),末条在读 bthread 内处理。ProcessRpcRequest 解码 meta、建 Controller、定位服务和方法、检查并发、调用 interceptor,再获取 Protobuf request/response 实例并解码 body,默认在当前 bthread 直接 Service::CallMethod。同一条 TCP 连接允许多条 RPC 并发派发。InputMessenger · baidu_std
  3. done 执行 SendRpcResponse,序列化 response/meta,拼 IOBuf body/attachment 后写 socket。写队首通过 _write_head.exchange 竞争。连接可写时尝试由当前执行流直接写,写不完、配置后台写等条件走 KeepWrite,可能等待 EPOLLOUT。共享 socket 的多生产者竞争、写队列与 TCP 缓冲回压都值得单独测。pooled 每次取还 socket,空闲满时回收,8 条空闲保留不等于 8 条并发连接,突发超出保留数后的新建及回收也会放大成本。回包打包 · Socket 写路径 · Client 取池连接
  4. Client Channel::CallMethod 序列化、为有 timeout 的调用注册 timer、IssueRPC;done == nullptr 时按 correlation ID 同步 Join。响应处理用 ID 找回 Controller,解码 response,进入完成逻辑。同步 client 是 OS pthread 还是 bthread 会改变等待与唤醒成本,应写进实验参数。Client 发送与等待 · Client 接收
  5. 线程模型为 stackful bthread M:N pthread。worker 执行自己的队列并可窃取,空闲有 parking/wakeup;bthread-aware wait 可让出执行权,普通阻塞 syscall 或未适配的锁可占住承载的 pthread。元戎 rpc_server.cpp:42 对 TBB yield 做 bthread-aware override,ScopedBthreadLocal 承载请求级状态。ServerOptions.num_threads 配默认 tag 的池,不是一组独占业务线程。1.15.0 的最小值是 4,原生初始 bthread_concurrency 默认 9,tag 池初始化后只能增加。测 4 worker 应在首个 Server/Channel 初始化前设置进程 gflag,实测值用 bthread_getconcurrency_by_tag(0) 或 /vars 核对。线程池参数 · 动态设置限制 · Server 设置 · 用户态调度与切栈

可量化开销与观测口径

开销位置 源码指向 运行侧证据
TCP 通知与收包 epoll_wait、Socket::OnInputEvent、InputMessenger::OnNewMessages 单连接 vs 多连接,dispatcher 活跃度、每线程 CPU、TCP 连接数、每成功 RPC 的 syscall 与 context switches。相同 endpoint 的多个 bRPC single Channel 默认可共享 Socket,固定 N 条连接时每 lane 设置独立 connection_group 并核对 TCP 连接。
执行权和调度 bthread_start_urgent、TaskGroup::sched_to/steal_task、butex、timer 线程 CPU/迁核/调度事件,bthread_worker_count、bthread_worker_usage、bthread_switch_second。OS context switches 与用户态 bthread switches 分列。增加 worker 的收益或开销要在固定 cpuset 下比较。
编解码与对象生命周期 RpcMeta、Protobuf body、Controller、message factory、IOBuf 同请求/响应业务字节及空包 baseline,记录大小梯度、CPU µs/成功 RPC 与分配或采样栈。保持原生编码的对比同时包含序列化,不能直接从 QPS 差中扣出调度开销。
写队列及连接池 Socket::StartWrite/KeepWrite、SocketPool::GetSocket/ReturnSocket P99 与连接模式、TCP send queue、连接数、错误码一起看。用额外实验区分 pooled 取还/连接 churn 和 single 单 socket 争用。
业务执行与回压 CallMethod、max_concurrency、元戎过期请求拦截器 把 handler CPU 占用、bthread-aware sleep、OS blocking sleep 当成三种不同负载。错误按类别分开,成功 QPS 与尝试 QPS 分列。

Channel 连接分组实现 · 线程运行指标

元戎生成代码的 ClientSend/ClientRecv 位于同步 channel_->CallMethod 前后,涵盖客户端框架、传输、服务端处理与等待返回。ServerRecv 在元戎 adapter 内,bRPC 已完成派发及反序列化;ServerSend 在 adapter 内,早于 bRPC 真实回包序列化和 socket 写出。server_req_queue_us 是 adapter ServerExecStart−ServerRecv,不能直接解释为 bthread runnable queue。network_residual_us 用两个端各自区间时长相减,仍含 adapter 外的 bRPC 编解码、排队、TCP 内核路径和客户端唤醒。它无法给出纯网络 RTT。过期拦截器另外使用 bRPC get_rpc_received_us()(收包循环附近)与传送的相对 timeout,AcceptRequest 在 protobuf body 反序列化前执行。源证据 src/datasystem/common/rpc/plugin_generator/brpc_stub_generator.cpp:383、brpc_service_generator.cpp:419、brpc_perf_trace.h:351、brpc_expired_request_interceptor.h:51。上游 receive 标记 · 上游 interceptor 顺序

核对过程以共享 CodeGraph 查询 RpcServer、BrpcFactory 起步。索引返回了部分旧路径/测试节点,随后用 git grep <ref>、git show <ref>:<path>、CMake/Bazel pin 和官方 bRPC tag 源码逐条核对。整个源码子任务在元戎仓只读进行,产品链路的实测证据需与独立 RPC 测试单独列出。

实验输入的 Apache 1.15.0 发布包已用 sha256sum 核对为 CMake 的同一个包。以上上游源码位置也在该 tar 解包后复核。

Mooncake 与 yalantinglibs coro_rpc

Mooncake Store 与 yaLanTingLibs coro_rpc

版本与验证过程

  • 选用 Mooncake Store v0.3.14,提交 e1ea5d31bb5e46ff2df9f90e7c2fbaf52fcdee8c(2026-11-03)。先查询远端标签,再把发布提交取到临时 bare Git 仓,用 git show <commit>:mooncake-common/FindYLT.cmake 核对依赖。该发布版用 CMake FetchContent 固定 yaLanTingLibs 45c6463c0afbcc46262e373effa30635be3846a0,源码 tar.gz 实算 SHA-256 baa6b5c9ffcc0af8252742bba8f0fb9c6d37387c13b2ff5b26d29c6aaff6ee37,与 CMake 相符。同期 main 快照 0ff71517f0f80a198eebff3009834ed00504b982 已前进到 YLT 54f9c0642386e7860e8e11ada698c484dc562d7d。早先网页缓存中的 submodule gitlink 属于不同源码快照,因此实验口径以发布提交和该提交的 CMake 为准。Mooncake v0.3.14 的 FindYLT.cmake · main 快照的 FindYLT.cmake
  • 独立实验可直接链接 header-only 的 yalantinglibs::yalantinglibs CMake interface target。编译采用 C++20,源码树包含公共头文件、Asio 与 async_simple 等第三方头文件。原生 TCP 对照使用同一主机上的非 TLS 路径。Mooncake release 的 Linux 构建可启用 IBV,但 Store 仅在 MC_RPC_PROTOCOL=rdma 时选择 RDMA RPC,且受构建能力约束。YLT 安装目标 · Mooncake 协议选择 · Master 创建服务

线程模型

位置 执行位置与跨线程边界
Store 调用方 MasterClient 在 syncAwait 中借用/获取池内连接并发起 send_request,等待网络响应和返回值。调用接口为同步,I/O 由客户端池线程处理。Store I/O 池由 MC_STORE_RPC_CLIENT_IO_THREADS 控制,回落到 MC_RPC_CLIENT_IO_THREADS,再回落到 min(16, max(1, hardware_concurrency))。普通调用、HA 控制和 HA 探测可有各自的连接池配置,三者使用 Store 客户端 I/O 池。 MasterClient 构造 · 同步调用路径 · 线程数来源
框架服务端 默认 io_context_pool 为每个线程建立一个 asio::io_context(1),每个 context 用一个 OS 线程运行。acceptor 在池内 context 上监听,每次 accept 轮转选择 socket executor。连接的 I/O 和直接注册的普通 handler 由该连接所属线程处理。同一进程还有启动/汇合线程。I/O 池大小不是 ps -L 看到的进程总线程数。 I/O 池 · 轮转选择 · TCP accept · 执行普通 handler
Store Master 业务层 rpc_thread_num 控制 I/O 线程数,纯命令行默认回落到 min(max_threads=16, hardware_concurrency)。handler_worker_thread_num 默认跟随 I/O 线程数。GetReplicaList、ExistKey、BatchExistKey、PutStart 和 PutEnd 等以业务方法 ID 注册 Lazy wrapper,参数反序列化后由 coro_io::post 投递到独立 asio::thread_pool。业务结束后,框架把回包排到原连接 executor。Ping、ServiceReady 及三个 admin/prefetch 方法直接注册。HA 与非 HA 服务均安装业务池。 线程配置 · 业务池默认值 · 业务池与注册 · 方法清单 · HA · 非 HA
Store 调用线程  syncAwait ── 连接池 ── Client io_context ── TCP
                                                                │
Master 连接线程   TCP ── 读帧 ── 路由/解码 ── 普通直接 handler ────┤
                                      └─ 投递 ── 业务池 handler ─┤
Master 连接线程                        回包调度 ── 写队列 ── TCP ◀─┘

运行视图与可验证开销

区段 从源码可定位的开销 如何区分
客户端封包和等待 默认 struct_pack。请求建立序列号、timer 对象、promise 和 response handler,并在互斥保护下登记响应映射。读响应按序列号匹配,完成 future 并解析返回值。定时器启用与 timer 对象分配是两个不同判断。Store 另有连接池和 syncAwait 的边界。 对照短包与大包,分别观察客户端 CPU 时间/成功调用与 RTT。只有符号采样或分段探针才能把差异归到序列化、内存分配、锁或唤醒。 发请求 · 启动计时器
TCP 收发和编解码 TCP 请求头固定 20 B,响应头固定 16 B。协议读取头再读取 body/attachment,经路由完成 struct_pack 解码及返回值编码。响应头、body、可选 attachment 以多个 buffer 交给 Asio async_write。TCP 读写和内核拷贝、有效载荷序列化、应用层附件路径各有独立成本。 原生协议 echo 保留两框架自己的帧与编解码。相同应用字节量可比较调用,协议线上字节数仍各自统计。固定每连接在途请求数,区分闭环和 pipeline 数据。 帧结构 · 头长度 · 回包缓冲区
连接亲和与排队 少数热连接可能集中于单个 I/O 线程。普通同步 handler 在 I/O 线程上运行,阻塞时会影响同线程其他连接。一个连接的回包经 write_queue_ 串行写出。协程 handler 路径在响应仍在进行时可能建立新的 context;具备复用条件时可迁移已有的 body buffer。 用 1 与 N 条连接对照 N 个 I/O 线程。查看 per-TID CPU、上下文切换、吞吐与 P99。普通 echo 展示连接分布,带可控慢 handler 的混合请求可单独验证 I/O 线程队头阻塞。 上下文复用 · 连接写队列
Store 业务卸载 I/O 线程解码后投递工作队列,worker 执行业务,回包再调度到连接 executor。业务路径多了排队、唤醒和跨核数据接力,代价大小由实测决定。Ping 绕过业务池,仍与其他连接共享 I/O 线程、CPU 和网络资源。 以同一 coro_rpc 的直接 handler/显式 worker 池配对,保持 CPU 预算和慢请求比例。单独测快请求 P99 与慢请求 goodput,并记录 post→worker 和 handler done→I/O write 的排队时间,才可量化隔离收益与新增成本。 业务池 · 回到连接 executor

Store 与数据面边界

  • Store Master RPC 承载元数据、生命周期和协调。例如 Get → Query → GetReplicaList 得到副本描述,然后按副本类型走读取路径。Put 以 PutStart 申请副本,经 TransferWrite 送出数据,再按完成策略 PutEnd。BatchIsExist 调用 BatchExistKey,在该接口中只查存在性。 GET 元数据 · Query · PutStart · TransferWrite · PutEnd · BatchIsExist
  • Transfer Engine 自身也有用 coro_rpc 的控制通信。对象传输由 TransferSubmitter 向对应的传统 Engine/TENT submitTransfer 提交,随副本种类和路径配置执行。独立 echo 是原生 RPC 控制面微基准,Store 端到端成本还需副本策略、业务池、锁、数据传输和完成条件的测量。 TransferSubmitter 提交 · TENT RPC 接入

04实验数据

每个点展示同一配置下的轮次中位数。延迟单位为微秒。

bRPCcoro_rpc

单轮数据

CPU µs/RPC 以成功 RPC 为分母。P50/P99 为单轮成功请求延迟。图上的轮次中位数与合并请求后的分位数使用不同的统计口径。

05过程记录

5.1 tiantiyun 复测条件

项目 复测口径
源码 RPCBenchmark 提交 02cce72d0c6c2b77f04a5a522307f05eb3394e80。Apache bRPC 1.15.0 与 Mooncake v0.3.14 锁定的 YLT 45c6463,均校验原始发布包 SHA-256。基准适配器和主矩阵脚本沿用已有源码。
主机 tiantiyun-80c128g,CentOS Stream 9,双路 Intel Xeon Gold 6138,40 个物理核、80 个逻辑 CPU,两个 NUMA 节点。共享主机,实验期间保留其他服务。
构建 GCC 11.5.0、CMake 3.31.8、Ninja 1.10.2,沿用上次实验的 Protobuf 3.14.0、gflags、LevelDB RPM。bRPC 禁用 pthread mutex hook。动态库和内核的补丁版本见原始构建回执。编译中间文件写入独立 tmpfs,计时前构建已停止。
同 NUMA 主矩阵 服务端 CPU 4/6/8/10/12/14/16/18,客户端 20/22/24/26/28/30/32/34,均在 node 0。4 worker 用各自前四个 CPU。两端与监控分开,实验角色各占不同物理核。
跨 NUMA 补充 固定服务端,客户端切到 node 1 的 21/23/25/27/29/31/33/35。覆盖 64 B / 32 并发、64 B / 128 并发与 64 KiB / 32 并发。绑定 CPU,内存仍用系统默认 NUMA 策略。
计时 主矩阵 8 场景 × 2 框架 × 3 轮,每轮预热 1 秒,测量 4 秒,超时 1000 ms。独立 TCP 连接,闭环每连接一条在途,逐次校验响应,逐轮核验 TCP 连接数。
采集 cgroup v2 按角色记账,监控固定 CPU 36。记录逐线程调度运行时间、RSS、OS 上下文切换、CPU steal、mpstat、iostat、dstat 与 pidstat。另开独立运行窗口进行双端 perf。

5.2 启动过程和证据边界

前两次预检先后遇到主机缺少 rg 和嵌套 systemd-run 的 D-Bus 启动失败。两次均发生在计时前。启动脚本改为使用系统已有的 sed,并在外层委派 cgroup 内直接创建独立矩阵子组。失败日志和最终脚本均随证据包留档。

主矩阵和跨 NUMA 补充串行。双端 perf 的 64 B / 32 并发采样运行与主矩阵分离,cpu-clock 采样率 149 Hz、附着 4 秒。cgroup CPU 分母取主矩阵成功请求,perf 采样百分比对应采样窗口内的 CPU 执行位置。所有配置保留原生 TCP framing 和编解码。

bRPC dispatcher 对照使用单独构建的适配器补丁。固定每端 8 worker 和 8 个物理核,改变服务端与客户端 dispatcher 计数。预热失败原样归档,成功计时和独立 FD 诊断分别留档。Bazel 构建与冒烟是另一条验证路径,前述性能数据来自 CMake 产物。

复测节点(UTC+8) 证据
00:39:57 · CMake 构建与冒烟结束 3 项 CTest 通过,记录二进制 SHA-256 和动态依赖
00:41–00:43 · 两次预检 缺少 rg 和 D-Bus 启动失败均在计时前发现,保留原日志
00:46:02–00:50:12 · 同 NUMA 主矩阵 48 轮逐轮结果与拓扑记录
至 00:51:45 · 跨 NUMA 补充矩阵 18 轮逐轮结果及同配置配对
至 00:52:25 · 双端 perf 和阻塞探针 4 份进程级符号采样和 5 组探针配对
至 00:57:29 · dispatcher 配置对照 3 轮完成计时和 12 轮预热失败,另有 FD 观察轮
至 00:57:54 · dispatcher 独立诊断 1 × 1 与 2 × 2 的回读、FD 快照和首条 RPC 错误
00:58:16–01:02:03 · Bazel 构建与冒烟 Bazel 7.4.1、GCC 11.5.0,构建两个二进制,3 项冒烟通过,重复构建命中动作缓存

Bazel 使用准备好的依赖下载缓存,编译动作缓存从空目录起步,18 jobs,构建两个目标用时 223.69 秒。随后 3 项冒烟共 2.62 秒,重复构建 0.24 秒。这三项计时各有独立日志。Bazel 和 CMake 的依赖图、缓存和任务条件不同。

xqyun-32c32g 原始验证过程

5.3 xqyun · 从版本锁到可运行仓

阶段 实际处理 证据
1 · 锁定源码 用共享 CodeGraph 定位元戎 RPC 入口,再以 Git 提交及 CMake、Bazel 核实。Mooncake 取 v0.3.14 发布提交及该提交的依赖锁。浏览器缓存曾返回旧 YLT gitlink,下载和构建按 release 的 CMake 锁定。 第 3 节的提交、源路径及 SHA-256。
2 · 采集主机基线 xqyun-32c32g,CentOS Stream 9、Linux 5.14、32 个可用 vCPU、约 30 GiB 可见内存。验证了 cgroup v2 委派与 CPU 亲和。RPM 只下载和解包进实验私有 sysroot,最终使用系统 GCC 11.5、CMake 3.31.8 和私有 Ninja、protobuf 3.14。 原始包中的环境、RPM 下载和编译日志。
3 · 构建与复现 校验 Apache bRPC 与 YLT 源码归档后分开构建。bRPC 构建 shared library,YLT 从固定头文件编译。采用 Apache 源码、关闭 pthread mutex hook,链接实验机提供的依赖。SSH 握手曾间歇中断,已启动的 bRPC 编译继续,重连后用 systemd 单元启动适配器与矩阵。 bRPC 343 个构建步骤、适配器编译日志、CTest 日志、二进制 SHA-256。
4 · 冒烟与负载 三项 CTest 分别覆盖 bRPC、coro_rpc 内联、coro_rpc 独立 handler 池。独立仓内的 CMake、适配器、测试和矩阵脚本与远端实际使用的文件逐项比较。 Git bundle、CTest 记录、每轮配置与回包结果。
5 · 矩阵与运行证据 每种配置做 3 个独立轮次,每轮重新启动两进程。框架顺序随轮次与用例交替。ss 核验连接数。主矩阵记录两端 cgroup CPU、服务端线程和 OS 上下文切换,主机侧同步采集 dstat、iostat。 逐轮 JSON、直方图、线程快照、host counters 和 CSV。

5.4 xqyun · 对照条件与计算口径

  • Linux loopback TCP。bRPC 用 baidu_std / Protobuf,单连接模式每 lane 独立 connection_group。coro_rpc 用 TCP / struct_pack,每 lane 一个独立 client。等长二进制请求与响应,均校验内容。连接已预热,每 lane 只保留一个在途请求。
  • 每端 4 worker 配 4 vCPU。服务端 16–19、客户端 24–27。8 worker 场景各用 8 vCPU,扩到 16–23 与 24–31。监控绑 CPU 15。两端在不同 cgroup 子组记账,CPU 亲和约束不等于专用物理机。8 worker 场景同时改变 worker 和 CPU 预算。
  • 基础矩阵为 64 B 的 1/8/32/128 并发。固定 32 并发再扫 4 KiB/64 KiB、handler 忙等 25 µs、8 worker。每轮先预热 1 秒,测量 4 秒,截止后只收尾已发请求。成功 QPS 以整个测量加收尾窗口计算。
  • P50/P95/P99 来自每轮成功请求直方图的桶上界。到 1024 µs 为 1 µs 粒度,以上逐级扩桶。网页图取同配置轮次中位数,CSV 和原始文件保留单轮值及样本数。闭环会随服务变慢自动降速,结果对应固定在途请求模型。
  • cgroup CPU µs/RPC 以成功调用数为分母。服务端 OS 上下文切换取可见线程计数差。bthread 用户态切栈或 Asio handler 调度次数需各自的框架计数。RSS 为采样峰值。端到端 latency 计时包含调用、响应解码和校验。
  • 解释范围为这台主机上的框架级原生协议。元戎生成 adapter、Auth、Trace 与产品连接池,以及 Mooncake Store 业务池和 Transfer Engine 由源码章节单列。实验的等量是应用字节和连接数,两种协议的编码、内部对象和线上帧长度均保留原生实现。

5.5 xqyun · 控制实验与产物

独立的两连接线程探针在本地 WSL2 执行,固定一个服务端 I/O 线程。慢请求进入后等待人工放行,快请求使用另一条已预热连接。慢 handler 内联时,快请求在放行前返回 0/5。慢 handler 用一个 Asio worker 后,快请求在放行前返回 5/5。TID 核对表明前者快慢均在 I/O 线程,后者只有快请求留在 I/O 线程。探针以 -O0 编译并人为设置 100 ms 等待窗口,结果用于验证线程位置和返回顺序。原始 JSONL 和复现脚本随报告下载。

在 xqyun-32c32g 使用 GCC 11.5 复跑同一源码的两连接探针。主矩阵和 CPU 采样结束后才开始编译和运行。5 组配对均复现内联 0/5、卸载 5/5 的顺序和 TID 关系。

远端阶段 本地时间 UTC+8 核验
构建适配器与等待 bRPC 安装 20:20:36–20:23:43 CTest 3/3,合计 6.55 s
原生 TCP 矩阵 20:23:43–20:27:52 48 个完整轮次,逐轮连接数与有效 lane 一致
服务端独立符号采样 至 20:28:11 64 B、32 并发,每侧 4 worker;cpu-clock 149 Hz、每个服务端采样 4 秒,无丢失样本
coro_rpc 线程位置探针 20:28:13–20:28:52 两种模式各 5 次,两个 RPC 都成功

dstat 记录 CPU、磁盘、网络、分页与系统计数,iostat 记录主机 CPU 和盘队列。两个采集器与同轮 /proc 快照保存在原始包,汇总表另外保留每轮 affinity CPU steal 比例。

06复现与下载

复现基准入口为 RPCBenchmark 的 CMake 适配器和 tools/run_matrix.py。证据包保留主机编排脚本、固定版本、构建依赖 RPM 校验值及审计程序,可根据物理核布局替换亲和性。

RPCBenchmark master(需仓库权限) 含 CMake 和 Bazel 入口。下方 xqyun 源码附件对应原始实验快照。

xqyun-32c32g 原始证据与复现代码

README 记录了依赖、固定版本、两种适配器的启动命令和单机复测步骤。实验脚本支持独立指定客户端、服务端与监控 CPU,另存每轮的 histogram、进程和 cgroup 原始计数。

离线报告 ZIP · SHA-256 清单