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官方包,但亲测对压缩速度也提升了很大百分比。使用该库+对象池的方式可能会达到更显著优化效果。

  • 请尽量让自己的回复能够对别人有帮助
  • `单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传
  • 近期使用Golang官方的"compress/gzip"包对数据压缩返回给App,此场景特性:数据不固定、高并发。在实际过程中发现一个简单逻辑的API服务,几百QPS的情况下CPU却很高达到几个核负载。

    通过golang自带工具pprof抓图分析CPU,如下图(由于有业务代码,所以部分信息遮盖了):

    通过此图可以看出,整个工程里有两个CPU消耗大头:1)GC高 2)大部分CPU耗在Gzip上.看方法属于New操作,再加上GC高,很容易往一个方向上去想,就是对象创建过多造成。

    1.首先看下demo里原生的使用方式

    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官方包,但亲测对压缩速度也提升了很大百分比。使用该库+对象池的方式可能会达到更显著优化效果。

     最高记录 4314 ©2013-2019 studygolang.com Go语言中文网,中国 Golang 社区,致力于构建完善的 Golang 中文社区,Go语言爱好者的学习家园。 Powered by StudyGolang(Golang + MySQL)  • · CDN 采用 七牛云 VERSION: V4.0.0 · 9.101168ms · 为了更好的体验,本站推荐使用 Chrome 或 Firefox 浏览器