在对于线上系统profile时发现, 很大一块儿CPU消耗在于对于JSON请求参数的解析, 于是着手优化.
ffjson
首先试了一下ffjson, 直接根据结构定义生成特定的解析代码, 自然比encoding/json
基于反射的做法更加高效.
测试发现确实有提升, 但是提升空间有限.
看了下文档, 以及这个issue, 发现如果有很多的
interface{}
对象, ffjson 还是回退到 encoding/json 解析, 反而效率不好.
prefer json.RawMessage
to interface{}
那么有什么办法避免interface{}
成员参数呢? 用 json.RawMessage
.
在Golang这种静态类型语言中, 对于接口中格式不确定的字段, 一般做法是使用 interface{}
字段. 缺点:
- 读取内容麻烦, 在业务逻辑中需要不停地做类型断言, 心累
- 已经解析好的
interface{}
字段, 再转换成已有结构定义比较麻烦, 需要借助外部的库实现.
可以使用 json.RawMessage
先占位. 优点:
- 在后续的业务逻辑中, 根据需要在解析到特定类型
- 在解析时, 直接拷贝原始字段, 避免了解析开销, 提高了请求参数解析效率
实际上, 在我们处理请求的时候, 很多时候只关心其中某些字段, 其他字段对于我们暂时没用, 如果直接不定义不关心的字段, 那么在大请求参数日志的时候就丢掉了这部分信息.
可以将暂时用不倒的字段定义为json.RawMessage
, 既保证了请求数据的完整性, 也避免了解析开销.
更进一步, 对于请求处理中不常读取的字段, 可以设置为json.RawMessage
, 在真正需要读取时再解析, 也有助于提高参数解析的效率.
此外, 另外一种做法, 也值得借鉴:
type Request struct {
Id string
Ext interface{}
}
req := &Request{
Ext: CustomType{}
}
json.Unmarshal(req, data)
但需要在解析前就知道确定的各种字段类型, 不方便根据请求参数字段不同解析为不同的格式.
sync.Pool
另外一种针对GC的常见的优化手段就是使用对象池.
使用对象池, 需要注意在放回的时候, 需要将对象重置到零值状态. 因为JSON解析时, 不会重置已有的字段. 例如:
q := struct {
Id string `json:"id"`
A int `json:"a"`
} {
"a", 1,
}
json.Unmarshal([]byte(`{"id":"a"}`), &q)
// q := {a 1}
对于复杂的数据结构定义, 重置所有字段是个麻烦的差事, 不过幸好ffjson提供了-reset-fields
选项, 避免了这方面的工作.
看了一下-reset-fields
的实现, 也比较简单. 对于slice字段直接置为nil, 而从GC优化的角度来说 (不考虑内存泄漏情况下), reslice为0也许更好.
另外, 对于有很多指针字段的结构使用对象池, 效果有限, 因为还是需要频繁地调用new(T)
. 是否可以再对这些对象使用缓存池呢? 实现起来有困难, 因为不确定字段被回收的时机.
字段设计
所以结构字段设计时, 避免使用指针字段.
不过对于可选字段, 在序列化时, 会导致JSON的可选字段的omitempty
标签失效, 参见这里.
如果不介意记录的请求参数里面多一个"f":{}
的话, 还是值得去做的.
对于可选字段的策略: 需要评估一下出现的比率, 以及字段本身的大小, 如果很高, 比如90%, 那么每次直接一次性分配空间效率更高.
跑个分呗?
BenchmarkDecodeBidRequest0 2000 708281 ns/op 113067 B/op 1210 allocs/op
BenchmarkDecodeBidRequest1 2000 661625 ns/op 108668 B/op 1151 allocs/op
BenchmarkDecodeBidRequest2 3000 539420 ns/op 71471 B/op 716 allocs/op
BenchmarkDecodeBidRequest3 3000 534891 ns/op 73390 B/op 691 allocs/op
BenchmarkDecodeBidRequest0FF 3000 523347 ns/op 86995 B/op 1142 allocs/op
BenchmarkDecodeBidRequest1FF 3000 494646 ns/op 82579 B/op 1083 allocs/op
BenchmarkDecodeBidRequest2FF 3000 389595 ns/op 45383 B/op 648 allocs/op
BenchmarkDecodeBidRequest2FFPool 5000 373610 ns/op 38263 B/op 612 allocs/op
BenchmarkDecodeBidRequest3FFPool 5000 373319 ns/op 38266 B/op 602 allocs/op
说明:
- 0
interface{}
for dynamic fields - 1:
json.RawMessage
for dynamic fields - 2: 1 +
json.RawMessage
for rarely used fields - 3: 2 + avoid pointer objects
- *FF: * + ffjson
- *Pool: * +
sync.Pool
可以看到, 最后的最优情况, 速度提高了2.5倍, 内存分配减少了50%
prefer Decoder / Encoder
to Unmarshal / Marshal
在面向流式接口时, 解析优先选用 json.NewDecoder(r io.Reader)
, 从而复用 json.decodeState
.
序列化也优先使用 json.NewEncoder(w io.Writer)
, 可以复用到 json.encodeState
的对象池,
而 json.Marshal
是每次创建一个新的encodeState
.
此外, 即便在单次请求的读写, 使用 Decoder / Encoder
也可以利用到上下游 io.Reader / io.Writer
的潜在缓存机制, 避免临时 []byte
的分配.
// OK
func ReadReq(req *http.Request) (q *Query, err error) {
q = new(Query)
err = json.NewDecoder(req.Body).Decode(q)
return
}
func ReadReqBad(req *http.Request) (q *Query, err error) {
// 额外创建了 []byte
var data []byte
// 既便数据无效, 也要含着泪读完
data, err = ioutil.ReadAll(req.Body)
if err != nil {
return
}
q = new(Query)
err = json.Unmarshal(data, q)
return
}
// OK
func WriteRes(w http.ResponseWriter, res *Result) error {
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
return enc.Encode(res)
}
func WriteResBad(w http.ResponseWriter, res *Result) error {
// 额外创建了 []byte
data, err := json.Marshal(res)
if err != nil {
return err
}
_, err = w.Write(data)
return err
}
注意一点, 在API返回等这种非HTML内容时, 可以通过enc.SetEscapeHTML(false)
关闭对于”&,<,>”的转义.
这在于我们返回的内容里面有大段HTML字符串时, 有优化意义:
BenchmarkEncodeJSONMarshal 1000000 1792 ns/op 328 B/op 3 allocs/op
BenchmarkEncodeJSON 1000000 1416 ns/op 8 B/op 1 allocs/op
BenchmarkEncodeJSONNoEscape 2000000 768 ns/op 8 B/op 1 allocs/op
另外注意到 encoding/json
本身针对序列化已有优化手段, 将对象的encode方法保存下来.
type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts)
var encoderCache struct {
sync.RWMutex
m map[reflect.Type]encoderFunc
}