字节跳动 Go RPC 框架 KiteX 性能优化实践( 五 )

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

字节跳动 Go RPC 框架 KiteX 性能优化实践

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

字节跳动 Go RPC 框架 KiteX 性能优化实践

文章插图
 
大包
data size: 6MB

字节跳动 Go RPC 框架 KiteX 性能优化实践

文章插图
 
无拷贝序列化在一些 request 和 response 数据较大的服务中,序列化和反序列化的代价较高,有两种优化思路:
  1. 如前文所述进行序列化和反序列化的优化
  2. 以无拷贝序列化的方式进行调用
调研通过无拷贝序列化进行 RPC 调用,最早出自 Kenton Varda 的 Cap'n Proto 项目,Cap'n Proto 提供了一套数据交换格式和对应的编解码库 。
Cap'n Proto 本质上是开辟一个 bytes slice 作为 buffer,所有对数据结构的读写操作都是直接读写 buffer,读写完成后,在头部添加一些 buffer 的信息就可以直接发送,对端收到后即可读取,因为没有 Go 语言结构体作为中间存储,所有无需序列化这个步骤,反序列化亦然 。
简单总结下 Cap'n Proto 的特点:
  1. 所有数据的读写都是在一段连续内存中
  2. 将序列化操作前置,在数据 Get/Set 的同时进行编解码
  3. 在数据交换格式中,通过 pointer(数据存储位置的 offset)机制,使得数据可以存储在连续内存的任意位置,进而使得结构体中的数据可以以任意顺序读写
    1. 对于结构体的固定大小字段,通过重新排列,使得这些字段存储在一块连续内存中
    2. 对于结构体的不定大小字段(如 list),则通过一个固定大小的 pointer 来表示,pointer 中存储了包括数据位置在内的一些信息
首先 Cap'n Proto 没有 Go 语言结构体作为中间载体,得以减少一次拷贝,然后 Cap'n Proto 是在一段连续内存上进行操作,编码数据的读写可以一次完成,因为这两个原因,使得 Cap' Proto 的性能表现优秀 。
下面是相同数据结构下 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,}
字节跳动 Go RPC 框架 KiteX 性能优化实践

文章插图
 
(反序列化)+读出数据,视包大小,Cap'n Proto 性能大约是 Thrift 的 8-9 倍 。写入数据+(序列化),视包大小,Cap'n Proto 性能大约是 Thrift 的 2-8 倍 。整体性能 Cap' Proto 性能大约是 Thrift 的 4-8 倍 。
前面说了 Cap'n Proto 的优势,下面总结一下 Cap'n Proto 存在的一些问题:
  1. Cap'n Proto 的连续内存存储这一特性带来的一个问题:当对不定大小数据进行 resize,且需要的空间大于原有空间时,只能在后面重新分配一块空间,导致原来数据的空间成为了一个无法去掉的 hole。这个问题随着调用链路的不断 resize 会越来越严重,要解决只能在整个链路上严格约束:尽量避免对不定大小字段的 resize,当不得不 resize 的时候,重新构建一个结构体并对数据进行深拷贝 。
  2. Cap'n Proto 因为没有 Go 语言结构体作为中间载体,使得所有的字段都只能通过接口进行读写,用户体验较差 。
Thrift 协议兼容的无拷贝序列化【字节跳动 Go RPC 框架 KiteX 性能优化实践】Cap'n Proto 为了更好更高效的支持无拷贝序列化,使用了一套自研的编解码格式,但在现在 Thrift 和 ProtoBuf 占主流的环境中难以铺开 。为了能在协议兼容的同时获得无拷贝序列化的性能,我们开始了 Thrift 协议兼容的无拷贝序列化的探索 。
Cap'n Proto 作为无拷贝序列化的标杆,那么我们就看看 Cap'n Proto 上的优化能否应用到 Thrift 上:
  1. 自然是无拷贝序列化的核心,不使用 Go 语言结构体作为中间载体,减少一次拷贝 。此优化点是协议无关的,能够适用于任何已有的协议,自然也能和 Thrift 协议兼容,但是从 Cap'n Proto 的使用上来看,用户体验还需要仔细打磨一下 。


    推荐阅读