Thrift简介
thrift协议你主要提供3种编解码格式协议,Binary、Compact、SimpleJson(还有debug、virtual);其中最常用的是Binary协议,其传输层一般以BufferedTransport、BufferedTransport居多;这里讨论主要以Binary协议为基础。本身来说,Binary协议不做任何压缩,只是为了支持跨系统网络传输(当然也可以做本地编解码工具)。
protobuf、flat对比
Compact协议对标protobuf编码,主要是利用varint压缩;Binary对标的是flat编码
和protobuf对比,他们thrift区别在于:
- protobuf、flat协议编解码方式只有一种,而thrift有3种
- protobuf以小端序传输,thrift以大端序传输,它们都只是跨系统网络传输;flat仅适用于机型系统(大小端相同)
- protobuf支持反射、thrift不支持
- protobuf3.0后支持更丰富的类型比如time、date、duration,thrift不支持
Binary编码介绍
Binary整体编码形式如下:
简单类型(byte、int8、int16、int32、int64、float64): 数据类型(1字节)+ 数据
字符串(string):数据类型(1字节)+ 数据长度(4字节)+数据
数组([]byte、[]int32、[]int64等):数据类型(1字节)+数组长度(4字节)+ 连续数据...
数组结构([]struct):数据类型(1字节)+数组长度(4字节)+ 连续(struct数据)...
字符串数组([]string):数据类型(1字节)+数组长度(4字节)+连续 (字符串长度+数据)...
map:数据类型(1字节)+key类型+value类型+map长度(4字节)+连续(key+value)...
对于非字符串、[]byte类型的超过2个连续字节的编码(包含长度),都是大端序
奇思妙想
如上面所说,Binary编码不做任何数据压缩,且是大端编码,这种方式是否足够好呢?对于编解码工具来说,除了能做跨网络传输编解码协议,好像看不到任何其他收益;
想想的确是这样
- 相比protobuf,不能做压缩
- 相比flat,性能上有损
既然,thrift的compact本身是对标protobuf,binary是对标flat,那我们可以可以把Binary编解码性能提升到等同flat呢???
答案是可以!!!
但是有一个问题待解决,flat是无法跨系统网络传输的,因为编码本身是依赖机器自身大小端。可以绕过去吗?
可以假如我们在编码的时候,把机器大小端信息带上去不就可以了!!!标识放到哪?放到Version头部即可!
具体实现
在编码的时候,新定义一种ProtocolID类型,叫ProtocolIDFlatLtlEnd(Flate小端序编码),因为ProtocolIDBinary本身是大端序(可以看做是flat变种),需要再新增ProtocolIDFlatBigEnd。
以go为例
const (
ProtocolIDBinary ProtocolID = 0
ProtocolIDJSON ProtocolID = 1
ProtocolIDCompact ProtocolID = 2
ProtocolIDDebug ProtocolID = 3
ProtocolIDVirtual ProtocolID = 4
ProtocolIDSimpleJSON ProtocolID = 5
ProtocolIDFlatLtlEnd ProtocolID = 6
)
Binary统一flat编码
- 编码:
- 先判断本机大小端:如果是大端,则头部写ProtocolID= ProtocolIDBinary,否则头部写ProtocolID= ProtocolIDFlatLtlEnd
- 把数据字段拷贝到内存即可,无需先做大端转换,再拷贝;特别的,对于数组类型[]int16,[]int32,[]int64,直接大段数据拷贝,无需遍历做端转换后一个个拷贝
- 解码:
- 获取协议头部ProtocolID,如果ProtocolID=ProtocolIDBinary且本机是大端;或者如果ProtocolID= ProtocolIDFlatLtlEnd且本机是小端,直接从内存copy数据到字段即可,无需做大端转换;特别的,对于数组类型[]int16,[]int32,[]int64,直接大段数据拷贝到数组,无需多次分段大端转换再赋值
如何判断大小端,参考如下代码:
var isLittleEndian bool = true
func
init() {
a := uint16(0x1234)
ptr := (*byte)(unsafe.Pointer(&a))
if *ptr == 0x12 {
isLittleEndian = false
}
}
兼容性
Binary是大端序,即ProtocolID= ProtocolIDBinary;为了保持兼容,必须设置默认ProtocolID= ProtocolIDBinary,并提供一个接口是否开启支持FlatLtlEnd。
编码过程变成如下:
- 如果本机是大端,直接flat编码,并且设置ProtocolID= ProtocolIDBinary;
- 如果本机是小端,并且用户设置了开启支持FlatLtlEnd,则直接flat编码,设置ProtocolID= ProtocolIDFlatLtlEnd
- 如果本机是小端,并且用户未设置开启支持FlatLtlEnd,仍然按照大端编码;和现有编解码保持一致
如果要开启FlatLtlEnd,则下游服务方必须开启支持FlatLtlEnd解码,这个直接通过header_protocol协议探测即可--服务端在业务数据header部加一个是否支持FlatLtlEnd即可。
那么上面的编码过程进一步简化成自动化探测协议,达到完美兼容现有thrift协议,整个过程如下图所示:
这里4种情况:
- client升级支持FlatLtlEnd,server没升级
- client升级支持FlatLtlEnd,server升级支持FlatLtlEnd
- client没升级,server没升级
- client没升级,server升级支持FlatLtlEnd
client升级支持FlatLtlEnd,server升级支持FlatLtlEnd
client支持FlatLtlEnd、server不支持
如果server某次升级支持FlatLtlEnd,那么场景回归到第一种情形
client不支持、server支持FlatLtlEnd
如果client某次升级支持FlatLtlEnd,那么场景回归到第一种情形
高性能
一般的,thrift rpc大部分场景是内部局域网通信,机器大小端一致,那么thrift编码完全退化成flat编码;会有以下性能提升
- 省去大端编码开销,直接拷贝
- 对于数组类型[]uint8,[]uint16,[]uint32,[]uint64,[]float32,[]float64,直接按照内存拷贝,无需遍历先大端后copy;且能充分利用avx、avx2、avx512等特殊指令,能够享受并行处理带来的性能提升(提升20倍+);
初步估算:改进后的编解码性能提升,在2~20+倍
性能测试
测试代码代码如下(简要模拟thrift编解码、优化后的flat编解码)
buptbill220/gooptlibgithub.com测试结论如下
- 优化后的thrift(Binary)编解码效率明显高于原生的编解码,平均在100%+(考虑到原生的bufio效率低,实际高300%以上,参考
)
- 特别对于数组较多的场景,优化后的thrift编解码,性能提升2000%;对于map多的可以改成平行数组优化
测试结果如下
# 模拟不同测试用例下,观测新thrift编码性能提升数据;
观察BenchmarkThrift(最原始),BenchmarkThriftNewOpt(最终优化)的数据对比
数据结构
type Data struct {
A int32
B int64
C []int64
D map[int]string
E []string
F []float64
}
case
1:所有数据类型比较平均,且数据量小;提升50%
===========================================================================
var data = &Data{
A: 123,
B: 123456789,
C: make([]int64, 5),
D: map[int]string{
12: "23424",
23: "34324",
44: "xxsdsfsfd",
64: "2sxdrwr",
},
E: []string{"2334", "23234234"},
F: []float64{1.0,23,23},
}
BenchmarkThrift-4
6316356
217 ns/op
BenchmarkThriftNew-4
6065745
213 ns/op
BenchmarkThriftOpt-4
7148114
229 ns/op
BenchmarkThriftNewOpt-4
8790902
144 ns/op
===========================================================================
case
2:所有数据类型比较平均,且数据量小;提升100%
===========================================================================
var data = &Data{
A: 123,
B: 123456789,
C: make([]int64, 20),
D: map[int]string{
12: "23424",
23: "34324",
344: "xcxfsf",
545: "xcfsfsdffd",
43: "2342344",
9: "jhdkajhf",
87: "sdfsf",
},
E: []string{"2334", "23234234", "sdfsdf", "sdfsfsf", "sdfsfsfsff"},
F: []float64{1.0,23,23, 3242, 34, 345345, 345, 435, 243},
}
BenchmarkThrift-4
2750547
505 ns/op
BenchmarkThriftNew-4
3740912
418 ns/op
BenchmarkThriftOpt-4
4432053
289 ns/op
BenchmarkThriftNewOpt-4
4456192
252 ns/op
===========================================================================
case
3:所有数据类型比较平均,且数据量中(数组偏多);提升1719%
===========================================================================
var data = &Data{
A: 123,
B: 123456789,
C: make([]int64, 1024),
D: map[int]string{
12: "23424",
23: "34324",
344: "xcxfsf",
545: "xcfsfsdffd",
43: "2342344",
9: "jhdkajhf",
87: "sdfsf",
},
E: []string{"2334", "23234234", "sdfsdf", "sdfsfsf", "sdfsfsfsff"},
F: []float64{1.0,23,23, 3242, 34, 345345, 345, 435, 243, },
}
BenchmarkThrift-4
195631
5930 ns/op
BenchmarkThriftNew-4
3081625
391 ns/op
BenchmarkThriftOpt-4
402711
2845 ns/op
BenchmarkThriftNewOpt-4
3696662
326 ns/op
===========================================================================
case
4:所有数据类型比较平均,且数据量多(数组偏多,考虑内存扩容);提升2192%
===========================================================================
var data = &Data{
A: 123,
B: 123456789,
C: make([]int64, 10240),
D: map[int]string{
12: "23424",
23: "34324",
344: "xcxfsf",
545: "xcfsfsdffd",
43: "2342344",
9: "jhdkajhf",
87: "sdfsf",
},
E: []string{"2334", "23234234", "sdfsdf", "sdfsfsf", "sdfsfsfsff"},
F: []float64{1.0,23,23, 3242, 34, 345345, 345, 435, 243, },
}
BenchmarkThrift-4
26301
49450 ns/op
BenchmarkThriftNew-4
533421
2164 ns/op
BenchmarkThriftOpt-4
44829
28206 ns/op
BenchmarkThriftNewOpt-4
477034
2157 ns/op
===========================================================================
case
5:所有数据类型比较平均,且数据量多(数组偏多,不考虑内存扩容);提升1867%(猜测cache miss影响)
===========================================================================
var data = &Data{
A: 123,
B: 123456789,
C: make([]int64, 10240),
D: map[int]string{
12: "23424",
23: "34324",
344: "xcxfsf",
545: "xcfsfsdffd",
43: "2342344",
9: "jhdkajhf",
87: "sdfsf",
},
E: []string{"2334", "23234234", "sdfsdf", "sdfsfsf", "sdfsfsfsff"},
F: []float64{1.0,23,23, 3242, 34, 345345, 345, 435, 243, },
}
BenchmarkThrift-4
21034
47975 ns/op
BenchmarkThriftNew-4
439399
2506 ns/op
BenchmarkThriftOpt-4
39430
27047 ns/op
BenchmarkThriftNewOpt-4
535525 ns/op
===========================================================================
case 6: 所有数据类型比较平均,且数据量多(非数组偏多,不考虑内存扩容);提升30%(map遍历性能比较差,指令cache miss,数据cache miss)
===========================================================================
var data = &Data{
A: 123,
B: 123456789,
C: make([]int64, 10240),
D: map[int]string{
12: "23424",
23: "34324",
344: "xcxfsf",
545: "xcfsfsdffd",
43: "2342344",
9: "jhdkajhf",
87: "sdfsf",
},
E: []string{"2334", "23234234", "sdfsdf", "sdfsfsf", "sdfsfsfsff"},
F: []float64{1.0,23,23, 3242, 34, 345345, 345, 435, 243, },
}
BenchmarkThrift-4
45409
27783 ns/op
BenchmarkThriftNew-4
40582
26488 ns/op
BenchmarkThriftOpt-4
52592
21835 ns/op
BenchmarkThriftNewOpt-4
52173
21219 ns/op
===========================================================================