【免费下载链接】brpc
brpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".
brpc 是 C++ 实现的工业级 RPC 框架,其 RDMA(Remote Direct Memory Access)模块让应用可以在支持 InfiniBand / RoCE 的网卡上,以零拷贝、滑动窗口流控等机制进行高性能数据传输。本文以仓库文档 docs/en/rdma.md 为核心,结合 src/brpc/rdma 目录下的源码与 example/rdma_performance 示例,系统讲解 RDMA 的编译接入、基于 verbs API 的底层实现,以及全部可调参数的语义与默认值,帮助你完成从「启用 RDMA」到「调优内存与窗口」的完整落地。
一、编译与构建:如何启用 RDMA 支持
RDMA 依赖网卡驱动与硬件支持,brpc 目前只在 Linux 上验证过 RDMA 的构建。仓库同时提供了 config_brpc、CMake、Bazel 三种构建方式,开启宏开关后即可把 RDMA 模块编译进 brpc。
1.1 使用 config_brpc 构建
sh config_brpc.sh --with-rdma --headers="/usr/include" --libs="/usr/lib64 /usr/bin" make cd example/rdma_performance # example for rdma make--with-rdma会为编译过程定义BRPC_WITH_RDMA宏,--headers与--libs用于指定包含头文件(含infiniband/verbs.h)与链接库(如 libibverbs)的路径。
1.2 使用 CMake 构建
mkdir bld && cd bld && cmake -DWITH_RDMA=ON .. make cd example/rdma_performance # example for rdma mkdir bld && cd bld && cmake .. makeCMake 方案通过-DWITH_RDMA=ON开启 RDMA,示例目录自身也需要单独走一遍 CMake 生成与编译。
1.3 使用 Bazel 构建
# Server bazel build --define=BRPC_WITH_RDMA=true example:rdma_performance_server # Client bazel build --define=BRPC_WITH_RDMA=true example:rdma_performance_client1.4 验证:运行 RDMA 性能示例
示例目录 example/rdma_performance 中,server.cpp 默认监听 8002 端口,client.cpp 提供压测入口。两个程序都定义了DEFINE_bool(use_rdma, true, "Use RDMA or not"),并分别把该标志写入ServerOptions.use_rdma与ChannelOptions.use_rdma(见 server.cpp 与 client.cpp),是观察 RDMA 生效与否最直接的参考实现。
二、核心实现:RdmaEndpoint 与 verbs 数据通路
2.1 Socket 之上叠加 RdmaEndpoint
与 TCP 使用 socket API 不同,RDMA 通过 verbs API 直接驱动网卡。brpc 仍然复用brpc::Socket抽象:当用户把ChannelOptions.use_rdma或ServerOptions.use_rdma置为 true 时,对应 Socket 会创建RdmaEndpoint(实现见 src/brpc/rdma/rdma_endpoint.cpp,接口见 src/brpc/rdma/rdma_endpoint.h)。
启用 RDMA 后,发送方不再把数据写入 TCP fd,而是通过 verbs API 把待发送数据 post 到 RDMA 队列对(QP,Queue Pair)上;接收方则由RdmaEndpoint通过 verbs API 从完成队列(CQ,Completion Queue)取出完成事件。值得注意的事件驱动细节是:CQ 关联了一个专用 fd(completion channel),该 fd 会被注册进 brpc 的 EventDispatcher,事件到达后由 RdmaEndpoint::PollCq 统一处理,随后才交由InputMessenger解析 RPC 消息。
2.2 RC 模式与基于 TCP 的握手(handshake)
brpc 使用 RDMA 的 RC(Reliable Connection)可靠连接模式,每个RdmaEndpoint拥有独立的 QP。在建立 RDMA 连接之前,双方必须通过一次 TCP 连接交换 GID、QP 号(QPN)等信息,这个流程称为握手(handshake):
- 握手以 brpc 的 AppConnect 方式完成,即
RdmaConnect(见 rdma_endpoint.h); - 握手期间 TCP fd 仍然有效,握手报文是固定结构的
HelloMessage,包含 magic 字符串("RDMA")、消息长度、hello/impl 版本、接收块大小、SQ/RQ 大小、LID、GID、QP 号等字段(字段布局注释与序列化实现见 rdma_endpoint.cpp); - 握手结束后 TCP 连接保持在 EST 状态,但不再承载业务数据;一旦该 TCP 连接被关闭,对应的 RDMA 连接会被置为错误状态;
- 握手期间还定义了从
C_ALLOC_QPCQ到ESTABLISHED的完整状态机,以及FALLBACK_TCP、FAILED两种异常出口(状态枚举见 rdma_endpoint.h)。例如握手协商失败或网卡不可用时,连接会自动回退到普通 TCP,保证可用性。
2.3 三大传输特性之一:零拷贝(zero copy)
发送零拷贝:所有待发送数据都在 IOBuf 的 Blocks 中,无需 memcpy,直接把各 Block 的地址拼成ibv_sge数组 post 到发送队列。这些 Block 的引用保存在RdmaEndpoint::_sbuf中,必须等对端完成接收后才能释放。
接收零拷贝:接收侧提前把 IOBuf 的 Blocks 作为 receive buffer post 到接收队列,引用保存在RdmaEndpoint::_rbuf中(逻辑见 rdma_endpoint.cpp)。注意:接收侧 post 的每个 Block 有固定大小recv_block_size,因此发送方单条消息不能超过该值,否则接收侧无法完整收下。
小消息走拷贝路径:零拷贝并非无条件生效。HandleCompletion中,当收到的wc.byte_len小于rdma_zerocopy_min_size(默认 512 字节)时,会退化为普通拷贝(_socket->_read_buf.append(_rbuf_data[...], wc.byte_len)),避免为极短消息承担块管理开销,见 rdma_endpoint.cpp。
2.4 三大传输特性之二:滑动窗口流控
流控用于避免发送端过快、压垮慢速接收端(内核 TCP 栈也有类似机制)。RdmaEndpoint通过接收侧的显式 ACK 实现窗口机制:
- 握手阶段计算出本地窗口容量
_local_window_capacity = min(本地 SQ 大小, 对端 RQ 大小) - RESERVED_WR_NUM,其中RESERVED_WR_NUM = 3是为纯 ACK 预留的 WR 数量(见 rdma_endpoint.cpp); _window_size是原子计数,发送前检查窗口是否大于 0,post 一个发送 WR 就减一,收到对端 ACK 再恢复;- 为降低 ACK 开销,ACK 编号可以搭便车(piggyback)在普通数据消息的 immediate data 中一起发送,即每条发送 WR 都携带
IBV_WR_SEND_WITH_IMM与imm_data,接收侧在HandleCompletion中解析wc.imm_data来批量回收_sbuf并更新窗口(见 rdma_endpoint.cpp 与 rdma_endpoint.cpp)。
2.5 三大传输特性之三:事件抑制(event suppression)
每条消息的大小都被限制在recv_block_size(默认 8KB)内,如果每条消息都触发一次完成事件,性能会非常差(甚至不如拥有 GSO/GRO 的 TCP)。因此RdmaEndpoint会依据数据大小、窗口水位与 ACK 情况为每条消息设置 solicited 标志,控制对端是否产生事件。从发送侧逻辑(rdma_endpoint.cpp)可以看到具体决策:
- 写队列中最后一条消息、或当前窗口内最后一条消息,强制标记 solicited;
- 未 solicited 消息数超过窗口容量 1/4、或累计 ACK 超过对端窗口 1/4、或未 solicited 字节数超过 1MB 时,也会强制 solicited,保证接收端能被及时唤醒返回 ACK;
- 对应的,发送完成事件也做了抑制:每发送窗口容量 1/4 的 WR 才设置一次
IBV_SEND_SIGNALED(见 rdma_endpoint.cpp),且 SQ 被刻意放大为sq_size * 5 / 4以容纳最多 1/4 窗口的 unsignaled WR(见 rdma_endpoint.cpp)。
2.6 注册内存与 IOBuf 内存池接管
RDMA 要求所有参与传输的内存必须先行注册(memory registration),而注册开销很大,通常用内存池避免频繁注册。brpc 的做法是:把 IOBuf 的内存分配直接接管到 RDMA 内存池中(实现见 src/brpc/rdma/block_pool.cpp),从而在 IOBuf 层面实现彻底零拷贝且无需复制。由于 IOBuf 缓冲区不直接受用户控制,IOBuf 的总内存消耗需要谨慎管理,文档建议应用按自身需求一次性注册足够的内存。
如果应用想自行管理内存,可以通过IOBuf::append_user_data_with_meta发送自建数据:先调用rdma::RegisterMemoryForRdma(声明见 src/brpc/rdma/rdma_helper.h)注册内存,该函数返回注册内存的 lkey,调用append_user_data_with_meta时需要把 lkey 作为 data meta 一并提供。发送侧在cut_into_sglist_and_iobuf中正是通过读取 block 的 meta 来获取 lkey 的;若 lkey 为 0 且无法从块池解析,会直接报错ERDMAMEM并提示内存未注册(见 rdma_endpoint.cpp)。
2.7 网卡参数的自动探测与默认选择
RDMA 与硬件强相关,涉及 device、port、GID、LID、MaxSge 等概念。brpc 在初始化时从网卡读取这些参数并做出默认选择(逻辑见 src/brpc/rdma/rdma_helper.cpp)。例如 GID 选择逻辑(rdma_helper.cpp):对 InfiniBand 每个端口只有一个 GID,对 RoCE 则有 2 个基于 MAC 的 GID 和 2 个基于 IP 的 GID,一般最后一个 GID 是 IP 生成的 RoCEv2 类型 GID,因此默认取最大 GID 索引。当默认选择不符合预期时,可以按下一节介绍的 flag 方式覆盖。
三、可配置参数全解
RDMA 模块的参数全部通过 gflags 定义,可在启动时以-flag=value的方式覆盖,也可以在 brpc 运行期通过在线 flag 机制动态调整。下表完整列出 docs/en/rdma.md 中的可配置参数,并补充源码中的定义位置(见 rdma_endpoint.cpp、rdma_helper.cpp、block_pool.cpp):
| 参数 | 含义 | 默认值 |
|---|---|---|
rdma_trace_verbose | 是否在日志中打印 RDMA 连接信息 | false |
rdma_recv_zerocopy | 是否启用接收侧零拷贝 | true |
rdma_zerocopy_min_size | 接收侧零拷贝的最小消息大小(字节),小于该值的消息走拷贝路径 | 512 |
rdma_recv_block_type | 接收所用块类型:default(8KB)/large(64KB)/huge(2MB) | default |
rdma_prepared_qp_size | 应用启动时预创建 QP 的 SQ/RQ 大小 | 128 |
rdma_prepared_qp_cnt | 应用启动时预创建的 QP 数量 | 1024 |
rdma_max_sge | sglist 最大长度,0 表示使用设备允许的最大值 | 0 |
rdma_sq_size | SQ 大小 | 128 |
rdma_rq_size | RQ 大小 | 128 |
rdma_cqe_poll_once | 单次从 CQ 轮询的 CQE 数量 | 32 |
rdma_gid_index | 使用的本地 GID 表索引,-1 表示最大 GID 索引 | -1 |
rdma_port | 使用的端口号 | 1 |
rdma_device | IB 设备名,空表示第一个活跃设备 | 空 |
rdma_memory_pool_initial_size_mb | RDMA 内存池初始 region 大小(MB) | 1024 |
rdma_memory_pool_increase_size_mb | RDMA 内存池每次增长的 region 大小(MB) | 1024 |
rdma_memory_pool_max_regions | RDMA 内存池最大 region 数量 | 4(上限为 16) |
rdma_memory_pool_buckets | 内存池用于降低互斥竞争的 bucket 数 | 4 |
rdma_memory_pool_tls_cache_num | 内存池线程本地缓存块数量 | 128 |
3.1 接收块类型与 IOBuf 块头
rdma_recv_block_type与 IOBuf 的块实现耦合:GlobalInitialize会根据块类型从GetBlockSize取块大小再减去IOBuf_BLOCK_HEADER_LEN(32 字节)得到g_rdma_recv_block_size(见 rdma_endpoint.cpp)。三种块大小定义在 block_pool.cpp:8KB、64KB、2MB,实际可承载载荷分别是 8192-32、65536-32、2MB-32 字节。这解释了为什么「每条消息都被限制在 recv_block_size」——它是减掉 IOBuf 块头后的净载荷。
3.2 QP/SQ/RQ 的边界约束
rdma_sq_size与rdma_rq_size虽然默认是 128,但并非任意取值:RdmaEndpoint构造函数会把小于 16(MIN_QP_SIZE)的值抬升到 16,大于 4096(MAX_QP_SIZE)的值压回 4096(见 rdma_endpoint.cpp)。同时AllocateResources会优先从预创建的资源链表(由rdma_prepared_qp_size、rdma_prepared_qp_cnt控制)里领取 QP/CQ,不足时再现场创建(见 rdma_endpoint.cpp),连接关闭时满足条件的小 QP 会被重置后归还链表复用(见 rdma_endpoint.cpp)。
3.3 内存池的取值边界
内存池参数同样有硬边界:rdma_memory_pool_max_regions合法范围为 [1, 16](源码RDMA_MEMORY_POOL_MAX_REGIONS = 16,注意默认 flag 值实际为 4,文档中标注的 16 是允许的上限);初始/增长 region 大小范围为 [32, 1048576] MB(见 block_pool.cpp)。region 大小会按块大小与 bucket 数做正则化对齐,超出上限时会输出 "Memory pool reaches max regions" 日志并返回ENOMEM(见 block_pool.cpp)。
四、连接状态与调试手段
RdmaEndpoint内部维护了一套清晰的握手状态机(GetStateStr,见 rdma_endpoint.cpp),包括 UNINIT、C_ALLOC_QPCQ、C_HELLO_SEND、C_HELLO_WAIT、C_BRINGUP_QP、C_ACK_SEND、S_HELLO_WAIT、S_ALLOC_QPCQ、S_BRINGUP_QP、S_HELLO_SEND、S_ACK_WAIT、ESTABLISHED、FALLBACK_TCP、FAILED。把-rdma_trace_verbose=true打开后,可以在日志中看到 "Start handshake on ..."、"Handshake ends (use rdma) on ..." 等连接过程信息。
此外,RdmaEndpoint::DebugInfo(见 rdma_endpoint.cpp)会输出窗口大小、本地/远端窗口容量、sbuf/rbuf 游标、未 ACK 的接收 WR 数、未 solicited 发送数、未 signaled 发送 WR 数等内部状态,配合 brpc 的调试接口可以快速定位窗口耗尽、事件抑制过度等问题。
五、小结与使用建议
综合以上内容,在 brpc 中启用 RDMA 的完整链路是:以任一构建方式带上--with-rdma/-DWITH_RDMA=ON/--define=BRPC_WITH_RDMA=true编译 → 在ChannelOptions/ServerOptions中把use_rdma置为 true → 通过握手完成 TCP 换 RDMA 的升级,数据随之走 verbs 通道。过程中需要注意:
- 单条消息不能超过接收块净载荷(默认 8KB 减去 32 字节),需要更大消息请切换
rdma_recv_block_type=large或huge; - IOBuf 内存已被块池接管,务必按业务峰值评估
rdma_memory_pool_*参数,避免 OOM 或频繁扩 region; - 使用自管内存时,必须正确调用
rdma::RegisterMemoryForRdma并让 lkey 随append_user_data_with_meta传递,否则会触发ERDMAMEM错误; - 硬件环境(设备、端口、GID 索引)不匹配时,通过
rdma_device、rdma_port、rdma_gid_index等 flag 显式指定即可。
更深入的理解可以继续阅读 src/brpc/rdma/rdma_endpoint.cpp 中的发送(CutFromIOBufList)、接收(PollCq/HandleCompletion)与 QP 状态机(BringUpQp,RESET→INIT→RTR→RTS 三段式迁移,见 rdma_endpoint.cpp),以及 src/brpc/rdma/block_pool.cpp 的块池实现。
【免费下载链接】brpc
brpc is an Industrial-grade RPC framework using C++ Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. "brpc" means "better RPC".
相关推荐
brpc RDMA 支持实战指南:从编译配置到内核实现与参数调优
brpc RDMA 支持实战指南:从编译配置到内核实现与参数调优 本指南以 docs/en/rdma.md https://link.gitcode.com/i
RPC框架后端微服务网络通信brpc RDMA 深度指南:编译、零拷贝实现与参数调优
brpc RDMA 深度指南:编译、零拷贝实现与参数调优 RDMA(Remote Direct Memory Access)是高性能网络场景(搜索、存储、机器学
RPC框架后端微服务网络通信Apache BRPC中的RDMA支持深度解析
Apache BRPC中的RDMA支持深度解析 概述 在现代分布式系统中,远程直接内存访问 RDMA 技术因其低延迟、高吞吐的特性而备受关注。Apache BR
后端RPC框架通信网络
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考