Protobuf 总结
用途
Protobuf 是 google 出品的序列化框架,可跨平台、跨语言使用,扩展性良好。与 XML, JSON 等序列化框架相同,Protobuf 广泛的应用于数据存储,网络传输,RPC 调用等环境。
序列化: 将 数据结构或对象转换成二进制串的过程
反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
笔者认为序列化和反序列化可理解为上学的时候偷偷传的「纸条」,小 A 和 小 B 之间提前约定某种规则,小 A 按照规则写纸条的过程就是「序列化」,小 B 接到后按照规则翻译纸条上的内容的过程就是「反序列化」
序列化原理分析
优势
性能方面
- 体积小:Protobuf 中使用了多种编码(Varint、Zigzag),序列化后,数据大小可缩小 3 倍
- 传输速度快:带宽相同的情况下,体积小的传输速度更快
- 序列化速度快:直接把对象和字节数据做转换
使用方面
-
使用简单,维护成本低:仅需要维护一份 .proto 文件,protoc 编译器支持多种平台代码的生成
-
向后兼容好:支持字段的增加和删除,笔者认为 json 亦可支持
-
安全性:protobuf 编码后的数据比 json 编码的数据更难分析
-
适用性: 不适用于对基于文本的标记文档(如 HTML)建模,但在传输数据量大 & 网络环境不稳定的数据存储、RPC 数据交换的场景下很适用
原理
-
Protocol Buffer 将消息里的每个字段进行编码后,再利用 T - L - V 方式进行数据存储。
-
Tag - Length - Value, 标识 - 长度 -字段值存储方式
不需要分隔符就能分割字段,减少分割符使用
采用 Varint & Zigzag 编码方式,存储空间利用率高
没有设置字段值的字段,不需要编码(存储或传输过程中数据是完全不存在的),相应字段在解码的时候才会被设置默认值
-
-
Protocol Buffer 对于不同数据类型,采用不同的序列化方式(编码方式 & 数据存储方式)
-
Varint 编码方式
变长编码方式,用字节表示数字,值越小的数字,使用越少的字节数表示,通过减少表示数字的字节数从而进行数据压缩。
- 对于 int32 类型的数字,一般需要 4 个字节表示,若使用 Varint 编码,对于很小的 int32 类型数字,可以用 1 个字节表示,但很大的数字需要 5 个字节表示。
- 在计算机内,负数的符号位为数字的最高位,会被计算机理解为很大的整数,一定需要 5 个 byte,所以 protobuf 中又引入了 Zigzag 编码。
-
Zigzag 编码方式
变长编码方式,使用无符号数来表示有符号数字,使得绝对值小的数字都可以采用比较少子节来表示。特别是对表示负数的数据能更好地进行数据压缩
-
-
Ptotocol Buffer 对于数据编码方式和T - L -V 数据存储方式 ,使得序列化后体积更小
使用建议
-
字段标识号(Field_Number)尽量使用 1-15,且不要跳动使用
tag 里的 Field_Number 字段是需要占用字节空间的。如果 Field_Number 大于 16, Field_Number 的编码就会占用 2 个字节, 那么 Tag 在编码时也就会占用更多的字节。
-
若需要使用的字段值出现负数,优先使用 sint32/sint64
采用 sint32/ sint64 数据类型表示负数时,会优先使用 Zigzag 编码再采用 Varint 编码,更加有效的压缩数据。
测试结果分析
在分析过原理之后,深入思考下 protobuf 是不是在任何使用场景下都是合适的?可以考虑如下几种场景:
- 如果字段大部分都是字符串,占到决定性因素应该是字符串拷贝速度,而不是解析速度。
- 影响解析速度的决定性因素是分支的数量,上述建议字段标识号不要超过 15。因为分支的存在,解析仍然是一个串行的过程。
- 理论和实践并不一定完全保存一致。Protobuf 是一个理论上更快的格式,但是实现它的库并不一定就更快。而是取决于优化做得好不好,比如是否有不必要内存分配或者重复读取。
网上整理的测评结果
测评结果整理自 Protobuf 性能到底有没有比 JSON 快 5 倍,整理的是 Jackson 和 Protobuf 的性能对比,详情可点击查看:
测评方式 | 测评结果(Protobuf vs Jackson) |
---|---|
整数解码 | 8.51 倍 |
整数编码 | 2.9 倍 |
double 解码 | 13.75 倍 |
double 编码 | 12.71 倍 |
1 个字段的对象解码 | 2.5 倍 |
5 个字段的对象解码 | 1.3 倍 |
10 个字段的对象解码 | 1.22 倍 |
1 个字段的对象编码 | 1.22 倍 |
5 个字段的对象编码 | 1.68 倍 |
10 个字段的对象编码 | 1.72 倍 |
整数列表解码 | 2.92 倍 |
整数列表编码 | 1.35 倍 |
对象列表解码 | 1.26 倍 |
对象列表编码 | 2.22 倍 |
double 数组解码 | 5.18 倍 |
double 数组编码 | 15.63 倍 |
长字符串解码 | 1.85 倍 |
长字符串编码 | 0.96 倍,Jackson 比 Protobuf 略快 |
Jackson 是 Java 程序中用的最多的 JSON 解析器,benchmark 中开启了 AfterBurner 的加速特性(笔者不懂 Java 不知道这是用来做什么的)
自测数据
本着实践的精神笔者用 golang 写了一版本 protobuf VS json VS CustomWay(自定义格式)编码解码的测试,测试结果如下:
-
protobuf 格式
message Content { string identifier = 1; string resourceLocator = 2; string bucket = 3; int64 time = 4; }
-
json 格式
type JContent struct { Identifier string `json:"Identifier"` ResourceLocator string `json:"ResourceLocator,omitempty"` Bucket string `json:"Bucket"` Time int64 `json:"Time,omitempty"` }
-
自定义格式
field1 field2 field3 field4
注意: 按照空格分隔符分割 4 个字段,编码为拼接 4 个字段,解码为分割 4 个字段
测试结果:
goos: darwin
goarch: amd64
pkg: protobuf/benchmark
BenchmarkMarshalByProtoBuf-4 10000000 181 ns/op
BenchmarkUnmarshalByProtoBuf-4 10000000 158 ns/op
BenchmarkMarshalByCustomWay-4 20000000 85.0 ns/op
BenchmarkUnmarshalByCustumWay-4 10000000 143 ns/op
BenchmarkMarshalByJson-4 2000000 844 ns/op
BenchmarkUnmarshalByJson-4 1000000 2520 ns/op
PASS
ok protobuf/benchmark 12.141s
测试代码:
package test
import (
"encoding/json"
"strings"
"testing"
proto "github.com/golang/protobuf/proto"
)
var (
f = []string{
"shanghai",
"master",
"shanghai/chongming",
"1541388122",
}
content = Content{
Identifier: "shanghai",
ResourceLocator: "shanghai/chongming",
Bucket: "master",
Time: 1541388122,
}
jContent = JContent{
Identifier: "shanghai",
ResourceLocator: "shanghai/chongming",
Bucket: "master",
Time: 1541388122,
}
)
type customWay string
func (s *customWay) Marshal(fields []string) {
s1 := strings.Join(fields, " ")
*s = customWay(s1)
}
func (s *customWay) Unmarshal() []string {
return strings.Split(string(*s), " ")
}
type JContent struct {
Identifier string `json:"Identifier"`
ResourceLocator string `json:"ResourceLocator,omitempty"`
Bucket string `json:"Bucket"`
Time int64 `json:"Time,omitempty"`
}
func BenchmarkMarshalByProtoBuf(b *testing.B) {
for i := 0; i < b.N; i++ {
proto.Marshal(&content)
}
}
func BenchmarkUnmarshalByProtoBuf(b *testing.B) {
bytes, _ := proto.Marshal(&content)
result := Content{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
proto.Unmarshal(bytes, &result)
}
}
func BenchmarkMarshalByCustomWay(b *testing.B) {
var sc customWay
for i := 0; i < b.N; i++ {
sc.Marshal(f)
}
}
func BenchmarkUnmarshalByCustumWay(b *testing.B) {
var sc customWay
sc.Marshal(f)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sc.Unmarshal()
}
}
func BenchmarkMarshalByJson(b *testing.B) {
for i := 0; i < b.N; i++ {
json.Marshal(jContent)
}
}
func BenchmarkUnmarshalByJson(b *testing.B) {
bytes, _ := json.Marshal(jContent)
result := JContent{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Unmarshal(bytes, &result)
}
}
Proto3 区别于 Proto2 的使用
- 在第一行非空非注释行,必须写:
syntax = "proto3";
- 字段规则移除 「required」,并把 「optional」改为 「singular」
- 「repeated」字段默认使用 paced 编码
- 移除 default 选项
- 枚举类型的第一个字段必须要为 0
- 移除对扩展的支持,新增 Any 类型, Any 类型是用来替代 proto2 中的扩展的
- 增加了 JSON 映射特性