diff --git a/docs/cn/brpc_flatbuffers_design_talk.md b/docs/cn/brpc_flatbuffers_design_talk.md new file mode 100755 index 0000000000..464d3640ba --- /dev/null +++ b/docs/cn/brpc_flatbuffers_design_talk.md @@ -0,0 +1,529 @@ +# bRPC FlatBuffers 免反序列化与面向 RDMA/URMA 的通用数据视图方案 + +## 1. 本次会议希望解决什么 + +本次会议不是评审一份已经完成的最终代码,也不是立即决定完整的 RDMA/URMA 实现,而是希望确认第一阶段方案的方向和边界: + +1. 是否值得在 bRPC 中支持 FlatBuffers 免反序列化访问; +2. 第一阶段是否应先完成 FlatBuffers over TCP; +3. FlatBuffers 应以独立 RPC protocol 还是 attachment 数据视图的形式接入; +4. 怎样保证数据校验、内存所有权和访问生命周期安全; +5. 怎样为后续 RDMA/URMA 的远端内存访问留下通用扩展接口。 + +一句话概括方案: + +> 为 bRPC 增加一种以 FlatBuffers 为首个落地对象、面向 TCP/RDMA/URMA 演进的免反序列化数据访问能力,减少大对象 RPC 中的编解码和内存复制开销。 + +这里优先使用“免反序列化访问”这一表述,而不直接承诺“全链路零拷贝”。是否发生复制还取决于 IOBuf 分片、网络接收布局和缓冲区生命周期。 + +--- + +## 2. 背景与问题 + +bRPC 当前主要围绕 Protobuf 构建 RPC 接口。传统处理流程通常是: + +```text +业务对象 + → Protobuf 序列化 + → 网络传输 + → Protobuf 反序列化 + → 构造接收端对象 + → 业务访问 +``` + +在普通小请求中,这套机制成熟且易用。但在 AI Infra、张量、大型嵌套结构体和大对象传输场景中,可能产生以下成本: + +- 发送端遍历对象并编码; +- 接收端重新解析并构造对象; +- 网络缓冲区、连续缓冲区和业务对象之间发生内存复制; +- 大对象占用较多 CPU 时间和内存带宽; +- 业务只访问少数字段时,完整反序列化可能做了大量无效工作。 + +因此要研究的问题是: + +> 当收到的数据本身已经具有可直接访问的布局时,bRPC 能否安全地向业务提供一个数据视图,而不是立即把整条消息还原成另一套 C++ 对象? + +本方案不是替换 Protobuf。Protobuf 继续服务于兼容性和通用 RPC 场景;FlatBuffers 作为可选能力,服务于对大对象、按需访问和内存复制成本敏感的场景。 + +--- + +## 3. 为什么先选择 FlatBuffers + +FlatBuffers 将表、向量和字符串组织在一个可随机访问的编码缓冲区中。接收端在完成边界和结构校验后,可以直接通过生成的访问器读取字段,不必把整个消息重新构造成普通对象。 + +三种已测试方案的定位如下: + +| 方案 | 接收端典型行为 | 主要特点 | +|---|---|---| +| Protobuf | 解析字节并构造消息对象 | 生态成熟、兼容性好 | +| FlatBuffers | 在连续编码缓冲区上建立视图 | 适合免反序列化和按需访问 | +| Cap’n Proto | 在其消息布局上建立 reader | 同样偏向原位访问,但布局及生态不同 | + +选择 FlatBuffers 作为第一个实现,不表示它在所有场景中一定最快。它适合验证三个关键问题: + +- bRPC 怎样持有编码缓冲区; +- 数据视图的生命周期怎样与缓冲区绑定; +- 业务怎样在不构造完整对象的情况下安全访问字段。 + +--- + +## 4. 前期完成的测试 + +测试统一覆盖: + +- Protobuf、FlatBuffers、Cap’n Proto; +- simple 和 complex 两类结构; +- 64B 至 8MiB 的 payload; +- checksum 正确性校验; +- 多轮重复运行。 + +### 4.1 单进程序列化框架基准 + +在同一进程中分别记录: + +- 序列化时间; +- 编码后大小; +- 数据复制时间; +- 反序列化或视图初始化时间; +- 部分字段访问时间; +- 完整访问时间。 + +这组测试主要用于分离“格式本身的成本”和“网络传输成本”。 + +### 4.2 独立生产者与消费者测试 + +使用两个独立进程和共享内存区域: + +- producer 只负责构造、序列化并发布数据; +- consumer 只负责解析或建立视图、访问数据并校验; +- 分开记录生产端和消费端的耗时。 + +这组测试用于模拟发送端与接收端分离的情形。共享内存不是 RDMA,但可以先验证数据区域、发布状态和视图生命周期等机制。 + +### 4.3 完整 bRPC TCP RPC 测试 + +客户端构造三种格式的数据,通过 bRPC 发给服务端。服务端解析或建立视图、访问字段、计算 checksum,并通过 RPC 返回结果。 + +这组测试用于验证格式进入真实 RPC 链路后,序列化成本、RPC 往返成本、服务端解析或视图建立成本和访问成本之间的关系。 + +### 4.4 当前测试得到的启发 + +- 三种格式均能覆盖 simple/complex 和 64B~8MiB 数据,并通过正确性校验; +- FlatBuffers 和 Cap’n Proto 的接收端可以避免传统的完整对象重建; +- FlatBuffers 的主要潜在收益位于接收端视图建立和按需访问,而不是保证所有端到端场景都更快; +- 小消息中,RPC 固定开销可能掩盖序列化差异; +- 大消息中,内存复制、缓冲区连续性和完整扫描成本变得更加重要; +- 因此正式测试必须同时报告序列化、解析或视图初始化、访问、复制和 RPC 往返时间,不能只报告一个总耗时。 + +--- + +## 5. RMMap 论文对方案的启发 + +论文带来的核心启发不是直接照搬其全部实现,而是把“数据所在区域”和“业务访问方式”分开: + +1. 不要在收到数据后无条件复制并重建完整对象; +2. 用数据区域抽象保存地址、长度、权限、所有权和生命周期; +3. 在区域之上建立经过验证的类型化视图; +4. 根据业务访问行为直接访问或按需获取数据; +5. 让本地缓冲区、RDMA 注册内存和 URMA 区域尽量共享上层访问模型。 + +对应到 bRPC: + +```text +FlatBuffers 是第一种格式 +TCP IOBuf 是第一种本地数据区域来源 +RDMA/URMA 是后续的数据区域传输与访问方式 +``` + +因此,FlatBuffers 不应直接依赖 RDMA;RDMA/URMA 的区域抽象也不应只服务于 FlatBuffers。 + +--- + +## 6. 为什么调研 Apache bRPC PR #3196 和 #3197 + +Apache bRPC 社区此前已经尝试过接入 FlatBuffers。调研这两个历史 PR 的目的不是直接复制旧代码,而是确认社区曾经怎样拆分问题、哪些机制可以复用,以及哪些设计在当前主线上已经不适用。 + +### 6.1 PR #3196:FlatBuffers 消息构造和内存管理 + +该 PR 主要引入: + +- FlatBuffers `MessageBuilder`; +- FlatBuffers 消息包装对象; +- `ReleaseMessage()`; +- `ParseFbFromIOBUF()`; +- `SerializeFbToIOBUF()`; +- FlatBuffers buffer 与 `butil::IOBuf` 的衔接。 + +它回答的是: + +> FlatBuffers 消息怎样在 bRPC 的内存体系中构造、持有、发送和访问? + +这是消息构造和数据面的基础。 + +### 6.2 PR #3197:完整 RPC 协议接入 + +该 PR 主要涉及: + +- 新增 FlatBuffers RPC protocol; +- 协议注册; +- Channel、Controller 和 Server 调用路径; +- 请求打包和响应解析; +- FlatBuffers service/method descriptor; +- 客户端 stub 与服务端 service 分发。 + +它回答的是: + +> FlatBuffers 消息怎样进入 bRPC 完整的请求和响应链路? + +两个 PR 的关系是: + +```text +PR #3196:消息怎样构造、保存和访问 + ↓ +PR #3197:消息怎样进入完整的 bRPC RPC 链路 +``` + +### 6.3 调研的实际意义 + +- 避免重复实现 builder、IOBuf 适配和协议注册等机制; +- 找到非 Protobuf 格式接入 bRPC 时真正需要修改的边界; +- 验证连续缓冲区上的免反序列化路径在工程上可行; +- 发现 bRPC 某些接口仍强依赖 Protobuf descriptor; +- 提前发现输入验证、生命周期、分片和构建依赖等风险; +- 为是否基于旧实现继续演进提供代码依据。 + +一句话总结: + +> #3196 和 #3197 帮助我们确认“能不能做”和“要改哪里”;当前工作要进一步解决“怎样安全、可维护、可验证地做”。 + +--- + +## 7. 当前原型进展 + +当前在 `prototype/flatbuffers-builder-port` 分支进行原型验证。 + +已经完成: + +- 将 PR #3196 的 builder/message 机制移植到当前主线; +- builder 部分已经形成独立原型提交; +- 将 PR #3197 的 protocol 改动应用到当前主线; +- 解决旧 PR 与当前主线之间的冲突; +- 统一 FlatBuffers 相关命名空间; +- 重新生成与当前 FlatBuffers 版本兼容的示例代码; +- bRPC 静态库成功编译; +- `MessageBuilder::ReleaseMessage()`、`ParseFbFromIOBUF()` 和 `SerializeFbToIOBUF()` 已进入 `libbrpc.a`; +- FlatBuffers 示例客户端和服务端已经成功构建。 + +尚未完成: + +- protocol 移植代码尚未整理成正式提交; +- 示例 RPC 运行验证尚未完成; +- 还没有完整的请求和响应数据一致性测试; +- 服务端尚未形成强制 `Verifier` 的安全路径; +- 当前存在原型性质的 descriptor 类型兼容处理; +- 尚未补齐单元测试、异常报文测试和性能基准; +- 尚未在 RDMA/URMA 设备上测试。 + +因此当前状态应描述为: + +> FlatBuffers builder 和 protocol 的可编译原型已经打通,但尚未达到可以向社区提交正式功能代码的程度。 + +--- + +## 8. 已确认的技术边界 + +### 8.1 免反序列化不等于全链路零拷贝 + +FlatBuffers 可以让业务直接读取编码缓冲区,但不保证数据从网卡到业务访问之间完全不发生复制。 + +当前 `SingleIOBuf::assign()` 的行为表明: + +- 数据位于单个连续 IOBuf block 中时,可以直接引用该区域; +- 数据跨多个 IOBuf block 时,为满足 FlatBuffers 连续布局要求,可能需要申请连续内存并合并复制。 + +所以准确的承诺应该是: + +> 提供免反序列化访问,并在连续缓冲区条件满足时提供零拷贝快路径;分片情况下允许安全降级为合并复制。 + +### 8.2 数据安全验证 + +`flatbuffers::GetRoot()` 本身不等于完成不可信输入校验。网络输入应先通过 `flatbuffers::Verifier`,验证成功后才能暴露类型化视图。 + +还需要限制: + +- 最大消息长度; +- 嵌套深度; +- vector 和 string 的边界; +- 截断和畸形报文; +- 校验失败后的错误传播。 + +### 8.3 生命周期 + +FlatBuffers 对象只是指向底层字节的视图。如果 IOBuf、共享区域或 RDMA region 被提前释放,视图会悬空。 + +因此 API 必须保证: + +```text +View 的有效期 ≤ Region/Buffer 的有效期 +``` + +业务若要在 RPC 回调结束后继续持有数据,必须显式取得共享所有权或复制数据。 + +### 8.4 类型安全 + +当前 bRPC 的部分协议接口假定 method descriptor 是 Protobuf 类型。原型中为了验证链路存在临时类型兼容处理,但正式方案不能依赖不安全的强制类型转换。 + +正式实现应采用: + +- 独立的 FlatBuffers pack/dispatch 回调;或 +- 明确带类型标签的通用方法描述符;或 +- 更上层的统一 RPC method abstraction。 + +--- + +## 9. 建议的核心抽象 + +总体路径: + +```text +业务数据 + ↓ +FlatBuffers Builder + ↓ +Encoded Buffer / IOBuf + ↓ +bRPC Transport(TCP → RDMA/URMA) + ↓ +Owned Serialized Region + ↓ +Verified Typed View + ↓ +业务按需访问字段 +``` + +### 9.1 SerializedRegion:数据区域 + +负责描述: + +- 数据地址与长度; +- 连续或分片状态; +- 只读或可写属性; +- 内存所有权; +- 引用计数和生命周期; +- 释放回调; +- 后续可扩展的 RDMA/URMA 注册信息和访问句柄。 + +### 9.2 VerifiedView:验证后的类型化视图 + +负责: + +- 持有或引用 `SerializedRegion`; +- 执行格式校验; +- 在验证成功后提供 `GetRoot()`; +- 保证访问期间底层区域有效; +- 禁止未经验证的不可信网络数据直接暴露给业务。 + +### 9.3 Transport Adapter:传输适配层 + +负责把不同来源的数据转化为统一区域: + +- TCP `IOBuf`; +- 共享内存; +- RDMA 注册内存; +- URMA 内存区域。 + +格式与传输解耦后,FlatBuffers 可以运行在 TCP、RDMA 或 URMA 上;其他适合原位访问的格式也可以复用相同的区域和生命周期机制。 + +--- + +## 10. 分阶段实施建议 + +### V1:FlatBuffers over TCP + +目标是先做出安全、可测试、可维护的 FlatBuffers RPC 能力: + +- FlatBuffers 作为可选依赖,默认不影响现有用户; +- 提供 builder/message/view API; +- 使用类型安全的 FlatBuffers method descriptor; +- 接收端默认或强制执行 `Verifier`; +- 明确 view 与 IOBuf 的生命周期关系; +- 单 block 时走直接引用快路径; +- 多 block 时允许合并复制并记录降级; +- 增加正确性、安全性和性能测试。 + +### V2:提取通用 SerializedRegion + +- 从 FlatBuffers 专用实现中提取传输无关的数据区域; +- 支持连续与分片区域描述; +- 增加复制次数、复制字节数和降级原因观测; +- 让不同序列化格式共享区域所有权和传输接口; +- 评估 scatter/gather 与连续视图之间的边界。 + +### V3:接入 RDMA/URMA + +- 注册内存与 region handle; +- 远端地址、访问权限和 rkey 等元数据; +- region lease 和撤销机制; +- 按需读取或直接映射策略; +- 超时、断连、重试和并发安全; +- 本地、TCP、RDMA 和 URMA 路径的统一语义。 + +V1 不需要 RDMA 环境;V3 才需要在具有 RDMA/URMA 设备的原生 Linux 服务器上验证。 + +--- + +## 11. 正式实现需要补齐的测试 + +### 正确性测试 + +- simple/complex 结构; +- 空字段、可选字段、字符串、vector 和嵌套对象; +- 64B~8MiB; +- 请求和响应双向校验; +- checksum 与字段级一致性。 + +### 安全性测试 + +- 截断 buffer; +- 错误 root offset; +- 非法 vector/string 长度; +- 超大消息; +- 校验失败后不得调用业务方法; +- fuzz 测试。 + +### 生命周期测试 + +- RPC 回调期间访问; +- 回调结束后的非法访问防护; +- 异步回调; +- 超时、取消和重试; +- 并发请求; +- buffer 释放次数和引用计数。 + +### 性能测试 + +- 序列化时间; +- parse/view 初始化时间; +- 部分访问与完整访问时间; +- RPC latency 的 P50/P95/P99; +- throughput; +- CPU 使用率; +- 内存峰值; +- 复制次数与复制字节数; +- 单 block 与多 block 的差异。 + +--- + +## 12. 当前主要风险 + +1. 历史 PR 与当前主线差距较大,不能直接作为正式实现; +2. bRPC 现有接口存在 Protobuf 类型假设; +3. FlatBuffers 需要连续数据,IOBuf 分片可能触发复制; +4. 未经 `Verifier` 的网络数据存在安全风险; +5. view 生命周期处理不当会产生悬空指针; +6. FlatBuffers 版本和生成代码需要明确兼容策略; +7. 引入依赖不能影响默认 bRPC 构建; +8. TCP 原型结果不能直接等价于 RDMA/URMA 效果。 + +--- + +## 13. 希望会议形成的决策 + +建议围绕以下问题逐项确认: + +1. 是否接受“先 FlatBuffers over TCP,再抽象 RDMA/URMA”的实施顺序? +2. V1 是否只承诺免反序列化和条件式零拷贝快路径? +3. FlatBuffers 应作为独立 bRPC protocol,还是作为 attachment 上的类型化视图? +4. 是否接受 FlatBuffers 为可选编译依赖? +5. 接收端是否必须默认启用 `Verifier`? +6. 多分片输入第一版是否允许合并复制? +7. view 是否允许跨 RPC 回调持有;如果允许,采用什么所有权接口? +8. 当前历史 PR 原型中哪些部分可以复用,哪些部分必须重写? +9. 哪些 benchmark 和安全测试应作为合入门槛? +10. RDMA/URMA 是否纳入当前接口设计,但推迟到后续版本实现? + +--- + +## 14. 建议的现场串讲话术 + +### 开场 + +> 今天想讨论的是 bRPC 在大对象 RPC 场景中的免反序列化能力。我们并不是要替换 Protobuf,也不是今天就实现完整 RDMA/URMA,而是希望先以 FlatBuffers over TCP 验证一个安全的数据区域和类型化视图模型。 + +### 介绍问题 + +> 当网络数据已经具有可直接访问的布局时,如果接收端仍然把整条消息重新构造成一套对象,会额外消耗 CPU 和内存带宽。对于大对象或只访问少数字段的业务,这部分成本可能是不必要的。 + +### 介绍已有验证 + +> 前期分别完成了单进程、共享内存双进程和完整 bRPC TCP 三层 benchmark,覆盖 Protobuf、FlatBuffers、Cap’n Proto,包含 simple/complex 结构和 64B 到 8MiB。测试说明免反序列化视图具备价值,但收益依赖消息大小和访问方式,不能简单概括为 FlatBuffers 在所有场景都更快。 + +### 介绍历史 PR + +> 社区以前的 PR #3196 解决消息构造和 IOBuf 集成,PR #3197 解决完整 RPC 协议接入。它们证明了方向可行,也帮助我们定位修改边界;但旧代码缺少完整校验、生命周期约束和测试,并且部分接口仍建立在 Protobuf 类型假设上,所以不能直接作为最终方案。 + +### 介绍方案 + +> 正式方案希望拆成数据区域、验证视图和传输适配三层。FlatBuffers 只负责格式和访问器,IOBuf 或 RDMA/URMA 负责承载数据,region 负责所有权和生命周期。这样 FlatBuffers 与传输方式不会相互绑定。 + +### 说明零拷贝边界 + +> 当前分析确认,单个连续 IOBuf block 可以直接引用;跨 block 时 FlatBuffers 由于连续布局要求可能需要合并复制。因此第一阶段应该承诺免反序列化,并将零拷贝定义为满足连续性条件时的快路径。 + +### 结束 + +> 今天希望先确认 V1 的技术边界和 API 方向。如果认可,我们会把当前可编译原型整理成类型安全、默认校验、可选编译并具有完整测试的 FlatBuffers over TCP 实现,然后再提取通用 region,最后进入 RDMA/URMA 验证。 + +--- + +## 15. 可能被问到的问题 + +### Q1:FlatBuffers 就是零拷贝吗? + +不是。FlatBuffers 可以免去传统反序列化对象重建,但网络接收、IOBuf 分片合并、发送系统调用和设备传输仍可能发生复制。 + +### Q2:为什么不直接做 RDMA/URMA? + +TCP 环境更容易调试,适合先验证 API、校验、所有权和生命周期。直接进入 RDMA/URMA 会把格式、协议、设备、内存注册和并发问题混在一起。 + +### Q3:为什么不只使用 attachment? + +attachment 是成本较低的实验路径,但无法自然提供类型化 service/method、自动校验和一致的调用接口。是否采用 attachment 或独立 protocol,正是本次评审需要决定的问题。 + +### Q4:为什么还要保留 Protobuf? + +Protobuf 生态成熟、兼容性强,对小消息和普通 RPC 很合适。本特性是可选的补充能力,不应破坏现有协议和用户代码。 + +### Q5:历史 PR 能直接合并吗? + +不能。它们落后当前主线,缺少测试和安全约束,并暴露出 descriptor 类型安全问题。它们更适合作为原型基础和设计证据。 + +### Q6:什么时候需要 RDMA/URMA 服务器? + +V1 FlatBuffers over TCP 在本地 WSL 即可完成。进入 V3 的注册内存、远端区域访问和真实传输性能测试时,必须使用具有 RDMA/URMA 设备的原生 Linux 环境。 + +--- + +## 16. 当前暂停点 + +- 工作分支:`prototype/flatbuffers-builder-port`; +- builder 移植已经形成独立提交; +- protocol 移植已经解决冲突并完成 bRPC 静态库编译; +- protocol 改动仍处于暂存状态,尚未形成正式提交; +- FlatBuffers 示例已经构建,但尚未完成运行和数据一致性验证; +- 暂停现场已保存为 Git 状态和 staged/unstaged diff; +- 当前没有 FlatBuffers 客户端或服务端进程运行。 + +在评审明确方案边界前,不建议把当前 protocol 原型作为正式功能代码提交。 + +--- + +## 17. 相关材料 + +- 内部设计初稿: +- Apache bRPC: +- 历史 FlatBuffers builder PR: +- 历史 FlatBuffers protocol PR: +- FlatBuffers: +- Cap’n Proto: + diff --git a/docs/cn/brpc_flatbuffers_visual_design.md b/docs/cn/brpc_flatbuffers_visual_design.md new file mode 100644 index 0000000000..a97d1af2c0 --- /dev/null +++ b/docs/cn/brpc_flatbuffers_visual_design.md @@ -0,0 +1,368 @@ +# bRPC FlatBuffers 免反序列化方案(图示版) + +> 用途:方案串讲与技术评审。 +> 当前范围:先验证 FlatBuffers over TCP;后续再提取通用 Region,并接入 RDMA/URMA。 + +## 1. 一句话目标 + +> 将网络收到的编码字节保存在生命周期受控的数据区域中,经过类型安全校验后,为业务提供可直接读取的类型化视图,减少完整反序列化、对象分配和不必要的内存复制。 + +第一阶段承诺的是“免反序列化 + 条件式零拷贝快路径”,不是无条件的全链路零拷贝。 + +## 2. 传统路径与目标路径 + +```mermaid +flowchart LR + subgraph OLD[传统 Protobuf 路径] + A1[C++ 业务对象] --> A2[序列化] + A2 --> A3[编码缓冲区] + A3 --> A4[bRPC / TCP] + A4 --> A5[接收缓冲区] + A5 --> A6[反序列化] + A6 --> A7[新消息对象] + A7 --> A8[业务访问] + end + + subgraph NEW[目标 FlatBuffers 路径] + B1[FlatBuffers Builder] --> B2[编码缓冲区] + B2 --> B3[bRPC / TCP] + B3 --> B4[SerializedRegion] + B4 --> B5[Verifier] + B5 --> B6[VerifiedView] + B6 --> B7[业务按需访问] + end +``` + +主要变化发生在接收端: + +```text +传统:字节 → 解析全部字段 → 构造新对象 → 业务访问 +目标:字节 → 安全校验 → 建立只读视图 → 业务按需访问 +``` + +## 3. 整体分层设计 + +```mermaid +flowchart TB + APP[业务层
读取字段,不关心底层传输] + VIEW[VerifiedView<T>
类型化只读访问 + 已验证状态] + REGION[SerializedRegion
地址 + 长度 + 所有权 + 生命周期 + 连续性] + ADAPTER[Transport Adapter
将不同数据来源转换成 Region] + + TCP[TCP / bRPC IOBuf] + SHM[共享内存] + RDMA[RDMA 注册内存] + URMA[URMA 内存区域] + + APP --> VIEW + VIEW --> REGION + REGION --> ADAPTER + ADAPTER --> TCP + ADAPTER --> SHM + ADAPTER --> RDMA + ADAPTER --> URMA +``` + +| 层次 | 主要职责 | +|---|---| +| 业务层 | 使用类型化接口读取字段 | +| `VerifiedView` | 格式解释、安全状态和访问入口 | +| `SerializedRegion` | 数据地址、长度、所有权、生命周期和内存属性 | +| Transport Adapter | 接入 IOBuf、共享内存、RDMA、URMA | + +设计原则是格式与传输解耦:FlatBuffers 不直接依赖 RDMA,RDMA/URMA Region 也不只服务于 FlatBuffers。 + +## 4. 普通 C++ 对象与 FlatBuffers 布局 + +### 4.1 普通 C++ 对象 + +```mermaid +flowchart LR + OBJ[User 对象
id
name 指针
scores 指针] + NAME[另一块内存
Alice] + SCORES[另一块内存
1.0 2.0 3.0] + + OBJ -->|name 指针| NAME + OBJ -->|scores 指针| SCORES +``` + +普通 `std::string`、`std::vector` 通常包含进程内指针。发送端的指针值在接收端没有意义,因此不能直接传输普通 C++ 对象内存。 + +### 4.2 FlatBuffers 编码布局 + +```mermaid +flowchart LR + ROOT[Root Offset] + TABLE[User Table
id + 相对偏移] + VTABLE[VTable
字段位置] + STR[String
长度 + 字符] + VECTOR[Vector
长度 + 连续元素] + + ROOT -->|相对偏移| TABLE + TABLE -->|字段布局| VTABLE + TABLE -->|相对偏移| STR + TABLE -->|相对偏移| VECTOR +``` + +FlatBuffers 使用相对偏移而不是进程指针。因此整块 buffer 移动到另一地址或另一机器后,内部关系仍然可以解释。 + +## 5. 类型化视图如何构造 + +```mermaid +flowchart TD + A[收到 IOBuf / Region] + B[获得连续地址、实际长度和所有权] + C[根据 RPC Method 确定预期 Root 类型] + D[构造 flatbuffers::Verifier] + E{类型专用 Verify 函数通过?} + F[拒绝请求
设置 RPC 错误
不调用业务] + G[GetRoot<T> 计算根对象地址] + H[将 root 与 Region 所有权绑定] + I[生成 VerifiedView<T>] + J[业务通过生成访问器读取字段] + + A --> B --> C --> D --> E + E -- 否 --> F + E -- 是 --> G --> H --> I --> J +``` + +概念代码: + +```cpp +auto region = SerializedRegion::FromIOBuf(input); + +flatbuffers::Verifier verifier( + region.data(), region.size()); + +if (!VerifyRequestBuffer(verifier)) { + return InvalidRequest(); +} + +const Request* root = + flatbuffers::GetRoot(region.data()); + +VerifiedView view( + std::move(region), root); +``` + +这里没有构造一套新的 `Request` 对象树。`root` 只是原始 FlatBuffers 数据的类型化入口。 + +## 6. 安全校验过程 + +```mermaid +flowchart TD + NET[不可信网络字节] + BOUNDARY{RPC 报文是否完整?} + SIZE{消息大小是否在限制内?} + CONTIG[得到生命周期受控的连续 Region] + VERIFY[Verifier 检查
Root / VTable / Offset / String / Vector / 嵌套] + VALID{校验是否成功?} + VIEW[创建 VerifiedView] + SERVICE[调用业务 Service Method] + ERROR[返回协议或数据错误
释放资源并记录指标] + + NET --> BOUNDARY + BOUNDARY -- 否 --> ERROR + BOUNDARY -- 是 --> SIZE + SIZE -- 否 --> ERROR + SIZE -- 是 --> CONTIG --> VERIFY --> VALID + VALID -- 否 --> ERROR + VALID -- 是 --> VIEW --> SERVICE +``` + +必须校验的原因:FlatBuffers 内部保存大量相对偏移和长度。恶意或损坏数据可能伪造 Root Offset、Vector 长度或 String 边界。直接 `GetRoot()` 并不能证明这些值合法。 + +安全策略: + +- 先检查 RPC 消息边界和最大长度; +- 再获得有效且不会提前释放的 Region; +- 使用当前 Method 对应的类型专用 Verifier; +- 校验失败时不得调用业务方法; +- 校验成功后才允许构造 View; +- 同时限制嵌套深度、对象数量和并发资源占用。 + +Checksum 不能代替 Verifier:Checksum 检查内容是否符合测试预期,Verifier 检查这些字节是否能被安全解释。 + +## 7. IOBuf 的连续与分片路径 + +```mermaid +flowchart TD + INPUT[收到一条逻辑完整的 IOBuf 消息] + CHECK{完整消息是否位于一个连续 Block?} + REF[引用原 Block
不合并数据] + COPY[申请连续内存
复制并合并多个 Block] + REGION[连续 SerializedRegion] + VERIFY[Verifier] + VIEW[VerifiedView] + + INPUT --> CHECK + CHECK -- 是:快路径 --> REF --> REGION + CHECK -- 否:降级路径 --> COPY --> REGION + REGION --> VERIFY --> VIEW +``` + +### 快路径 + +```text +单个连续 IOBuf Block → Region 引用原内存 → Verifier → View +``` + +可以做到接收端不合并复制,同时免去完整反序列化。 + +### 降级路径 + +```text +多个不连续 Block → 合并复制 → 连续 Region → Verifier → View +``` + +发生了一次复制,但仍不需要构造完整对象树。业务接口无需因底层布局不同而改变。 + +## 8. View 与 Region 的生命周期 + +```mermaid +flowchart LR + VIEW[VerifiedView<T>] + ROOT[const T* root] + REGION[SerializedRegion] + OWNER[IOBuf Block / Shared Memory / Registered Memory] + + VIEW --> ROOT + VIEW -->|持有所有权| REGION + REGION -->|保持存活| OWNER + ROOT -.指向.-> OWNER +``` + +必须保证: + +```text +View 的有效期 ≤ Region 的有效期 +``` + +如果只保存 `const T*`,底层 IOBuf 被释放后该指针会悬空。跨 RPC 回调持有数据时,业务必须显式取得 Region 所有权或复制需要长期保存的数据。 + +## 9. FlatBuffers 接入 bRPC 的两种路线 + +```mermaid +flowchart TB + START[FlatBuffers 接入 bRPC] + + START --> A[路线 A:独立 FlatBuffers RPC Protocol] + START --> B[路线 B:Protobuf 控制消息 + FlatBuffers Attachment] + + A --> A1[类型化 Stub / Service] + A --> A2[统一校验和生命周期] + A --> A3[核心代码改动较大] + + B --> B1[复用现有 RPC 和 Attachment] + B --> B2[核心改动较小] + B --> B3[类型和校验更多由业务管理] +``` + +| 路线 | 优点 | 主要代价 | +|---|---|---| +| 独立 Protocol | 类型化接口完整,可统一生成 Stub/Service | 修改 Channel、Controller、Server,维护成本高 | +| Attachment View | MVP 快、对核心侵入较小 | 类型和校验不够自动化,用户接口不够自然 | + +这是本次评审需要重点决定的问题。 + +## 10. 历史 PR 与当前工作的关系 + +```mermaid +flowchart TD + P3196[PR #3196
MessageBuilder + Message + IOBuf] + P3197[PR #3197
Protocol + Channel + Controller + Server] + PROTO[当前可编译原型
证明接入路径可行] + FORMAL[正式 V1
类型安全 + Verifier + 生命周期 + 测试] + REGION[通用 SerializedRegion] + REMOTE[RDMA / URMA] + + P3196 --> PROTO + P3197 --> PROTO + PROTO --> FORMAL --> REGION --> REMOTE +``` + +- PR #3196 回答消息怎样构造、持有和访问; +- PR #3197 回答消息怎样进入完整 RPC 请求/响应链路; +- 当前原型用于证明“能否接入”; +- 正式实现还要解决类型安全、校验、生命周期、兼容性和测试。 + +## 11. 分阶段实施路线 + +```mermaid +timeline + title FlatBuffers 免反序列化能力演进 + V1 FlatBuffers over TCP + : Builder / Message / View + : 类型安全 RPC 接口 + : 强制 Verifier + : 单 Block 快路径与多 Block 降级 + : 正确性、安全性和性能测试 + V2 通用 SerializedRegion + : 格式与传输解耦 + : 连续和分片区域 + : 所有权与生命周期 + : 复制次数和字节数观测 + V3 RDMA / URMA + : 注册内存与 Region Handle + : 访问权限和 Lease + : 按需读取 + : 超时、断连与撤销 + : 原生设备测试 +``` + +V1 可以在本地 WSL 完成;V3 才需要具有 RDMA/URMA 设备的原生 Linux 环境。 + +## 12. 当前状态 + +```mermaid +flowchart LR + DONE1[已完成
三种格式 benchmark] + DONE2[已完成
历史 PR 调研] + DONE3[已完成
Builder 原型移植] + DONE4[已完成
bRPC 静态库与示例构建] + TODO1[待完成
RPC 运行和字段一致性] + TODO2[待完成
Verifier 与生命周期] + TODO3[待完成
类型安全接口] + TODO4[待完成
正式测试与性能证据] + + DONE1 --> DONE2 --> DONE3 --> DONE4 --> TODO1 --> TODO2 --> TODO3 --> TODO4 +``` + +准确状态是: + +> FlatBuffers builder/protocol 的可编译原型已经打通,但尚未形成可向社区提交的正式功能实现。 + +## 13. 希望会议确认的事项 + +1. 是否认可“FlatBuffers over TCP → 通用 Region → RDMA/URMA”的路线? +2. V1 是否只承诺免反序列化和条件式零拷贝快路径? +3. FlatBuffers 应采用独立 Protocol 还是 Attachment View? +4. 是否接受 FlatBuffers 为可选依赖? +5. 接收端是否必须执行类型专用 Verifier? +6. V1 是否允许多分片输入合并复制? +7. View 能否跨 RPC 回调持有,采用何种所有权接口? +8. 历史 PR 中哪些机制保留,哪些接口重写? +9. 哪些正确性、安全性和性能指标作为合入门槛? + +## 14. 建议结论 + +```text +先保证正确和安全 + ↓ +再验证免反序列化收益 + ↓ +再优化连续缓冲区零拷贝快路径 + ↓ +最后推广至 RDMA/URMA +``` + +建议暂不把当前历史 PR 移植代码作为正式功能提交。先完成类型安全接口、强制校验、生命周期规则和 TCP 测试,再根据可复现证据提取通用 Region。 + +## 15. 相关链接 + +- 内部方案 PR: +- Apache bRPC: +- 历史 Builder PR: +- 历史 Protocol PR: +- FlatBuffers: + diff --git a/docs/cn/brpc_flatbuffers_zero_copy_review.md b/docs/cn/brpc_flatbuffers_zero_copy_review.md new file mode 100644 index 0000000000..4aa2a5f29b --- /dev/null +++ b/docs/cn/brpc_flatbuffers_zero_copy_review.md @@ -0,0 +1,287 @@ +# bRPC FlatBuffers 免反序列化方案预研评审 + +> 文档性质:技术预研与方向评审,不是最终实现说明。 +> 当前目标:先以 FlatBuffers over TCP 验证安全的免反序列化路径,再为 RDMA/URMA 提取通用数据区域抽象。 + +## 1. 背景与问题 + +bRPC 当前主要围绕 Protobuf 组织 RPC 消息。对于大型、嵌套或只需访问少数字段的数据,接收端完整反序列化可能带来额外的 CPU、内存分配和内存带宽成本。 + +```text +传统路径:C++ 对象 → 序列化 → 网络 → 反序列化 → 新对象 → 业务访问 +目标路径:编码缓冲区 → 网络 → 安全校验 → 类型化视图 → 按需访问 +``` + +希望验证的问题是: + +> bRPC 能否在保证输入安全和生命周期正确的前提下,让业务直接访问编码缓冲区,避免完整对象重建,并尽量减少额外复制? + +本方案不是替换 Protobuf,而是为大对象和按需访问场景增加一项可选能力。 + +## 2. 术语和承诺边界 + +- **免反序列化**:不把整条消息重新构造成另一套 C++/Protobuf 对象,而是在编码缓冲区上读取字段。 +- **零拷贝**:数据在链路中没有从一块内存复制到另一块内存。 + +二者并不等价: + +```text +免反序列化 ≠ 全链路零拷贝 +``` + +V1 建议只承诺: + +- 提供 FlatBuffers 免反序列化访问; +- 连续单块缓冲区满足条件时提供零拷贝快路径; +- 分片缓冲区无法直接访问时,安全降级为合并复制; +- 不承诺网络和操作系统链路完全没有复制。 + +## 3. 为什么先选择 FlatBuffers + +| 方案 | 接收端典型行为 | 主要特点 | +|---|---|---| +| Protobuf | 解析并构造消息对象 | 生态成熟、兼容性好 | +| FlatBuffers | 在连续编码缓冲区上建立视图 | 适合按需访问和免对象重建 | +| Cap’n Proto | 在其消息布局上建立 reader | 同样偏向原位访问,布局与生态不同 | + +FlatBuffers 适合验证: + +1. 编码缓冲区如何进入 bRPC; +2. bRPC 如何持有和释放底层数据; +3. 接收端如何校验并建立类型化视图; +4. 业务如何在安全生命周期内访问字段。 + +这不意味着 FlatBuffers 在所有消息大小和访问模式下都一定比 Protobuf 快。 + +## 4. 已完成的前期验证 + +测试覆盖: + +- Protobuf、FlatBuffers、Cap’n Proto; +- simple/complex 两类嵌套结构; +- 64B~8MiB payload; +- checksum 正确性校验; +- 多轮重复运行。 + +| 测试层次 | 目的 | 主要测量项 | +|---|---|---| +| 单进程 | 分离格式自身成本 | 序列化、解析/视图、部分访问、完整访问、编码大小 | +| 双进程共享内存 | 分离生产端与消费端 | producer 序列化、区域发布、consumer 解析/视图和访问 | +| 完整 bRPC TCP RPC | 验证真实 RPC 链路 | 客户端序列化、RPC 往返、服务端解析/视图和访问 | + +当前可以得出的结论: + +- 三种格式均完成上述范围的正确性测试; +- FlatBuffers 和 Cap’n Proto 可避免传统的完整对象重建; +- FlatBuffers 的潜在优势主要在接收端视图建立和按需访问; +- 小消息中,RPC 固定开销可能掩盖格式差异; +- 大消息中,复制、缓冲区连续性和完整扫描成本更重要; +- 正式评估不能只使用一个端到端耗时,还需要 P50/P95/P99、吞吐量、CPU、内存和复制字节数。 + +当前数据属于预研基线,不能解释为 FlatBuffers 在所有场景下已经证明更快。 + +## 5. RMMap 论文带来的启发 + +本方案借鉴的是“数据区域与访问视图分离”的思想,而不是直接照搬论文实现: + +1. 不在收到数据后无条件复制并重建完整对象; +2. 用 region 描述地址、长度、权限、所有权和生命周期; +3. 在 region 上建立经过验证的类型化视图; +4. 根据访问模式直接访问或按需获取数据; +5. 让本地缓冲区、RDMA 注册内存和 URMA 区域尽量共享上层语义。 + +```text +FlatBuffers:第一种类型化格式 +TCP IOBuf:第一种 region 数据来源 +RDMA/URMA:后续 region 的远端传输与访问实现 +``` + +格式与传输需要解耦:FlatBuffers 不应直接依赖 RDMA,RDMA/URMA region 也不应只服务于 FlatBuffers。 + +## 6. 为什么调研 PR #3196 和 #3197 + +Apache bRPC 社区此前已经尝试过接入 FlatBuffers。 + +### PR #3196:消息构造与 IOBuf 集成 + +主要包含 `MessageBuilder`、消息包装、`ReleaseMessage()`、`ParseFbFromIOBUF()`、`SerializeFbToIOBUF()` 及 FlatBuffers buffer 与 IOBuf 的衔接。 + +它回答的是: + +> FlatBuffers 消息如何在 bRPC 内存体系中构造、持有、发送和访问? + +### PR #3197:完整 RPC protocol + +主要涉及协议注册、Channel、Controller、Server、请求打包、响应解析、service/method descriptor、客户端 stub 和服务端分发。 + +它回答的是: + +> FlatBuffers 消息如何进入完整的 bRPC 请求/响应链路? + +```text +#3196:消息怎样构造、持有和访问 + ↓ +#3197:消息怎样通过完整 RPC 链路 +``` + +调研意义: + +- 避免重复实现已经探索过的机制; +- 找到非 Protobuf 格式接入 bRPC 的真实修改边界; +- 验证连续缓冲区上的免反序列化路径可行; +- 发现输入验证、生命周期、分片和类型安全风险。 + +它们不能直接作为最终实现,因为其落后当前主线、缺少完整测试和安全约束,部分接口还假定 method descriptor 为 Protobuf 类型。 + +## 7. 建议的总体设计 + +```text +┌───────────────────────────────────────────────────────┐ +│ 业务层 │ +│ VerifiedView / 字段访问 │ +└─────────────────────────┬─────────────────────────────┘ + │ 持有生命周期 +┌─────────────────────────▼─────────────────────────────┐ +│ SerializedRegion / OwnedRegion │ +│ 地址、长度、连续性、所有权、权限、释放策略、注册信息 │ +└─────────────────────────┬─────────────────────────────┘ + │ 由适配器提供 +┌─────────────────────────▼─────────────────────────────┐ +│ Transport Adapter │ +│ TCP IOBuf │ 共享内存 │ RDMA region │ URMA region │ +└───────────────────────────────────────────────────────┘ +``` + +### SerializedRegion + +描述地址、长度、连续/分片状态、读写属性、所有权、引用计数和释放策略;未来可扩展 RDMA/URMA 注册句柄和访问权限。 + +### VerifiedView + +持有或引用 region,先使用 `flatbuffers::Verifier` 校验不可信输入,成功后才允许 `GetRoot()`。 + +必须满足: + +```text +View 有效期 ≤ Region 有效期 +``` + +### Transport Adapter + +把 TCP IOBuf、共享内存、RDMA 或 URMA 数据转化为统一 region,使格式不绑定具体传输。 + +## 8. 当前原型状态 + +当前原型分支:`prototype/flatbuffers-builder-port`。 + +已经完成: + +- 移植 PR #3196 的 builder/message 基础,并形成独立原型提交; +- 将 PR #3197 的 protocol 改动应用到当前主线; +- 解决旧代码与当前主线的冲突; +- bRPC 静态库成功编译; +- FlatBuffers 关键符号进入 `libbrpc.a`; +- FlatBuffers 示例客户端和服务端成功构建。 + +尚未完成: + +- 示例 RPC 的完整运行和字段级一致性验证; +- 服务端和客户端强制 `Verifier`; +- 类型安全的非 Protobuf method descriptor 接口; +- view 生命周期和异步持有规则; +- 分片 IOBuf 的复制降级测试; +- 异常报文、并发、超时、取消和重试测试; +- 正式性能基准; +- RDMA/URMA 设备验证。 + +当前结论: + +> 已经得到可编译的 FlatBuffers builder/protocol 原型,用于证明接入路径可行;尚未形成可以提交给社区的正式功能实现。 + +## 9. 已确认的关键限制 + +### IOBuf 连续性 + +当前 `SingleIOBuf::assign()` 路径表明: + +- 消息完整位于一个连续 IOBuf block 时,可以直接引用; +- 消息跨多个 block 时,为满足 FlatBuffers 连续布局要求,可能需要合并复制。 + +### 输入安全 + +`GetRoot()` 不等于完整校验。正式实现必须在业务访问前执行 `Verifier`,并限制消息大小、嵌套深度、vector/string 边界。 + +### 类型安全 + +当前原型存在 descriptor 类型的临时兼容处理。正式设计需要独立的类型安全回调或带明确类型标签的通用描述符,不能保留不安全转换。 + +### 生命周期 + +FlatBuffers 对象是指向底层字节的视图。跨 RPC 回调持有必须显式取得共享所有权或复制数据。 + +## 10. 分阶段实施建议 + +### V1:FlatBuffers over TCP + +- FlatBuffers 作为可选依赖; +- builder/message/view API; +- 类型安全的 method descriptor 与调用路径; +- 接收端强制 `Verifier`; +- 明确 region/view 生命周期; +- 单 block 零拷贝快路径; +- 多 block 安全合并降级; +- 正确性、安全性和性能测试。 + +V1 可在本地 WSL 完成,不需要 RDMA 设备。 + +### V2:通用 SerializedRegion + +- 从 FlatBuffers 专用实现提取通用区域抽象; +- 支持连续和分片数据描述; +- 提供复制次数、复制字节数和降级原因观测; +- 让不同格式共享所有权和传输接口。 + +### V3:RDMA/URMA + +- 注册内存、region handle、远端地址和访问权限; +- region lease、撤销、超时和断连处理; +- 按需读取策略; +- 并发和重试安全; +- 在具有 RDMA/URMA 设备的原生 Linux 环境测试。 + +## 11. 正式测试门槛建议 + +- **正确性**:simple/complex、嵌套对象、空字段、vector/string、64B~8MiB、请求响应字段级校验; +- **安全性**:截断消息、非法 offset/长度、超大消息、深层嵌套、校验失败阻断、fuzz; +- **生命周期**:同步/异步、超时、取消、重试、并发、引用与释放次数; +- **性能**:序列化、parse/view、部分/完整访问、P50/P95/P99、吞吐量、CPU、内存、复制次数和字节数; +- **缓冲区布局**:单 block 与多 block 分别测试。 + +## 12. 希望本次会议确认的事项 + +1. 是否认可“FlatBuffers over TCP → 通用 Region → RDMA/URMA”的路线? +2. V1 是否只承诺免反序列化和条件式零拷贝快路径? +3. FlatBuffers 应作为独立 protocol,还是 attachment 上的类型化 view? +4. 是否接受 FlatBuffers 为可选依赖,默认不影响现有用户? +5. 接收端是否必须执行 `Verifier`? +6. V1 是否允许多分片输入合并复制? +7. view 能否跨 RPC 回调持有,应采用何种所有权接口? +8. 历史 PR 中哪些机制保留,哪些接口重写? +9. 哪些测试和性能指标作为合入门槛? +10. RDMA/URMA 是否只进入当前接口约束,推迟到后续版本实现? + +## 13. 建议结论 + +暂不把当前历史 PR 移植代码作为正式功能提交。建议先确认 V1 边界,完成类型安全接口、强制校验、生命周期规则及 TCP 测试,再根据证据提取通用 region,最后进入 RDMA/URMA 验证。 + +> 先把 FlatBuffers over TCP 做成安全、可测试、可维护的免反序列化能力;证据充分后,再把数据区域抽象推广到 RDMA/URMA。 + +## 14. 相关链接 + +- 内部方案 PR: +- Apache bRPC: +- 历史 builder PR: +- 历史 protocol PR: +- FlatBuffers: +- Cap’n Proto: + 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 通用免反序列化方案”提供稳定的消息对象和内存所有权基础。