记一次golang的gzip优化

blov · · 349 次点击 · · 开始浏览    

背景

近期使用Golang官方的"compress/gzip"包对数据压缩返回给App,此场景特性:数据不固定、高并发。在实际过程中发现一个简单逻辑的API服务,几百QPS的情况下CPU却很高达到几个核负载。

问题追踪

通过golang自带工具pprof抓图分析CPU,如下图(由于有业务代码,所以部分信息遮盖了): http://img-hxy021.didistatic.com/static/way/do1_In6jEmpzPXAca5KnRNUD

通过此图可以看出,整个工程里有两个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的实现,如下图: http://img-hxy021.didistatic.com/static/way/do1_TIZjHEQ3BarilNCNC3Z8 http://img-hxy021.didistatic.com/static/way/do1_QvSBEc6YCpXRafIxvK9C

跟踪代码寻找几处与Pprof图相关的有New操作的地方,首先第一张图每次都会New一个Writer,然后在第二张图里的Write的时候,每次又都会为新创建的Writer分配一个压缩器。对于对象的反复创建有一个通用的思路,使用对象池。

3.尝试使用对象池

http://img-hxy021.didistatic.com/static/way/do1_HQTmIbkd8zBFJVKPmPwr

通过上图我们发现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个核左右):

http://img-hxy021.didistatic.com/static/way/do1_gmYIJXtQ7KNJtts9itST

使用对象池(CPU 使用22个核左右):

http://img-hxy021.didistatic.com/static/way/do1_mJGJQGHiQNYvumIZiqTx

通过CPU使用来看有消耗降低22%左右,由于QPS并不是很高,所以这里对象池的“全局大锁”的影响暂且可以忽略。

结论

针对官方Gzip的压缩可以使用对象池来改善。

klauspost所提供的方案也列举在demo中了,虽然属于自己改了压缩算法不兼容Golang官方包,但亲测对压缩速度也提升了很大百分比。使用该库+对象池的方式可能会达到更显著优化效果。

demo地址:https://github.com/thinkboy/gzip-benchmark

349 次点击  
加入收藏 微博
下一篇:泛型展开
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传