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
- 就绪按 fd 分配 EventDispatcher,epoll 等待事件。
- 读与拆帧Socket 启动 urgent bthread,InputMessenger 从 IOBuf 切帧。
- 派发默认多帧时前面的消息另起 bthread,末条在读 bthread 内处理。
- 执行校验并发与元数据,解码 Protobuf,调用服务。
- 回包done 序列化并写入 socket。必要时由 KeepWrite 续写。
coro_rpc · Asio 与 C++20 协程
- 接入accept 循环接收连接,连接分配到 Asio I/O 执行器。
- 读与恢复异步读请求头和 body,I/O 完成后恢复连接协程。
- 派发按函数 ID 找 handler,以 struct_pack 解码参数。
- 执行同步 handler 在当前执行器执行。协程 handler 可挂起等待。
- 回包编码响应,交由连接上的异步写链发送。
03接入与开销
元戎与 Apache bRPC
元戎 bRPC 源码核对
版本与边界
- 元戎基线取工作区
v0.9.2.rc40的c0bbb1582e49ecc2d2d925836d1288633d322012。本地缓存的上游main/master另核对到a128d452510181622c69440783c9a0611f08d82e(2026-11-05),两者均依赖 Apache bRPC1.15.0,发布 commit50a9075de62f1d2b825b902acbfecfddb3d9f314。CMake Apache 源码包 SHA-2560bc8c2aee810c96e6c77886f828fbfdf32ae353ce997eb46f2772c0088010c35。Bazel GitHub tag 包 SHA-256f674b753af71dc313d9d2dcf34f574f0a3438c9f9bb9e7e6ca500a3b0ca7ddfb。两个哈希对应不同打包。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 盲重试与熔断,各方法还可传入更紧的预算。SDKClientWorkerRemoteApi的对象 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,服务端增加原始请求认证捕获,过期拦截器去掉开关。源证据 rc40src/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 路径
- 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。默认 flagsusercode_in_pthread=false、usercode_in_coroutine=false。Dispatcher 初始化与分片 · Linux epoll 循环 · Socket 输入事件 InputMessenger从IOBuf拆baidu_std帧,12BPRPC前缀外还有 protobufRpcMeta与请求 body。默认普通 TCP 一次切出多条消息时,前面的消息走其他 bthread(批量BTHREAD_NOSIGNAL+bthread_flush),末条在读 bthread 内处理。ProcessRpcRequest解码 meta、建 Controller、定位服务和方法、检查并发、调用 interceptor,再获取 Protobuf request/response 实例并解码 body,默认在当前 bthread 直接Service::CallMethod。同一条 TCP 连接允许多条 RPC 并发派发。InputMessenger · baidu_stddone执行SendRpcResponse,序列化 response/meta,拼IOBufbody/attachment 后写 socket。写队首通过_write_head.exchange竞争。连接可写时尝试由当前执行流直接写,写不完、配置后台写等条件走KeepWrite,可能等待EPOLLOUT。共享 socket 的多生产者竞争、写队列与 TCP 缓冲回压都值得单独测。pooled每次取还 socket,空闲满时回收,8 条空闲保留不等于 8 条并发连接,突发超出保留数后的新建及回收也会放大成本。回包打包 · Socket 写路径 · Client 取池连接- Client
Channel::CallMethod序列化、为有 timeout 的调用注册 timer、IssueRPC;done == nullptr时按 correlation ID 同步Join。响应处理用 ID 找回 Controller,解码 response,进入完成逻辑。同步 client 是 OS pthread 还是 bthread 会改变等待与唤醒成本,应写进实验参数。Client 发送与等待 · Client 接收 - 线程模型为 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 分列。 |
元戎生成代码的 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 固定 yaLanTingLibs45c6463c0afbcc46262e373effa30635be3846a0,源码 tar.gz 实算 SHA-256baa6b5c9ffcc0af8252742bba8f0fb9c6d37387c13b2ff5b26d29c6aaff6ee37,与 CMake 相符。同期main快照0ff71517f0f80a198eebff3009834ed00504b982已前进到 YLT54f9c0642386e7860e8e11ada698c484dc562d7d。早先网页缓存中的 submodule gitlink 属于不同源码快照,因此实验口径以发布提交和该提交的 CMake 为准。Mooncake v0.3.14 的 FindYLT.cmake · main 快照的 FindYLT.cmake - 独立实验可直接链接 header-only 的
yalantinglibs::yalantinglibsCMake 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/TENTsubmitTransfer提交,随副本种类和路径配置执行。独立 echo 是原生 RPC 控制面微基准,Store 端到端成本还需副本策略、业务池、锁、数据传输和完成条件的测量。 TransferSubmitter 提交 · TENT RPC 接入
04实验数据
每个点展示同一配置下的轮次中位数。延迟单位为微秒。
当前报告没有可绘制的实验轮次。
同配置跨主机对照
仅配对相同框架、载荷、并发、worker 和 handler 工作量的基线场景。比值为复测主机与原始主机各自轮次中位数的比。CPU、内核、编译器和宿主机负载会共同影响结果。
单轮数据
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复现与下载
- tiantiyun 逐轮 CSV · 中位数 CSV · 完整 JSON
- 主矩阵与跨主机审计 · 跨 NUMA 审计 · dispatcher 对照
- tiantiyun 复现脚本、构建日志与原始采集 · 发布清单
复现基准入口为 RPCBenchmark 的 CMake 适配器和 tools/run_matrix.py。证据包保留主机编排脚本、固定版本、构建依赖 RPM 校验值及审计程序,可根据物理核布局替换亲和性。
RPCBenchmark master(需仓库权限) 含 CMake 和 Bazel 入口。下方 xqyun 源码附件对应原始实验快照。
xqyun-32c32g 原始证据与复现代码
- 独立 Git 仓 bundle · 源码快照 ZIP
-
原始实验数据与构建记录 · 逐轮 CSV · 中位数 CSV · 完整 JSON
git clone ./rpc-framework-lab.bundle rpc-framework-lab cd rpc-framework-lab cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build -j 8 ctest --test-dir build --output-on-failure
README 记录了依赖、固定版本、两种适配器的启动命令和单机复测步骤。实验脚本支持独立指定客户端、服务端与监控 CPU,另存每轮的 histogram、进程和 cgroup 原始计数。