ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

brpc 零拷贝缓冲区 IOBuf 完全指南:Cut / Append / Parse / Serialize 操作与性能剖析

brpc 零拷贝缓冲区 IOBuf 完全指南:Cut / Append / Parse / Serialize 操作与性能剖析 brpc 零拷贝缓冲区 IOBuf 完全指南Cut / Append / Parse / Serialize 操作与性能剖析【免费下载链接】brpcbrpc 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.项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc本文以 Apache brpc 的butil::IOBuf为核心系统讲解这一非连续、零拷贝缓冲区的设计定位、底层数据结构、六大核心操作Cut、Append、Parse、Serialize、Print、文件描述符读写及其性能表现。读者读完可以掌握 IOBuf 的完整 API 语义、与 protobuf 零拷贝流的协作方式以及在 RPC 附件attachment与 HTTP body 场景下的正确使用方法与性能边界。IOBuf 是什么为 RPC 数据流而生的非连续零拷贝缓冲区brpc 在部分协议的附件attachment以及 HTTP body 中使用了butil::IOBuf作为数据结构见 docs/en/iobuf.md 与 src/butil/iobuf.h。它是一块非连续non-contiguous的零拷贝缓冲区在前序项目中已被验证具备出色的性能。其接口风格与std::string相似但设计目标完全不同std::string保证内存连续IOBuf 则通过引用共享 分段描述来避免数据搬移。如果你曾经使用过 Kylin 中的BufHandle就能体会到 IOBuf 的便利前者封装得很差把内部结构直接暴露给用户用户必须小心翼翼地处理引用计数极易出错并引发 bug。而 IOBuf 把这些细节全部收纳在内部用户面对的是一个类似std::string的简洁接口。中文版本见 docs/cn/iobuf.md核心声明与实现位于 src/butil/iobuf.h 与 src/butil/iobuf.cpp配套单元测试见 test/iobuf_unittest.cpp测试用 proto 定义见 test/iobuf.proto。内部结构从源码看零拷贝是如何实现的从 src/butil/iobuf.h 的源码注释可以确认 IOBuf 的本质设计IOBuf 是一个 BlockRef 队列。BlockRef由offset、length与指向Block*的指针构成src/butil/iobuf.h它只是对底层数据块的一段视图描述本身不持有数据。小缓冲与大缓冲两种视图当 BlockRef 数量少时使用内联的SmallView2 个 BlockRefsrc/butil/iobuf.h数量多时升级为BigView动态数组环形索引src/butil/iobuf.h。默认构造函数不分配任何内存。8KB 引用计数块底层数据以块Block为单位默认块大小 8192 字节src/butil/iobuf.cpp中的static size_t default_block_size 8192。可通过butil::GetDefaultBlockSize()/butil::SetDefaultBlockSize()查询与修改SetDefaultBlockSize要求新值必须是 4096 的倍数见 src/butil/iobuf.cpp。引用计数共享多个 IOBuf 可以共享同一个 Block各自只记录自己的 offset/length 视图。复制一个 IOBuf 时只复制管理结构BlockRef 队列不复制 payload。线程安全模型务必牢记头文件注释明确给出了 IOBuf 的线程语义src/butil/iobuf.hthread-compatible不同线程同时使用不同的IOBuf 是安全的多个线程只读同一个 IOBuf 也是安全的。NOT thread-safe多个线程同时修改同一个IOBuf 是不安全的很可能导致崩溃。因此实践中 IOBuf 的生命周期应该尽量短避免长期持有导致引用计数的 8KB 块被锁住过多内存这也是原文档IOBuf cant一节的核心忠告。IOBuf 能做什么、不能做什么原文档给出了清晰的能力边界这里结合源码逐一展开IOBuf 可以能力说明源码依据默认构造不分配内存IOBuf()零成本创建src/butil/iobuf.h可复制写时共享复制不影响原对象只复制管理结构不复制数据IOBuf(const IOBuf)src/butil/iobuf.h追加另一个 IOBuf 不复制数据append(const IOBuf)共享 payloadsrc/butil/iobuf.h追加字符串复制数据append(const std::string)/append(const char*)src/butil/iobuf.h从文件描述符读写IOPortal子类提供append_from_file_descriptor/cut_into_file_descriptor等src/butil/iobuf.h与 protobuf 消息互转IOBufAsZeroCopyInputStream/IOBufAsZeroCopyOutputStreamsrc/butil/iobuf.h像std::ostream一样构造IOBufBuildersrc/butil/iobuf.hIOBuf 不能不能当作程序中通用的字符串类使用。因为 IOBuf 依赖按引用计数的 8KB 块Block如果 IOBuf 生命周期过长、长期驻留会锁住大量内存。IOBuf 的生命周期应当短暂用完后尽快释放或clear()。Cut从头部切出数据Cut 操作从 IOBuf前端切出数据是解析网络流的核心动作。原文档展示了两种形态源码中对应的实现分别是cutn与pop_frontsrc/butil/iobuf.cpp、src/butil/iobuf.cpp。从 source_buf 前端切 16 字节并追加到dest_buf若 source_buf 长度不足 16则全部切走source_buf.cut(dest_buf, 16); // 当 source_buf 长度 16 时切走全部字节只是从 source_buf 前端弹出丢弃16 字节source_buf.pop_front(16); // 当 source_buf 长度 16 时清空 source_buf从源码看cutn有一族重载覆盖三种目标类型src/butil/iobuf.hsize_t cutn(IOBuf* out, size_t n); // 切到另一个 IOBuf不复制数据 size_t cutn(void* out, size_t n); // 切到用户内存 size_t cutn(std::string* out, size_t n); // 切到 std::string此外还有按分隔符切割的cut_until(IOBuf* out, const char* delim)找不到分隔符时返回 -1src/butil/iobuf.h以及向文件描述符/写者直接切割输出的cut_into_file_descriptor/cut_into_writersrc/butil/iobuf.h。pop_front的返回值是被实际弹出的字节数n 0时什么也不做n length()时全部弹出见 src/butil/iobuf.h。语义提醒cut与pop_front在数据不足时的行为是全部拿走/全部清空而不是报错解析时请自行判断返回值或length()。Append向尾部追加Append 有两种截然不同的语义性能差异巨大。追加另一个 IOBuf不复制数据buf.append(another_buf); // 不复制数据共享底层块底层实现只是把another_buf的 BlockRef 追加到当前队列因此 O(1) 且零数据搬移src/butil/iobuf.h。如果希望转移语义追加后清空源可以使用Movable包装buf.append(another_buf.movable()); // 追加并把 another_buf 清空追加 std::string复制数据buf.append(str); // 将 str 的数据复制进 buf对应实现为append(const std::string)/append(const char*)/append(const void*, size_t)注意这类追加是带复制的src/butil/iobuf.h。头文件注释特别提醒push_back/append这类便捷接口由于频繁的 BlockRef 管理与引用计数开销相对较慢适合零星使用如果需要大量追加应改用IOBufAppender或IOBufBuilder——它们通过独占 Block降低开销src/butil/iobuf.h。扩展appendv(const const_iovec*, size_t)支持一次批量追加多段数据比逐个 append 更快append_user_data(void* data, size_t size, deleter)可以无复制地接管用户自有内存块由 IOBuf 引用计数管理其生命周期src/butil/iobuf.h。Parse从 IOBuf 解析数据解析 protobuf 消息IOBuf 通过IOBufAsZeroCopyInputStream包装为 protobuf 的零拷贝输入流然后直接交给消息解析IOBufAsZeroCopyInputStream wrapper(iobuf); pb_message.ParseFromZeroCopyStream(wrapper);对应源码见 src/butil/iobuf.h该类继承google::protobuf::io::ZeroCopyInputStream实现Next/BackUp/Skip/ByteCount。关键约束构造时它会保存源 IOBuf 的内部信息因此在 wrapper 生命周期内源 IOBuf 必须保持不变即使只创建不解析也一样否则结果未定义。解析自定义二进制格式配合CodedInputStream可以按自定义格式逐字段解析IOBufAsZeroCopyInputStream wrapper(iobuf); CodedInputStream coded_stream(wrapper); coded_stream.ReadLittleEndian32(value); ...由于底层块是 8KB 对齐的跨块字段由零拷贝流机制自动处理用户无需关心边界。Serialize把数据写入 IOBuf序列化 protobuf 消息IOBufAsZeroCopyOutputStream wrapper(iobuf); pb_message.SerializeToZeroCopyStream(wrapper);源码见 src/butil/iobuf.h。与输入流不同IOBufAsZeroCopyOutputStream不会先清空目标 IOBuf而是直接追加——这意味着你可以在同一个 IOBuf 上追加一段、序列化一条消息、再追加、再序列化交错使用头文件注释明确说明这一点。其构造函数有两种形态explicit IOBufAsZeroCopyOutputStream(IOBuf*); IOBufAsZeroCopyOutputStream(IOBuf*, uint32_t block_size);默认情况下同一线程内所有ZeroCopyOutputStream共享 TLS 中的 8KB 块池如果同一时刻存活的流很多可能产生大量碎片。此时可以传入正的block_size让该流拥有独占块避免碎片化。用 IOBufBuilder 像 std::ostream 一样构造IOBufBuilder os; os anything can be sent to std::ostream; os.buf(); // 得到 IOBufIOBufBuilder内部基于IOBufAsZeroCopyOutputStream实现src/butil/iobuf.h因此继承了独占 Block、减少引用计数开销的优势支持所有std::ostream的重载适合拼装可打印的协议头或日志缓冲。Print输出与转换为字符串IOBuf 可直接输出到std::ostream。注意以下示例中的 iobuf 应当只包含可打印字符否则输出会是乱码或二进制内容。std::cout iobuf std::endl; // 或 std::string str iobuf.to_string(); // 注意这会分配内存 printf(%s\n, str.c_str());operator(std::ostream, const IOBuf)声明于 src/butil/iobuf.hto_string()则会把全部数据拷贝进一个新的std::stringsrc/butil/iobuf.h适合小缓冲对大数据应避免反复to_string()。如果只想看一眼头部数据而不想复制可以用fetch(void* aux_buffer, size_t n)当数据在内部连续时它直接返回内部指针零拷贝否则拷贝到aux_buffersrc/butil/iobuf.h。IOBuf 还提供equals(const StringPiece)/operator用于与字符串比较src/butil/iobuf.h。进阶能力IOPortal、IOBufInputStream 与文件描述符除原文档内容外结合源码还有两个高频实战能力值得掌握IOPortal直接从 fd 读入。IOPortal继承自 IOBuf提供append_from_file_descriptor(int fd, size_t max_count)从 socket/文件描述符读取数据并追加到自身src/butil/iobuf.h是 brpc 网络层缓冲 socket 字节的典型用法。它内部缓存 Block消息切走变空后可调用return_cached_blocks()将块归还 TLS 复用每次append_xxx后都调用它没有意义且可能损害性能。IOBufInputStream喂给 std::istream 解析器。IOBufInputStream把 IOBuf 包装成std::istream可直接用于nlohmann::json::parse(in)这类基于流的 JSON 解析数据直接进入 IOBuf 块、无中间 string 拷贝src/butil/iobuf.h。同样要求源 IOBuf 在流生命周期内不被修改。性能文档实测数据IOBuf 的性能优势来自零拷贝 引用共享 块级管理。原文档给出的实测吞吐数据如下Read from file → Cut 12分片 → Copy → Merge into another buffer → Write to /dev/null操作吞吐QPSCut 1216 bytes → Copy → Merge → Write240.423MB/s8586535Cut 12128 bytes → Copy → Merge → Write790.022MB/s5643014Cut 121024 bytes → Copy → Merge → Write1519.99MB/s1467171可以看到分片越大吞吐越高、QPS 越低——大分片摊薄了每次操作的固定开销BlockRef 管理与引用计数而小分片则能获得更高的每秒操作数。这组数据印证了 IOBuf 的设计取舍把切、拼、合这类高频小操作做成 O(1) 的管理结构变更而非数据搬移。需要说明的是这是文档中给出的实测环境数据具体数值会随硬件与机器负载变化读者应以自己的基准测试为准。总结IOBuf 使用要点定位IOBuf 是 RPC 附件与 HTTP body 的传输缓冲区不是通用字符串容器保持生命周期短暂避免 8KB 引用计数块长期锁定内存。零拷贝IOBuf 之间的 append/cut 只动管理结构跨入 std::string 或用户内存则必然复制。protobuf 协作通过IOBufAsZeroCopyInputStream/IOBufAsZeroCopyOutputStream与ParseFromZeroCopyStream/SerializeToZeroCopyStream无缝衔接注意输入流要求源 IOBuf 全程不变。线程安全不同 IOBuf 可跨线程使用同一 IOBuf 的并发修改不安全。性能高频拼接用IOBufBuilder/IOBufAppender高频小分片 cut/pop 是 IOBuf 的强项大块数据输出用cut_into_file_descriptor直接落盘避免中间拷贝。更完整的 API 清单resize、reserve、append_to、copy_to、backing_block等可查阅 src/butil/iobuf.h并通过 test/iobuf_unittest.cpp 查看各操作的实际用法与边界行为。【免费下载链接】brpcbrpc 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.项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表