从上面的输出结果可以看出,加强内联程度确实减少了一些"function too complex",看下 benchmark 结果:

文章插图
上面开启最高程度的内联强度,确实消除了不少因为“function too complex”带来无法内联的函数,但是压测结果显示收益不太明显 。
测试结果我们构建了基准测试来对比优化前后的性能,下面是测试结果 。
环境:Go 1.13.5 darwin/amd64 on a 2.5 GHz Intel Core i7 16GB小包
data size: 20KB

文章插图
大包
data size: 6MB

文章插图
无拷贝序列化在一些 request 和 response 数据较大的服务中,序列化和反序列化的代价较高,有两种优化思路:
- 如前文所述进行序列化和反序列化的优化
- 以无拷贝序列化的方式进行调用
Cap'n Proto 本质上是开辟一个 bytes slice 作为 buffer,所有对数据结构的读写操作都是直接读写 buffer,读写完成后,在头部添加一些 buffer 的信息就可以直接发送,对端收到后即可读取,因为没有 Go 语言结构体作为中间存储,所有无需序列化这个步骤,反序列化亦然 。
简单总结下 Cap'n Proto 的特点:
- 所有数据的读写都是在一段连续内存中
- 将序列化操作前置,在数据 Get/Set 的同时进行编解码
- 在数据交换格式中,通过 pointer(数据存储位置的 offset)机制,使得数据可以存储在连续内存的任意位置,进而使得结构体中的数据可以以任意顺序读写
- 对于结构体的固定大小字段,通过重新排列,使得这些字段存储在一块连续内存中
- 对于结构体的不定大小字段(如 list),则通过一个固定大小的 pointer 来表示,pointer 中存储了包括数据位置在内的一些信息
下面是相同数据结构下 Thrift 和 Cap'n Proto 的 Benchmark,考虑到 Cap'n Proto 是将编解码操作前置了,所以对比的是包括数据初始化在内的完整过程,即结构体数据初始化+(序列化)+写入 buffer +从 buffer 读出+(反序列化)+从结构体读出数据 。
struct MyTest {1: i64 Num,2: Ano Ano,3: list<i64> Nums, // 长度131072 大小1MB}struct Ano {1: i64 Num,}
文章插图
(反序列化)+读出数据,视包大小,Cap'n Proto 性能大约是 Thrift 的 8-9 倍 。写入数据+(序列化),视包大小,Cap'n Proto 性能大约是 Thrift 的 2-8 倍 。整体性能 Cap' Proto 性能大约是 Thrift 的 4-8 倍 。
前面说了 Cap'n Proto 的优势,下面总结一下 Cap'n Proto 存在的一些问题:
- Cap'n Proto 的连续内存存储这一特性带来的一个问题:当对不定大小数据进行 resize,且需要的空间大于原有空间时,只能在后面重新分配一块空间,导致原来数据的空间成为了一个无法去掉的 hole。这个问题随着调用链路的不断 resize 会越来越严重,要解决只能在整个链路上严格约束:尽量避免对不定大小字段的 resize,当不得不 resize 的时候,重新构建一个结构体并对数据进行深拷贝 。
- Cap'n Proto 因为没有 Go 语言结构体作为中间载体,使得所有的字段都只能通过接口进行读写,用户体验较差 。
Cap'n Proto 作为无拷贝序列化的标杆,那么我们就看看 Cap'n Proto 上的优化能否应用到 Thrift 上:
- 自然是无拷贝序列化的核心,不使用 Go 语言结构体作为中间载体,减少一次拷贝 。此优化点是协议无关的,能够适用于任何已有的协议,自然也能和 Thrift 协议兼容,但是从 Cap'n Proto 的使用上来看,用户体验还需要仔细打磨一下 。
推荐阅读
- 字节跳动官方出品的免费图标库,超好用还能自定义修改
- API怎么选?比较SOAP,REST,GraphQL和RPC
- 为啥需要RPC,而不是简单的HTTP?
- Rhino 字节跳动全链路压测的实践
- 从RPC到服务化框架
- YARN 在字节跳动的优化与实践
- 胎心115
- 聊聊从RPC到服务治理框架
- 主流RPC框架通讯协议实现原理与源码解析
- SpringBoot中使用dubbo实现RPC调用
