Golang JSON 解析优化

20 Jan 2017

在对于线上系统profile时发现, 很大一块儿CPU消耗在于对于JSON请求参数的解析, 于是着手优化.

ffjson

首先试了一下ffjson, 直接根据结构定义生成特定的解析代码, 自然比encoding/json基于反射的做法更加高效. 测试发现确实有提升, 但是提升空间有限.

看了下文档, 以及这个issue, 发现如果有很多的 interface{}对象, ffjson 还是回退到 encoding/json 解析, 反而效率不好.

prefer json.RawMessage to interface{}

那么有什么办法避免interface{}成员参数呢? 用 json.RawMessage.

在Golang这种静态类型语言中, 对于接口中格式不确定的字段, 一般做法是使用 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

说明:

可以看到, 最后的最优情况, 速度提高了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
}
HOME