最近在做一个工具,从 日志系统 ES 集群导出数据,压缩然后上传到腾讯云存储,作为离线冷备份数据。在此过程中,遇到了数个性能问题。
其中 Elasticsearch SDK 的反序列化性能成为了一个瓶颈,因此在这里记录下处理方案。
olivere/elastic
是一个常用的功能非常强大的 Go Elasticsearch 客户端 SDK,它将大部分 Elasticsearch 的请求参数封装成结构体和方法,易用程度非常高。在处理请求返回值时 olivere/elastic
会使用 Go 内建的 json
库,将请求结果反序列化为对应的结构体,非常便于后续操作。
但是当导出大量数据的时候,比如用 Search/Scroll 每批次上万条文档,进行索引导出时,经 pprof
分析,反序列化会占用大量的 CPU 循环。
因此,为了特定需求,追求极限性能,有必要对此进行特殊处理。
好在 olivere/elastic
提供了 PerformRequest
方法,返回裸字节,我们可以复用现有的工具构建请求,然后自行对返回原始字节进行处理。
这里选用了 buger/jsonparser
库,这个库可以在原始 JSON 字节上进行特定嵌套字段的搜索,和数组遍历。
相较于反序列化为结构体,再对字段进行处理,这种方法可以极大地提升性能并节约内存,确切说,不会消耗任何额外内存。经验证,反序列化所占用的 CPU 循环已经由原先的 80% 减少到 20%。
buger/jsonparser
的使用方法很简单,可以参考官方文档,也可以参考我下面这个例子:
这个例子就是从 Elasticsearch 的 Search/Scroll 结果中,获取 hits.hits
数组的 _source
字段,并调用外部回调,将 _source
字段的原始 JSON 字节传递出去。
// find hits.hits
var hitsBuf []byte
var hitsType jsonparser.ValueType
if hitsBuf, hitsType, _, err = jsonparser.Get(buf, "hits", "hits"); err != nil {
return
}
if hitsType != jsonparser.Array {
err = errors.New("hits.hits is not array")
return
}
// iterate hits.hits
var itErr error
var itCount int64
_, _ = jsonparser.ArrayEach(hitsBuf, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
itCount++
if itErr != nil {
return
}
srcBuf, srcType, _, srcErr := jsonparser.Get(value, "_source")
if srcErr != nil {
itErr = srcErr
return
}
if srcType != jsonparser.Object {
itErr = errors.New("missing _source in hits.hits")
return
}
if itErr = e.handler(srcBuf, e.count, e.total); err != nil {
return
}
atomic.AddInt64(&e.count, 1)
})
可以参考我以此为基础封装的库 https://github.com/guoyk93/esexporter
当然,基于 []byte
做搜索不算是完全的流式解析,但是已经比反序列化然后再做处理要好很多了。
如果不考虑复用现有的 *elastic.Client
对象,你可以选择使用使用其他的基于 io.Reader
的 JSON 流式解析库,做到完全不占用内存的实时提取。
核心的观点就是,面对特定需求,流式解析会极大地提升性能和内存占用