分享
  1. 首页
  2. 文章

记一次golang的gzip优化

blov · · 11434 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

背景

近期使用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


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

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

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏