- 坚持在连续内存上进行操作,并对用户使用提出严格要求:1. resize 操作必须重新构建数据结构 2. 当存在结构体嵌套时,对字段写入顺序有着严格要求(可以想象为把一个存在嵌套的结构体从外往里展开,写入时需要按展开顺序写入),且因为 Binary 等 TLV 编码的关系,在每个嵌套开始写入时,需要用户主动声明(如 StartWriteFieldX) 。
- 不完全在连续内存上操作,局部内存连续,可变字段则单独分配一块内存,既然内存不是完全连续的,自然也无法做到一次写操作便完成输出 。为了尽可能接近一次写完数据的性能,我们采取了一种链式 buffer 的方案,一方面当可变字段 resize 时只需替换链式 buffer 的一个节点,无需像 Cap'n Proto 一样重新构建结构体,另一方面在需要输出时无需像 Thrift 一样需要感知实际的结构,只要把整个链路上的 buffer 写入即可 。
然后让我们看下目前还有待解决的问题:
- 不使用 Go 语言结构体后带来的用户体验劣化
- 解决方案:改善 Get/Set 接口的使用体验,尽可能做到和 Go 语言结构体同等的易用
- Cap'n Proto 的 Binary Format 是针对无拷贝序列化场景专门设计的,虽然每次 Get 时都会进行一次解码,但是解码代价非常小 。而 Thrift 的协议(以 Binary 为例),没有类似于 pointer 的机制,当存在多个不定大小字段或者存在嵌套时,必须顺序解析而无法直接通过计算偏移拿到字段数据所在的位置,而每次 Get 都进行顺序解析的代价过于高昂 。
- 解决方案:我们在表示结构体的时候,除了记录结构体的 buffer 节点,还加了一个索引,里面记录了每个不定大小字段开始的 buffer 节点的指针 。
文章插图
测试结果概述:
- 小包场景,无序列化性能表现较差,约为 FastWrite/FastRead 的 85% 。
- 大包场景,无序列化性能表现较好,4K 以上的包较 FastWrite/FastRead 提升 7%-40% 。
参考资料
- https://github.com/alecthomas/go_serialization_benchmarks
- https://capnproto.org/
- https://software.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/compiler-reference/intrinsics/intrinsics-for-intel-advanced-vector-extensions-2/intrinsics-for-shuffle-operations-1/mm256-shuffle-epi8.html
公司内,基础架构团队主要负责字节跳动私有云建设,管理数以万计服务器规模的集群,负责数万台计算/存储混合部署和在线/离线混合部署,支持若干 EB 海量数据的稳定存储 。
文化上,团队积极拥抱开源和创新的软硬件架构 。我们长期招聘基础架构方向的同学,具体可参见 job.bytedance.com (文末“阅读原文”),感兴趣可以联系邮箱: tech@bytedance.com ,邮件标题: 姓名 - 工作年限 - 基础架构 。
推荐阅读
- 字节跳动官方出品的免费图标库,超好用还能自定义修改
- API怎么选?比较SOAP,REST,GraphQL和RPC
- 为啥需要RPC,而不是简单的HTTP?
- Rhino 字节跳动全链路压测的实践
- 从RPC到服务化框架
- YARN 在字节跳动的优化与实践
- 胎心115
- 聊聊从RPC到服务治理框架
- 主流RPC框架通讯协议实现原理与源码解析
- SpringBoot中使用dubbo实现RPC调用