diff --git a/docs/cn/flatbuffers_zero_copy_benchmark/complete_benchmark_report.md b/docs/cn/flatbuffers_zero_copy_benchmark/complete_benchmark_report.md new file mode 100644 index 0000000000..d13c4f4fb6 --- /dev/null +++ b/docs/cn/flatbuffers_zero_copy_benchmark/complete_benchmark_report.md @@ -0,0 +1,329 @@ +# Protobuf / FlatBuffers / Cap'n Proto 测试流程与结果汇总 + +## 1. 测试目标 + +本项目比较以下三种序列化方案: + +- Protobuf 3.21.12 +- FlatBuffers 25.12.19 +- Cap'n Proto 1.5.0 + +测试目标是为 bRPC 社区设计一套面向 TCP、RDMA/URMA 的通用免反序列化/低复制数据传输方案,并回答以下问题: + +1. 三种格式单独进行序列化时的成本有什么区别? +2. Protobuf 反序列化与 FlatBuffers/Cap'n Proto 建立只读视图的成本有什么区别? +3. 将生产者和消费者分成两个进程后,结果是否仍然成立? +4. 接入完整 bRPC TCP 请求—响应流程后,额外的数据复制会带来什么影响? +5. 哪种格式更适合作为 bRPC 免反序列化特性的第一阶段实现? + +## 2. 测试环境 + +| 项目 | 环境 | +|---|---| +| 操作系统 | Ubuntu 24.04 on WSL2 | +| 内核 | 6.18.33.2-microsoft-standard-WSL2 | +| 编译器 | GCC 13.3.0 | +| Protobuf | 3.21.12 | +| FlatBuffers | 25.12.19 | +| Cap'n Proto | 1.5.0 | +| bRPC commit | `6a1c6bfb496f56b77494de89146eb27c6c9ef0dd` | +| bRPC branch | `pr-22-compile-fix` | + +当前全部测试均在本地 WSL2 中完成。尚未使用真实 RDMA/URMA 设备,也尚未得到跨物理机器网络结果。 + +## 3. 测试数据模型 + +### 3.1 Simple 模型 + +Simple 模型包含: + +- Header:request ID、时间戳、版本、来源; +- SimplePayload:一个字节数组。 + +该模型用于观察以连续大块 Payload 为主、元数据较少的场景。 + +### 3.2 Complex 模型 + +Complex 模型包含: + +- Header; +- 16 个 Record; +- 每个 Record 包含 ID、名称、Metrics、samples、两个 Tag 和一部分 Payload。 + +该模型用于观察多层嵌套结构、字符串、数组和重复字段较多的场景。 + +### 3.3 Payload 范围 + +测试覆盖 18 个 Payload: + +```text +64B, 128B, 256B, 512B, +1KiB, 2KiB, 4KiB, 8KiB, +16KiB, 32KiB, 64KiB, +128KiB, 256KiB, 512KiB, +1MiB, 2MiB, 4MiB, 8MiB +``` + +所有方案使用相同的原始字节序列和 checksum 算法。正式结果中 checksum 失败数均为 0。 + +## 4. 已完成的测试层次 + +| 测试 | 进程模型 | 数据传递方式 | 主要目的 | 状态 | +|---|---|---|---|---| +| 库级测试 | 单进程 | 进程内缓冲区 | 分离测量编码、复制、解析/建视图和访问 | 已完成,共 6 轮 | +| IPC 测试 | 两个独立进程 | POSIX 共享内存单槽位 | 测量生产者和消费者分离后的成本 | 已完成,共 3 轮、35,100 条、0 失败 | +| bRPC 测试 | 客户端 + 服务端 | localhost TCP | 测量完整 RPC 请求—响应路径 | 已完成,共 3 轮、35,100 条、0 失败 | + +## 5. 单进程库级测试 + +### 5.1 测试流程 + +```text +构造对象 +→ 序列化 +→ memcpy 到消费者缓冲区 +→ Protobuf 反序列化,或 FlatBuffers/Cap'n Proto 建立视图 +→ 部分字段访问 +→ 完整数据访问 +→ checksum 校验 +``` + +CSV 分别记录: + +- `serialize_ns` +- `copy_ns` +- `deserialize_or_view_ns` +- `partial_access_ns` +- `full_access_ns` +- `end_to_end_ns` +- `encoded_bytes` +- `checksum` +- `success` + +因此,该测试既能单独比较序列化和反序列化,也能比较完整本地数据处理流水线。 + +### 5.2 主要结果 + +- Protobuf 对复杂小消息的编码结果最紧凑。 +- FlatBuffers 和 Cap'n Proto 可以在编码缓冲区上建立视图,不需要构造完整反序列化对象。 +- FlatBuffers 的连续缓冲区和运行稳定性更适合作为工程集成起点。 +- Cap'n Proto 建立 Reader 很快,但部分大消息和复杂对象测试中的波动较大。 +- Payload 增大后,复制和完整内存扫描逐渐成为主要成本。 + +## 6. 双进程共享内存测试 + +### 6.1 测试流程 + +```text +Serializer/Producer 进程 + 构造 simple/complex 对象 + → 序列化 + → memcpy 到 POSIX 共享内存 + → 将槽位状态设为 READY + +Deserializer/Consumer 进程 + 等待 READY + → Protobuf ParseFromArray + 或 FlatBuffers Verifier + GetRoot + 或 Cap'n Proto FlatArrayMessageReader + → 完整访问 Payload + → checksum 校验 + → 将槽位状态设为 CONSUMED +``` + +两个进程地址空间彼此独立,使用一个共享内存槽位进行严格的生产—消费 ping-pong。 + +CSV 分别记录: + +- `serialize_ns`:生产者序列化时间; +- `publish_ns`:复制到共享内存的时间; +- `consumer_parse_or_view_ns`:消费者解析或建视图时间; +- `consumer_access_ns`:消费者完整访问时间; +- `end_to_end_ns`:发布后至消费者处理完成的时间。 + +用于比较的完整流水线时间为: + +```text +serialize_ns + publish_ns + end_to_end_ns +``` + +每轮第一条记录包含人工/进程启动等待,在汇总统计中予以剔除。 + +### 6.2 完整流水线 P50 + +| 模型 / Payload | Protobuf | FlatBuffers | Cap'n Proto | +|---|---:|---:|---:| +| Simple 64B | 0.56 μs | 0.52 μs | 0.54 μs | +| Simple 4KiB | 1.56 μs | 1.35 μs | 1.88 μs | +| Simple 1MiB | 0.81 ms | 0.73 ms | 0.73 ms | +| Simple 8MiB | 9.35 ms | 8.28 ms | 8.05 ms | +| Complex 64B | 13.21 μs | 4.43 μs | 1.97 μs | +| Complex 4KiB | 15.49 μs | 6.78 μs | 3.55 μs | +| Complex 1MiB | 0.66 ms | 0.43 ms | 0.74 ms | +| Complex 8MiB | 7.27 ms | 5.52 ms | 7.95 ms | + +### 6.3 单独解析/建视图 P50(8MiB Complex) + +| 格式 | 解析/建视图时间 | +|---|---:| +| Protobuf | 620 μs | +| FlatBuffers | 4.4 μs | +| Cap'n Proto | 3.1 μs | + +该结果是免反序列化方向最关键的本地证据:FlatBuffers 和 Cap'n Proto 建立视图的成本比 Protobuf 构造完整对象低约两个数量级。 + +### 6.4 单独序列化 P50(8MiB Complex) + +| 格式 | 序列化时间 | +|---|---:| +| FlatBuffers | 4.01 ms | +| Protobuf | 5.33 ms | +| Cap'n Proto | 6.61 ms | + +Complex 中大消息场景下,FlatBuffers 的序列化和完整流水线性能最好。 + +### 6.5 IPC 测试结论 + +- Simple 小消息差别很小。 +- Complex 小消息中 Cap'n Proto 最快,FlatBuffers 次之,Protobuf 构造和解析成本最高。 +- Complex 中大消息中 FlatBuffers 整体表现最好。 +- 对全部 Payload 进行完整扫描时,三种格式都无法避免真实的内存读取成本。 +- 当前生产者仍先编码到临时缓冲区,再复制到共享内存;尚未实现直接在共享/注册内存中原地构建。 + +## 7. 完整 bRPC localhost TCP 测试 + +### 7.1 测试数据路径 + +Protobuf: + +```text +客户端构造原生 Protobuf RPC message +→ bRPC 内部编码 +→ localhost TCP +→ bRPC 自动解析 +→ 服务方法访问数据并校验 +→ 返回小型 BenchmarkResponse +``` + +FlatBuffers/Cap'n Proto: + +```text +客户端编码连续缓冲区 +→ 复制进 bRPC request_attachment/IOBuf +→ localhost TCP +→ 服务端从 IOBuf 复制到连续 vector +→ 建立视图/Reader +→ 访问数据并校验 +→ 返回小型 BenchmarkResponse +``` + +这是完整的 RPC 请求—响应测试,但属于“大请求 + 小响应”,服务端没有将完整 Payload 原样返回。 + +### 7.2 客户端完整流水线 P50 + +| 模型 / Payload | Protobuf native | FlatBuffers attachment | Cap'n Proto attachment | +|---|---:|---:|---:| +| Simple 64B | 69.0 μs | 68.3 μs | 69.1 μs | +| Simple 64KiB | 100.4 μs | 130.1 μs | 133.1 μs | +| Simple 1MiB | 0.87 ms | 1.08 ms | 1.28 ms | +| Simple 8MiB | 9.12 ms | 11.18 ms | 14.10 ms | +| Complex 64B | 87.4 μs | 76.4 μs | 75.4 μs | +| Complex 64KiB | 126.4 μs | 133.4 μs | 149.6 μs | +| Complex 1MiB | 0.61 ms | 0.93 ms | 1.08 ms | +| Complex 8MiB | 6.69 ms | 9.17 ms | 10.86 ms | + +客户端完整流水线按以下口径计算: + +```text +serialize_ns + rpc_roundtrip_ns +``` + +需要注意:Protobuf 的实际线性编码和服务端解析由 bRPC 内部完成,部分成本包含在 `rpc_roundtrip_ns` 中。 + +### 7.3 8MiB Complex 估算吞吐量 + +| 格式 | 吞吐量 | +|---|---:| +| Protobuf native | 约 1195 MiB/s | +| FlatBuffers attachment | 约 872 MiB/s | +| Cap'n Proto attachment | 约 737 MiB/s | + +### 7.4 bRPC 测试结论 + +- 64B Simple 场景三者约为 68~69 μs,主要由 RPC 固定开销主导。 +- 当前 bRPC 路径下,大消息 Protobuf native 最快,FlatBuffers attachment 次之,Cap'n Proto attachment 最慢。 +- 这不能直接证明 Protobuf 格式本身在大消息上更优,因为三种格式使用了不同的 bRPC 数据路径。 +- FlatBuffers/Cap'n Proto attachment 路径多出了客户端写入 IOBuf和服务端复制出 IOBuf 的成本。 +- 当前 CSV 中 `server_access_ns` 实际更接近 attachment 复制、建视图和访问组成的服务端总处理时间,不应解读成纯字段访问时间。 + +## 8. IPC 与 bRPC 结果的关键对照 + +以 8MiB Complex 为例: + +| 格式 | 共享内存双进程 | bRPC localhost TCP | +|---|---:|---:| +| Protobuf | 7.27 ms | 6.69 ms | +| FlatBuffers | 5.52 ms | 9.17 ms | +| Cap'n Proto | 7.95 ms | 10.86 ms | + +FlatBuffers 在共享内存路径中比 Protobuf 快约 24%,但在当前 bRPC attachment 路径中比 Protobuf 慢约 37%。 + +这表明当前的主要问题不是 FlatBuffers 无法带来收益,而是 attachment 路径中的额外复制掩盖了免反序列化收益。 + +## 9. 对 bRPC 免序列化特性的启发 + +仅仅把 FlatBuffers 或 Cap'n Proto 编码结果放入传统 attachment 并不够。建议为 bRPC 设计统一的可寻址数据区域抽象,例如: + +```text +RemoteRegion / ZeroCopyAttachment / SerializedView +``` + +理想路径: + +```text +发送端在可发送/注册内存中构建编码结果 +→ TCP、RDMA 或 URMA 传输 +→ 接收端直接持有接收内存区域 +→ FlatBuffers/Cap'n Proto 在该区域建立只读视图 +→ 按需访问字段 +``` + +第一阶段建议优先适配 FlatBuffers,原因包括: + +- Complex 中大消息的共享内存流水线性能最好; +- 单一连续缓冲区更容易映射到 IOBuf、RDMA 和 URMA 注册内存; +- 建视图成本很低; +- 运行结果总体比 Cap'n Proto 稳定; +- 对现有 bRPC attachment 接口的改造复杂度相对较低。 + +Cap'n Proto 可作为第二阶段适配对象,需要进一步处理 word 对齐、segment、遍历限制和大消息稳定性。 + +## 10. 当前尚未完成的测试 + +以下内容尚未测试: + +- 两台物理机器之间的普通 TCP; +- 大请求 + 大响应的 Payload Echo; +- 多客户端并发和吞吐量饱和; +- CPU 绑核、NUMA 和内存亲和性控制; +- 多槽位共享内存流水线; +- 发送端直接在目标共享/注册内存中原地构建; +- 真实 RDMA 数据路径; +- 真实 URMA 数据路径; +- 最终 bRPC RemoteRegion/ZeroCopyAttachment 特性的 A/B 对照。 + +## 11. 总结 + +当前已经完成: + +1. 单进程中可分项统计的序列化和反序列化/建视图测试; +2. 单个序列化生产者进程与单个反序列化消费者进程的共享内存测试; +3. 客户端与服务端之间完整的 bRPC localhost TCP 请求—响应测试。 + +全部正式测试均覆盖 Protobuf、FlatBuffers、Cap'n Proto、Simple/Complex 和 64B~8MiB,正确性检查全部通过。 + +当前最重要的实验结论是: + +> FlatBuffers/Cap'n Proto 的建视图确实远快于 Protobuf 反序列化,但如果 bRPC attachment 仍需要额外内存复制,这一优势可能被完全抵消。社区特性应同时解决反序列化和缓冲区复制问题,而不是只替换编码格式。 + +本文档中的结果是 WSL2 本地基线,不能替代真实 RDMA/URMA 环境中的最终实验。 diff --git a/docs/cn/flatbuffers_zero_copy_benchmark/deserialization_only_report.md b/docs/cn/flatbuffers_zero_copy_benchmark/deserialization_only_report.md new file mode 100644 index 0000000000..164ea2a1a2 --- /dev/null +++ b/docs/cn/flatbuffers_zero_copy_benchmark/deserialization_only_report.md @@ -0,0 +1,164 @@ +# Protobuf / FlatBuffers / Cap'n Proto 单独反序列化测试总结 + +> 重新生成日期:2026-09-03 +> 数据来源:`ipc-run1.csv`、`ipc-run2.csv`、`ipc-run3.csv`,共 35,100 条记录,失败 0 条。 +> 统计口径:仅使用 `consumer_parse_or_view_ns`;不包含序列化、发布复制、字段访问、IPC 等待和 RPC。 + +## 1. 测试目的 + +本测试只比较消费者已经获得完整编码缓冲区后,将其转换成可读取消息所需的成本: + +- Protobuf:完整反序列化并构造 C++ 对象; +- FlatBuffers:验证缓冲区并取得根对象视图; +- Cap'n Proto:建立 Reader 并取得根对象视图。 + +本文将该指标统一称为 `parse_or_view_ns`。它不包含生产端序列化、跨进程发布、RPC 或完整 Payload 扫描。 + +## 2. 单独反序列化的定义 + +计时起点是消费者已经持有完整、可访问的编码缓冲区,计时终点是得到可供字段访问的消息对象或只读视图: + +```text +已有编码缓冲区 +→ 开始计时 +→ 解析或建立视图 +→ 得到根消息 +→ 停止计时 +``` + +不包括: + +- 发送端对象构造和序列化; +- 编码缓冲区生成; +- memcpy 到共享内存; +- 生产者/消费者等待; +- 部分字段读取和完整 Payload 扫描; +- TCP、bRPC、RDMA 或 URMA。 + +## 3. 三种格式的计时边界 + +### 3.1 Protobuf + +```text +创建空的 SimpleMessage/ComplexMessage +→ ParseFromArray(encoded_buffer) +→ 得到完整 C++ 对象树 +``` + +Protobuf 必须遍历 wire format、分配嵌套对象和字符串/数组,并把字段填充到新对象中。 + +### 3.2 FlatBuffers + +```text +创建 Verifier +→ VerifyBuffer() +→ GetRoot() +→ 得到指向原缓冲区的只读视图 +``` + +FlatBuffers 不创建完整对象副本,但当前测试把完整缓冲区验证计入 `parse_or_view_ns`。 + +### 3.3 Cap'n Proto + +```text +创建 FlatArrayMessageReader +→ getRoot() +→ 得到指向原缓冲区的 Reader +``` + +Cap'n Proto 数据必须满足 word 对齐要求,并设置足够的 traversal limit。当前计时不包含对整个消息进行与 FlatBuffers Verifier 完全等价的全量验证,因此二者的安全检查口径并不完全相同。 + +## 4. 测试流程 + +双进程测试采用: + +```text +Producer 将编码消息发布到 POSIX 共享内存 +→ 槽位状态变成 READY +→ Consumer 直接在共享区域执行解析/建视图 +→ 停止 parse/view 计时 +→ 另行测量完整访问 +→ checksum 校验 +``` + +本文只使用 CSV 中的 `consumer_parse_or_view_ns`,不把 `consumer_access_ns` 加入反序列化结果。 + +模型和 Payload 与序列化测试一致:Simple/Complex,64B~8MiB。正式测试运行三轮且所有 checksum 正确。 + +## 5. 代表性解析/建视图 P50 + +### 5.1 Simple 模型 + +| Payload | Protobuf 解析 | FlatBuffers 验证+建视图 | Cap'n Proto 建 Reader | +|---|---:|---:|---:| +| 64B | 0.122 μs | 0.082 μs | 0.071 μs | +| 4KiB | 0.427 μs | 0.082 μs | 0.071 μs | +| 64KiB | 3.24 μs | 0.080 μs | 0.090 μs | +| 1MiB | 30.10 μs | 0.085 μs | 0.246 μs | +| 8MiB | 603 μs | 0.511 μs | 3.37 μs | + +### 5.2 Complex 模型 + +| Payload | Protobuf 解析 | FlatBuffers 验证+建视图 | Cap'n Proto 建 Reader | +|---|---:|---:|---:| +| 64B | 3.39 μs | 0.992 μs | 0.070 μs | +| 4KiB | 4.24 μs | 1.71 μs | 0.070 μs | +| 64KiB | 8.72 μs | 1.20 μs | 0.096 μs | +| 1MiB | 37.64 μs | 1.09 μs | 0.235 μs | +| 8MiB | 620 μs | 4.44 μs | 3.12 μs | + +## 6. 主要结论 + +1. Protobuf 解析时间随 Payload 增大而明显增长,因为它需要扫描编码数据并构造完整对象。 +2. FlatBuffers 和 Cap'n Proto 主要建立指向原始缓冲区的视图,建视图成本显著更低。 +3. 8MiB Complex 中,Protobuf 约为 620 μs,FlatBuffers 约为 4.4 μs,Cap'n Proto 约为 3.1 μs;后两者比 Protobuf 低约两个数量级。 +4. FlatBuffers 的 Complex 建视图数据包含 Verifier,因此比只建立 Reader 的 Cap'n Proto 更高。 +5. “建视图很快”不等于“完整处理消息不需要时间”。如果业务读取全部 8MiB Payload,内存扫描成本仍然存在。 + +## 7. 反序列化与数据访问必须分开 + +消费者阶段分为: + +```text +parse_or_view_ns +→ 将缓冲区变成可读消息或视图 + +consumer_access_ns +→ 实际遍历字段和 Payload,计算 checksum +``` + +免反序列化主要优化第一部分。对于只读取少数字段的业务,FlatBuffers/Cap'n Proto 可以避免解析和复制未访问字段,收益可能很大;对于必须完整扫描大 Payload 的业务,访问内存的成本无法通过格式本身消除。 + +## 8. 与 bRPC 测试的关系 + +在当前 bRPC 测试中: + +- Protobuf 由 bRPC 在调用服务方法前自动解析,无法在服务方法中单独计时; +- FlatBuffers/Cap'n Proto 需要先从 bRPC IOBuf 复制到连续 vector,再建立视图; +- 额外复制会掩盖免反序列化收益。 + +共享内存测试能够单独观察解析/建视图成本,因此更清楚地证明免反序列化的潜力;bRPC 测试则衡量现有系统中的真实完整路径。 + +## 9. 对特性设计的意义 + +要让本测试中的低建视图成本在 bRPC、RDMA/URMA 中真正发挥作用,接收端必须能直接访问传输完成后的内存区域: + +```text +传输完成 +→ 接收端获得 RemoteRegion/ZeroCopyAttachment +→ 不复制到新的连续 vector +→ 直接验证并建立 FlatBuffers/Cap'n Proto 视图 +→ 按需访问字段 +``` + +FlatBuffers 适合作为第一阶段:它采用单一连续缓冲区、建视图成本低、验证模型清晰,且比 Cap'n Proto 更容易接入 IOBuf 和注册内存。Cap'n Proto 可在第二阶段处理对齐、segment 和 traversal limit 等问题。 + +## 10. 当前限制 + +- 测试位于 WSL2 本地共享内存,不代表跨机器或 RDMA/URMA 延迟; +- 使用单槽 ping-pong 和忙等待,没有测试并发与流水线饱和; +- 没有进行 CPU 绑核和 NUMA 控制; +- FlatBuffers 与 Cap'n Proto 的验证强度不完全一致; +- 本文的反序列化结果不包含字段访问时间,这是有意的指标隔离。 + +当前结论是:FlatBuffers/Cap'n Proto 的建视图成本确实远低于 Protobuf 完整解析;但最终社区方案还必须同时消除接收路径复制,才能在完整 RPC 中兑现这部分收益。 diff --git a/docs/cn/flatbuffers_zero_copy_benchmark/serialization_only_report.md b/docs/cn/flatbuffers_zero_copy_benchmark/serialization_only_report.md new file mode 100644 index 0000000000..ec5b254ff9 --- /dev/null +++ b/docs/cn/flatbuffers_zero_copy_benchmark/serialization_only_report.md @@ -0,0 +1,153 @@ +# Protobuf / FlatBuffers / Cap'n Proto 单独序列化测试总结 + +> 重新生成日期:2026-09-03 +> 数据来源:`ipc-run1.csv`、`ipc-run2.csv`、`ipc-run3.csv`,共 35,100 条记录,失败 0 条。 +> 统计口径:仅使用 `serialize_ns`;不包含发布复制、反序列化、数据访问、IPC 等待和 RPC。 + +## 1. 测试目的 + +本测试只比较三种方案从统一业务数据生成最终可传输编码缓冲区的成本: + +- Protobuf 3.21.12 +- FlatBuffers 25.12.19 +- Cap'n Proto 1.5.0 + +本文不讨论反序列化、共享内存发布、网络传输或 RPC。 + +## 2. 单独序列化的定义 + +本项目将单独序列化定义为: + +```text +准备好的原始 Payload +→ 开始计时 +→ 构造对应格式的 Simple/Complex 消息 +→ 填充 Header、Record、Metrics、Tag 和 Payload +→ 生成最终编码缓冲区 +→ 停止计时 +``` + +计时结果记录在 `serialize_ns`。它包括对象/Builder 创建、字段填充、内存分配、Payload 写入以及生成最终 wire-format 缓冲区。 + +不包括: + +- 将编码结果复制到另一块缓冲区; +- `publish_ns` 共享内存发布; +- Protobuf `ParseFromArray()`; +- FlatBuffers `Verifier`、`GetRoot()`; +- Cap'n Proto `FlatArrayMessageReader`; +- 部分或完整字段访问; +- IPC 等待、TCP、bRPC、RDMA 或 URMA。 + +## 3. 三种格式的计时边界 + +### 3.1 Protobuf + +```text +创建 SimpleMessage/ComplexMessage +→ 填充所有字段 +→ SerializeToString() +→ 得到 std::string 编码结果 +``` + +### 3.2 FlatBuffers + +```text +创建 FlatBufferBuilder +→ 创建 String、Vector 和 Table +→ Finish() +→ 得到 Builder 中的连续编码缓冲区 +``` + +FlatBuffers 没有与 Protobuf 完全相同的“先构造普通对象,再单独编码”阶段;Builder 构造过程本身就是最终内存布局生成过程。 + +### 3.3 Cap'n Proto + +```text +创建 MallocMessageBuilder +→ 初始化结构体和列表 +→ 填充所有字段 +→ messageToFlatArray() +→ 得到连续 word 数组 +``` + +## 4. 测试数据 + +模型: + +- Simple:Header + 单个连续字节数组; +- Complex:Header + 16 个嵌套 Record,每个 Record 包含名称、Metrics、samples、Tag 和一部分 Payload。 + +Payload 覆盖: + +```text +64B、128B、256B、512B、1KiB、2KiB、4KiB、8KiB、 +16KiB、32KiB、64KiB、128KiB、256KiB、512KiB、 +1MiB、2MiB、4MiB、8MiB +``` + +正式 IPC 数据共运行三轮。以下结果取三轮合并后的中位数 P50;每轮第一条进程启动等待记录不参与汇总。 + +## 5. 代表性结果 + +### 5.1 Simple 模型 + +| Payload | Protobuf | FlatBuffers | Cap'n Proto | +|---|---:|---:|---:| +| 64B | 0.176 μs | 0.110 μs | 0.161 μs | +| 4KiB | 0.324 μs | 0.172 μs | 0.233 μs | +| 64KiB | 18.80 μs | 20.77 μs | 17.91 μs | +| 1MiB | 0.640 ms | 0.592 ms | 0.590 ms | +| 8MiB | 7.26 ms | 6.86 ms | 6.78 ms | + +Simple 模型中三者差距总体有限。小消息中 FlatBuffers 最快;8MiB 时 FlatBuffers 和 Cap'n Proto 接近,均略快于 Protobuf。 + +### 5.2 Complex 模型 + +| Payload | Protobuf | FlatBuffers | Cap'n Proto | +|---|---:|---:|---:| +| 64B | 8.32 μs | 3.04 μs | 1.25 μs | +| 4KiB | 9.15 μs | 3.21 μs | 1.30 μs | +| 64KiB | 15.94 μs | 16.42 μs | 30.28 μs | +| 1MiB | 0.475 ms | 0.294 ms | 0.601 ms | +| 8MiB | 5.33 ms | 4.01 ms | 6.61 ms | + +Complex 小消息中 Cap'n Proto 最快,原因是固定嵌套结构的 Builder 构造成本较低;随着 Payload 增大,Cap'n Proto 的连续化成本上升。Complex 1MiB 和 8MiB 中 FlatBuffers 最快。 + +## 6. 主要结论 + +1. 没有一种格式在所有模型和 Payload 下始终最快。 +2. Simple 小消息:FlatBuffers 略优,但绝对差异只有几十到几百纳秒。 +3. Complex 小消息:Cap'n Proto 明显领先,FlatBuffers 次之,Protobuf 最慢。 +4. Complex 中大消息:FlatBuffers 最有优势;8MiB 比 Protobuf 快约 25%,比 Cap'n Proto 快约 39%。 +5. 大消息序列化时间主要由 Payload 写入、内存分配和最终缓冲区生成决定。 +6. Protobuf 对复杂小消息通常编码更紧凑,但紧凑程度和序列化耗时是不同指标。 + +## 7. 公平性说明 + +`serialize_ns` 是“从统一原始数据得到可传输缓冲区”的业务口径,不是只测一个库函数的微基准。这一口径适合比较真实发送端成本,但应注意: + +- Protobuf 构造普通消息对象后再次执行编码; +- FlatBuffers 直接通过 Builder 构造最终布局; +- Cap'n Proto 通过 Builder 构造消息后又执行 `messageToFlatArray()` 连续化。 + +三种库的编程模型不同,无法完全拆成语义相同的内部步骤。 + +## 8. 当前限制与下一步 + +当前生产端仍然执行: + +```text +生成临时编码缓冲区 +→ 后续再复制到共享内存或 bRPC IOBuf +``` + +因此测试已经隔离出序列化时间,但尚未测量“直接在目标共享内存或 RDMA/URMA 注册内存中原地构建”。下一步应为 FlatBuffers 提供目标内存分配器,比较: + +```text +临时缓冲区构建 + memcpy +vs. +直接在可发送/注册内存中构建 +``` + +该测试结果来自 WSL2 本地 CPU 和内存,不能直接视为远端 RDMA/URMA 性能结果。 diff --git a/docs/cn/flatbuffers_zero_copy_design.md b/docs/cn/flatbuffers_zero_copy_design.md new file mode 100644 index 0000000000..f9167aab67 --- /dev/null +++ b/docs/cn/flatbuffers_zero_copy_design.md @@ -0,0 +1,423 @@ +# bRPC FlatBuffers 零拷贝集成与远程内存演进方案 + +> 文档性质:社区 RFC / Feature Proposal 初稿 +> 建议标题:**FlatBuffers Zero-Copy View and Transport-Aware Buffer Integration for bRPC** +> 目标社区:Apache bRPC +> 实施原则:先完善 FlatBuffers + `SingleIOBuf`,再扩展 RDMA/URMA;不在一个 PR 中同时引入序列化框架、协议和新传输层。 + +## 1. 摘要 + +bRPC 已经合入 `SingleIOBuf`,并正在推进 FlatBuffers 的消息构造和协议接入。因此,本方案不重新发明一套 FlatBuffers RPC,而是补齐以下能力: + +1. 客户端直接在 bRPC 管理的连续缓冲区中构造 FlatBuffer,避免 `FlatBufferBuilder -> vector/string -> IOBuf` 的额外复制。 +2. 服务端把收到的连续消息作为只读 FlatBuffers View 暴露给业务代码,避免 `IOBuf -> vector/string -> GetRoot()` 的额外复制。 +3. 在进入业务方法前完成一次有边界的合法性校验,并把底层 Block 生命周期绑定到请求或异步 Closure。 +4. 为现有 bRPC RDMA SEND/RECV 路径提供注册内存分配策略;无法连续分配或消息过大时安全回退。 +5. 后续以独立实验特性增加 `RemoteRegion`,让大对象可以通过 RDMA/URMA 单边读取按需访问,而不是塞入普通 RPC 消息。 + +该方案的核心不是“完全没有序列化”,而是:FlatBuffers 仍需构造线格式,但接收端无需反序列化重建对象,并尽量让构造、传输和访问共享同一块内存。 + +## 2. 背景与现状 + +### 2.1 社区已有基础 + +- bRPC 1.17.0 已引入 `SingleIOBuf`,用于管理单个连续的 `IOBuf::Block`,这是 FlatBuffers 连续内存要求与 bRPC I/O 缓冲区之间的基础桥梁。 +- 社区 FlatBuffers 系列工作已经规划为三步:`SingleIOBuf`、FlatBuffers 消息构造 API、FlatBuffers 协议处理。 +- 因此新贡献应围绕接收 View、校验、生命周期、内存分配策略、基准测试以及 RDMA 适配展开,而不是另建一套相互竞争的接口。 + +### 2.2 当前实验发现的问题 + +现有测试覆盖 Protobuf、FlatBuffers、Cap'n Proto,包含 simple/complex 两类嵌套结构和 64 B~8 MiB payload。 + +本机 WSL 测试的关键现象: + +- 三轮 bRPC localhost TCP 测试共 35,100 条记录,正确性失败为 0。 +- complex 8 MiB 的 bRPC 路径 P50:Protobuf 约 6.69 ms,FlatBuffers 约 9.17 ms。 +- 同一负载在双进程共享内存路径中,complex 8 MiB P50:Protobuf 约 7.27 ms,FlatBuffers 约 5.52 ms。 +- complex 8 MiB 接收端 parse/view P50:Protobuf 约 620 us,FlatBuffers 约 4.4 us。 + +这说明 FlatBuffers 的只读 View 很快,但现有实验 RPC 路径仍把 FlatBuffer 放进 attachment,并在服务端复制为连续 `vector` 后访问。额外内存复制和大块分配掩盖了免反序列化收益。 + +上述结论是根据当前实验实现作出的工程推断,不能直接当作 bRPC 主干实现的性能结论;正式贡献必须用主干和社区正在评审的 FlatBuffers 分支重新复现。 + +## 3. 要解决的问题 + +### 3.1 功能问题 + +1. FlatBuffers 要求连续字节区,而普通 `IOBuf` 可能由多个 Block 组成。 +2. attachment 只是非结构化字节,并不能提供类型安全的 FlatBuffers RPC 方法签名。 +3. 直接 `GetRoot()` 不代表数据合法;网络输入必须校验。 +4. View 中的指针依赖底层消息内存,异步服务容易产生悬空引用。 +5. RDMA 注册内存、普通堆内存和远程 Region 的所有权与释放方式不同。 + +### 3.2 性能问题 + +需要消除或量化以下复制: + +```text +业务对象 + -> FlatBufferBuilder 内部缓冲区 + -> string/vector + -> IOBuf + -> Socket/RDMA 缓冲区 + -> 接收 IOBuf + -> string/vector + -> FlatBuffers View +``` + +理想的首阶段路径为: + +```text +业务对象 + -> SingleIOBuf-backed MessageBuilder + -> bRPC 协议头 + 同一数据 Block + -> 接收侧 SingleIOBuf + -> Verified FlatBuffers View +``` + +## 4. 范围与非目标 + +### 4.1 首版范围 + +- C++ 客户端和服务端。 +- `baidu_std` 协议或社区当前 FlatBuffers PR 选定的协议路径。 +- TCP localhost、TCP 双机和现有 bRPC RDMA SEND/RECV。 +- FlatBuffers schema 生成的 simple/complex RPC。 +- 同步和异步服务的内存生命周期测试。 +- 64 B~8 MiB 基准与错误输入测试。 + +### 4.2 首版非目标 + +- 不替代 Protobuf;Protobuf 继续承担 IDL、控制面或兼容路径。 +- 不声称发送端“免序列化”;FlatBuffers 构造本身仍有成本。 +- 不在首个 PR 中实现完整 URMA 传输层。 +- 不要求任意分段 `IOBuf` 都可直接成为一个 FlatBuffer。 +- 不把 FlatBuffers、Cap'n Proto、URMA 和 RDMA 同时塞进一个巨型 PR。 + +## 5. 用户接口设计 + +以下接口应尽量复用社区现有 `brpc::flatbuffers::MessageBuilder` 和 `Message`,最终名称以现有 PR 为准。 + +### 5.1 构造与发送 + +```cpp +brpc::flatbuffers::BuilderOptions options; +options.initial_capacity = payload_size; +options.protocol_headroom = 64; +options.storage = brpc::flatbuffers::StoragePolicy::kAuto; + +brpc::flatbuffers::MessageBuilder builder(options); +auto request = CreateRequest(builder, /* fields */); +builder.Finish(request); + +brpc::flatbuffers::Message message = builder.ReleaseMessage(); +stub.Exchange(&controller, &message, &response, nullptr); +``` + +`StoragePolicy::kAuto` 的语义: + +- 普通 TCP:使用适合 `SingleIOBuf` 的连续 Block。 +- RDMA 已启用且容量满足:优先从注册内存池分配。 +- 无法满足时:回退普通内存或现有序列化路径,并暴露统计计数。 + +### 5.2 接收与访问 + +```cpp +void Exchange(google::protobuf::RpcController* cntl_base, + const brpc::flatbuffers::Message* request, + brpc::flatbuffers::Message* response, + google::protobuf::Closure* done) override { + brpc::ClosureGuard done_guard(done); + + auto root = request->GetVerifiedRoot(); + if (!root.ok()) { + static_cast(cntl_base) + ->SetFailed(EINVAL, "invalid FlatBuffers request"); + return; + } + Use(root->payload()); +} +``` + +建议增加的核心抽象: + +```cpp +struct VerifyOptions { + size_t max_message_bytes; + size_t max_depth; + size_t max_tables; +}; + +template +StatusOr> GetVerifiedRoot( + const VerifyOptions& options = {}) const; +``` + +`VerifiedView` 同时持有: + +- `const T*` 根对象; +- 底层 Block 的只读所有权引用; +- 已验证标记; +- 消息大小和可选 schema/type 标识。 + +不应向用户返回一个脱离所有权的裸指针。 + +## 6. 内部实现 + +### 6.1 发送端 + +1. `MessageBuilder` 使用现有 Slab/Block allocator 获取一个连续 Block。 +2. Block 前部预留 bRPC 协议头空间,FlatBuffers 从后续位置构造。 +3. `Finish()` 后冻结可写状态。 +4. `ReleaseMessage()` 转移 Block 引用,不复制 payload。 +5. 协议打包器只追加/引用该 Block,不调用 `to_string()` 或中间 `vector`。 + +必须增加调试断言或计数,确认消息打包期间没有发生 payload 字节复制。 + +### 6.2 接收端 + +1. 协议解析器识别消息类型、长度和可选 schema 标识。 +2. 若 payload 已在单个连续 Block 中,直接建立 `Message`。 +3. 若 payload 分段: + - 小消息可合并到一个连续 Block; + - 大消息默认回退并记录 `flatbuffers_receive_coalesce_bytes`; + - 不允许把不连续内存伪装成连续 FlatBuffer。 +4. 使用 `flatbuffers::Verifier` 做一次有上限校验。 +5. 业务方法得到 `VerifiedView`;请求完成或异步回调释放前,Block 必须存活。 + +### 6.3 生命周期状态 + +```text +Writable Builder + | + Finish + v +Frozen Message ---- send/in-flight ----> Received Message + | + Verify + v + Verified View + | + RPC/Closure 完成后释放 +``` + +约束: + +- Frozen 后不可修改。 +- View 不可跨越其 Block owner 生命周期。 +- 异步保存 View 时必须显式保留 owner,而不是只保存 `const T*`。 +- 同一个未声明线程安全的 builder 不得并发写。 + +### 6.4 协议元数据 + +首版建议只加入最少元数据: + +```text +encoding = flatbuffers +schema/type = stable type id(可选) +payload_size = N +flags = verified / compressed / remote-region +``` + +不要把 C++ RTTI 名字写入线协议。类型 ID 应稳定、跨编译器,并支持版本演进。压缩与零拷贝天然冲突:启用压缩时应明确退化为解压到新缓冲区。 + +## 7. RDMA 与 URMA 演进 + +### 7.1 现有 RDMA SEND/RECV + +bRPC RDMA 已经围绕 `IOBuf::Block` 和注册内存池实现零拷贝能力。FlatBuffers 可先复用该能力,但存在一个关键限制:FlatBuffer 需要一整块连续内存,而现有 RDMA 接收池常用固定大小 Block;8 MiB 消息不一定能由单个现有 Block 承载。 + +建议新增内部策略,而非立刻修改公开 API: + +```cpp +enum class RegisteredAllocationResult { + kRegisteredContiguous, + kNormalContiguous, + kSegmentedFallback, + kRejectedTooLarge, +}; +``` + +并提供: + +- 小/中消息注册连续块池; +- 大消息按需注册或大块池,带容量上限; +- 注册失败、内存压力或超限时回退; +- 指标记录实际走到的路径。 + +### 7.2 后续 RemoteRegion / URMA + +当 payload 很大且业务只访问少量字段时,把整个 8 MiB FlatBuffer主动发送到服务端仍不理想。后续可引入独立的远程区域描述符: + +```cpp +struct RemoteRegionDescriptor { + uint64_t region_id; + uint64_t remote_address; + uint64_t length; + uint32_t access_key; + uint32_t provider_id; // RDMA / URMA + uint64_t lease_id; +}; +``` + +控制面通过普通 bRPC 传递 descriptor,数据面由 provider 执行 RDMA/URMA Read。接收端可按需拉取 FlatBuffer 的索引或数据页,并通过 lease 保证远端内存仍有效。 + +这一阶段需要另行解决: + +- FlatBuffers 偏移访问跨远程页时的读取和缓存; +- lease、撤销、超时和断连清理; +- rkey/token 的认证与越界检查; +- 分页读取与预取策略; +- TCP fallback; +- URMA 设备能力探测和 provider 插件化。 + +因此 RemoteRegion 应是后续 RFC,而不是 FlatBuffers 首次集成的合入条件。 + +## 8. 安全与健壮性 + +必须包含以下保护: + +- 网络输入默认验证,不能只调用 `GetRoot()`。 +- 最大消息大小、最大嵌套深度和对象数量限制。 +- 长度加法、偏移和对齐的溢出检查。 +- schema/type 不匹配时明确失败。 +- fuzz:截断、随机偏移、超大 vector、非法 vtable。 +- Block 只读冻结,防止验证后修改(TOCTOU)。 +- RDMA/URMA descriptor 必须校验权限、长度、租约和连接身份。 +- 记录 fallback,避免“看起来是零拷贝,实际发生了合并复制”。 + +## 9. 可观测性 + +建议加入以下 bvar 或等价指标: + +- `flatbuffers_requests_total` +- `flatbuffers_verify_failures_total` +- `flatbuffers_builder_reallocations_total` +- `flatbuffers_send_copy_bytes` +- `flatbuffers_receive_coalesce_bytes` +- `flatbuffers_contiguous_fast_path_total` +- `flatbuffers_fallback_total{reason}` +- `flatbuffers_registered_block_total` +- `flatbuffers_registered_allocation_failures_total` +- `flatbuffers_remote_read_bytes`(后续) + +只有把复制字节数作为一等指标,基准结果才能说明是真正的零拷贝,而不是仅仅 API 名称如此。 + +## 10. 测试与验收 + +### 10.1 正确性矩阵 + +| 维度 | 取值 | +|---|---| +| Schema | simple、complex nested | +| Payload | 64 B~8 MiB,2 的幂 | +| Format | Protobuf、FlatBuffers;Cap'n Proto 仅作 benchmark 对照 | +| Transport | localhost TCP、双机 TCP、现有 RDMA | +| Invocation | sync、async | +| Buffer path | contiguous、segmented fallback、allocation failure | + +每个组合检查:字段值、checksum、encoded bytes、错误码和生命周期。 + +### 10.2 性能指标 + +分别报告,禁止只给一个模糊的“端到端”: + +- build/serialize latency; +- protocol pack latency; +- copied bytes; +- RPC round-trip latency; +- verify latency; +- first-field、sparse、full-scan access latency; +- QPS、CPU cycles、allocations、峰值内存; +- P50/P95/P99,而不只平均值。 + +建议首版验收目标: + +1. 所有正确性组合零失败。 +2. 连续快路径中不出现 payload 大小级别的 `IOBuf -> vector/string` 复制。 +3. complex 8 MiB FlatBuffers RPC 相比当前 attachment 实验至少降低 20% 的客户端构造至服务端访问总耗时;最终阈值以社区 CI/测试机复测为准。 +4. complex 8 MiB 服务端 view 初始化保持在微秒级,且不包含全量复制。 +5. Protobuf 和普通 attachment 基准无显著回退。 +6. ASan、UBSan、TSan(适用用例)及 fuzz 测试通过。 + +## 11. 社区贡献拆分 + +### PR 0:RFC 与可复现基准 + +- 先在 Issue/RFC 中对齐当前 #3196/#3197 的状态和接口。 +- 提交 simple/complex、64 B~8 MiB benchmark。 +- 增加 copied-bytes、allocation 和 verification 指标。 +- 明确现有 attachment 基准不是 FlatBuffers 原生集成结果。 + +### PR 1:API 加固与接收 View + +- 在现有 `Message` 上增加有界 verifier API。 +- 定义 owner-carrying `VerifiedView`。 +- 补充 null root、错误 schema、截断数据和异步生命周期测试。 +- 修复社区评审已发现的空字段和 descriptor 生命周期问题。 + +### PR 2:连续快路径 + +- `MessageBuilder -> SingleIOBuf -> protocol` 无中间 payload 复制。 +- 接收端连续 Block 直接建立 Message/View。 +- 分段数据合并与明确 fallback 指标。 +- TCP benchmark 和回归测试。 + +### PR 3:现有 RDMA 注册内存适配 + +- transport-aware 内部分配器。 +- 注册连续 Block 池、容量上限和失败回退。 +- RDMA 双机测试;没有 RDMA 设备的 CI 使用 mock allocator。 + +### PR 4:实验性 RemoteRegion provider + +- RDMA provider 和 URMA provider 统一接口。 +- descriptor、lease、权限和远程读状态机。 +- 仅在独立构建开关下启用,成熟后再讨论公共 API 稳定性。 + +## 12. 建议目录布局 + +```text +src/brpc/flatbuffers/ + message.h/.cpp + message_builder.h/.cpp + verified_view.h + verifier_options.h + block_allocator.h/.cpp + +test/flatbuffers/ + message_builder_test.cpp + verified_view_test.cpp + malformed_message_test.cpp + async_lifetime_test.cpp + protocol_roundtrip_test.cpp + +example/flatbuffers_c++/ + echo.fbs + client.cpp + server.cpp + +test/benchmark/ + flatbuffers_rpc_benchmark.cpp +``` + +实际路径应服从 #3196/#3197 已采用的目录,避免在它们合入前制造平行实现。 + +## 13. 向社区提交时的说明模板 + +> bRPC 已有 SingleIOBuf,并正在加入 FlatBuffers message/protocol support。本提案希望在现有实现上补充 verified zero-copy receive view、明确的 buffer lifetime、copy/fallback observability,以及现有 RDMA registered-block integration。我们的初步 benchmark 显示,FlatBuffers 在 8 MiB complex 消息上的 view 初始化只需微秒级,但 attachment 路径中的整块复制会掩盖这一优势。计划先提交可复现 benchmark 和 API/lifetime tests,再分别提交 TCP contiguous fast path、RDMA registered allocator,最后以实验 RFC 讨论 URMA RemoteRegion。 + +## 14. 推荐的近期行动 + +1. 把本地 bRPC 切到最新主干,在独立分支检查 `SingleIOBuf` 实际 API。 +2. 拉取或基于 #3196/#3197 分支构建,不从零复制一套 FlatBuffers service API。 +3. 将现有 benchmark 改成社区 MessageBuilder/Message API,删除服务端 `IOBuf -> vector`。 +4. 增加 copied-bytes 与 allocation 计数后重新跑 TCP 三轮。 +5. 整理最小复现、结果表和 flame graph,先发 Discussion/Issue 征求维护者意见。 +6. 获得接口方向确认后,从 PR 1 开始提交小而独立的改动。 + +## 15. 结论 + +该特性可行,但合适的社区贡献不是笼统的“给 bRPC 加 FlatBuffers”,因为基础工作已经存在。最有价值且可合入的方向是:让现有 FlatBuffers 消息真正贯通 `SingleIOBuf`、协议层和接收端只读 View;用验证、生命周期和可观测性保证它可安全用于生产;然后复用 bRPC RDMA 注册内存,最后再把 URMA/RDMA 单边远程内存作为独立演进层。 + +这一路线既能直接解释并改善当前 benchmark 中暴露的复制瓶颈,也能为后续“Over URMA/RDMA 通用免反序列化方案”提供稳定的消息对象和内存所有权基础。