背景
近期使用Golang官方的"compress/gzip"包对数据压缩返回给App,此场景特性:数据不固定、高并发。在实际过程中发现一个简单逻辑的API服务,几百QPS的情况下CPU却很高达到几个核负载。
问题追踪
通过golang自带工具pprof抓图分析CPU,如下图(由于有业务代码,所以部分信息遮盖了):
通过此图可以看出,整个工程里有两个CPU消耗大头:1)GC高 2)大部分CPU耗在Gzip上.看方法属于New操作,再加上GC高,很容易往一个方向上去想,就是对象创建过多造成。
于是google搜了一些资料发现有人尝试优化gzip,地址:https://github.com/klauspost/compress/tree/master/gzip,但经过测试虽然速度提升20~30%,但是并不兼容原生Gzip,似乎并不是一个很通用的方案。
分析源码
1.首先看下demo里原生的使用方式
demo地址:https://github.com/thinkboy/gzip-benchmark
func OldGzip(wr http.ResponseWriter, r *http.Request) {
buf := new(bytes.Buffer)
w := gzip.NewWriter(buf)
leng, err := w.Write(originBuff)
if err != nil || leng == 0 {
return
}
err = w.Flush()
if err != nil {
return
}
err = w.Close()
if err != nil {
return
}
b := buf.Bytes()
wr.Write(b)
// 查看是否兼容go官方gzip
/*gr, _ := gzip.NewReader(buf)
defer gr.Close()
rBuf, err := ioutil.ReadAll(gr)
if err != nil {
panic(err)
}
fmt.Println(string(rBuf))*/
}
2.其次看下官方gzip的实现,如下图:
跟踪代码寻找几处与Pprof图相关的有New操作的地方,首先第一张图每次都会New一个Writer,然后在第二张图里的Write的时候,每次又都会为新创建的Writer分配一个压缩器。对于对象的反复创建有一个通用的思路,使用对象池。
3.尝试使用对象池
通过上图我们发现gzip的Writer有个Reset()方法,该方法调用的init()里的实现是如果已经存在压缩器,就复用并且Reset()。也就是说其实官方已经提供了一种方式让用户不再反复New Writer。然后我们可以这样改造下实现代码:
func MyGzip(wr http.ResponseWriter, r *http.Request) {
buf := spBuffer.Get().(*bytes.Buffer)
w := spWriter.Get().(*gzip.Writer)
w.Reset(buf)
defer func() {
// 归还buff
buf.Reset()
spBuffer.Put(buf)
// 归还Writer
spWriter.Put(w)
}()
leng, err := w.Write(originBuff)
if err != nil || leng == 0 {
return
}
err = w.Flush()
if err != nil {
return
}
err = w.Close()
if err != nil {
return
}
b := buf.Bytes()
wr.Write(b)
// 查看是否兼容go官方gzip
/*gr, _ := gzip.NewReader(buf)
defer gr.Close()
rBuf, err := ioutil.ReadAll(gr)
if err != nil {
panic(err)
}
fmt.Println(string(rBuf))*/
}
我们给压缩过程中用到的Buffer以及Writer定义对象池spBuffer、spWriter,然后每次api请求都从对象池里去取,然后Reset,从而绕过New操作。
这里容易产生一个疑问:对象池其实本身就是一个“全局大锁”,高并发场景下这把全局大锁影响有多大?(其实有一种深度优化的方式就是拆锁,比如依据某个ID进行取余取不同的对象池。这里就拿一把大锁来实验).
下面看一下此次改造后的压测结果(QPS: 3000):
不使用对象池(CPU 使用28个核左右):
使用对象池(CPU 使用22个核左右):
通过CPU使用来看有消耗降低22%左右,由于QPS并不是很高,所以这里对象池的“全局大锁”的影响暂且可以忽略。
结论
针对官方Gzip的压缩可以使用对象池来改善。
klauspost所提供的方案也列举在demo中了,虽然属于自己改了压缩算法不兼容Golang官方包,但亲测对压缩速度也提升了很大百分比。使用该库+对象池的方式可能会达到更显著优化效果。
有疑问加站长微信联系(非本文作者)