json-iterator 使用要注意的大坑
版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc
1. 前言
众所周知, Golang标准库"encoding/json"的性能并不好,现在比较热的替代库是"json-iterator/go"
传送门: json-iterator/go
我们的日志打成json格式,然后做集中收集,为了提高性能用json-iterator替换encoding/json。打火焰图,却发现(JSON序列化日志)耗时占总时间的比重没有下降
2. 重新测试
难道json-iterator出的性能压测数据都是吹牛逼,只要自己又重新测试了一下,以下是测试代码
json_test.go
package tjson
import (
"testing"
"encoding/json"
jsoniter "github.com/json-iterator/go"
)
type Car struct {
Name string
Name1 string
Name2 string
Name3 string
}
func BenchmarkStructJsoniter(b *testing.B) {
c := Car{Name1:"xxxxxxxxxxxxx", Name2:"xxxxxxxxxxxxxxxxxxxx", Name3:"xxxxxxxxxxxxxxxxxx", Name: "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
for i := 0; i < b.N; i++ { //use b.N for looping
jsoniter.Marshal(&c)
}
}
func BenchmarkStructStd(b *testing.B) {
c := Car{Name1:"xxxxxxxxxxxxx", Name2:"xxxxxxxxxxxxxxxxxxxx", Name3:"xxxxxxxxxxxxxxxxxx", Name: "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
for i := 0; i < b.N; i++ { //use b.N for looping
json.Marshal(&c)
}
}
func BenchmarkMapJsoniter(b *testing.B) {
mymap := make(map[string]string, 10000)
mymap["Name1"] = "xxxxxxxxxxxxx"
mymap["Name2"] = "xxxxxxxxxxxxxxxxxxxx"
mymap["Name3"] = "xxxxxxxxxxxxxxxxxx"
mymap["Name"] = "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
for i := 0; i < b.N; i++ { //use b.N for looping
jsoniter.Marshal(&mymap)
}
}
func BenchmarkMapStd(b *testing.B) {
mymap := make(map[string]string, 10000)
mymap["Name1"] = "xxxxxxxxxxxxx"
mymap["Name2"] = "xxxxxxxxxxxxxxxxxxxx"
mymap["Name3"] = "xxxxxxxxxxxxxxxxxx"
mymap["Name"] = "buickxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
for i := 0; i < b.N; i++ { //use b.N for looping
json.Marshal(&mymap)
}
}
BenchMark脚本
go test -bench=. -benchmem
3. 结果
goos: darwin
goarch: amd64
BenchmarkStructJsoniter-4 2000000 826 ns/op 296 B/op 2 allocs/op
BenchmarkStructStd-4 2000000 894 ns/op 496 B/op 2 allocs/op
BenchmarkMapJsoniter-4 100000 22197 ns/op 430 B/op 4 allocs/op
BenchmarkMapStd-4 100000 24100 ns/op 1109 B/op 16 allocs/op
以下是对一个对象序列化1000次,所用的耗时
json库 | map | struct |
---|---|---|
json-iterator | 22197 ns/op | 826 ns/op |
encoding/json | 24100 ns/op | 894 ns/op |
从输出结果我们可以看出
1. 在对结构体对象做JSON序列化时,在Go1.8上json-iterator相比标准库有50%的性能提升, 而在Go 1.10 上性能几乎没有什么提升
2. 对map对象做JSON序列化时,json-iterator和标准库相比几乎没有性能优势(这与前言中提到的替换了库,性能仍旧不能提升的情况一致)
3. 对几乎同样大小的map对象和struct对象,无论是标准库还是json-iterator, 性能都几乎差2个数量级。
4. 可见在编写Golang代码时要尽可能使用struct而不是map
为什么序列化map对象和struct对象会有如此大的性能差距呢
4. 简单的解释
带着疑问我简单的阅读的源码,似乎找到了答案
1)我们都知道JSON序列化是实际是一个递归过程。
type Car struct{
//age int
Name string
Msg string
}
比如要需要序列化一个Car对象,需要先反射得到Car中的每一个属性的名称和类型,然后在根据每一个属性的类型来决定是继续向下递归,还是调用相应的类型的encoder
2)在Golang中反射是一个非常耗时的操作。
3)json-iterator使用modern-go/reflect2来优化反射性能。然后就是通过大幅度减少反射操作,来提高速度。
详细代码 reflect_extension.go
// 保存 type -> Decoder
var typeDecoders = map[string]ValDecoder{}
// 保存 type/field -> Decoder
var fieldDecoders = map[string]ValDecoder{}
// 保存 type -> Encoder
var typeEncoders = map[string]ValEncoder{}
// 保存 type/field -> Encoder
var fieldEncoders = map[string]ValEncoder{}
对于struct而言,它的field只需要在在第一次操作时,反射一次name和type,并判断需要的Encoder或Decoder,而后续只需要获得field的name,无需获取field的type。
fieldNames := calcFieldNames(field.Name(), tagParts[0], tag)
// 注意这里
fieldCacheKey := fmt.Sprintf("%s/%s", typ.String(), field.Name())
decoder := fieldDecoders[fieldCacheKey]
if decoder == nil {
decoder = decoderOfType(ctx.append(field.Name()), field.Type())
}
encoder := fieldEncoders[fieldCacheKey]
if encoder == nil {
encoder = encoderOfType(ctx.append(field.Name()), field.Type())
}
相对而言,因为map对象的key和value的name和type是不固定的,因此上面的优化没有办法应用在map上。所以我们看到测试结果中,对map对象的序列化,json-iterator库和标准库并没有显著的差异。
总结
json-iterator的api与标准库接近,很容易平滑的替换
注意只有使用struct才能获得显著的性能提升
PS:
在使用json-iterator的过程中,没有发现有json-iterator有内存泄露问题,可以放心使用,但在Go1.10以后性能与标准库差异不大,这个第三方库的意义已经不大了。
我使用更加复杂的json格式,数据类型更多,嵌套更多,发现即使在1.10版本中,jsoniter的速度也还是比原生json快一倍左右
jsoniter对struct序列化相比原生的库,肯定还是有提升的,只是不如在1.8的时候提升的那么大了。
我用 go1.11 测试发现,benchmark时虽然耗时小,到时内存分配多, 然后再生产环境实际数据跑发现性能比标准库慢了一倍。pprof发现耗时主要在内存分配上。
你这个序列化map压测有问题,只有4个key,却分配了10000的size。如果只分配4个size再压测会发现jsoniter还是有性能优势的(本地go1.12测试 2x左右差距)
在go.12 上测试,确实如你所说还是有2倍左右的性能差距的
可能是我的case设计的不大恰当