Golang中国

在公司项目中做服务性能优化,发现http反向代理中写响应:

func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
    if p.FlushInterval != 0 {
        if wf, ok := dst.(writeFlusher); ok {
            mlw := &maxLatencyWriter{
                dst:     wf,
                latency: p.FlushInterval,
                done:    make(chan bool),
            }
            go mlw.flushLoop()
            defer mlw.stop()
            dst = mlw
        }
    }

    io.Copy(dst, src)
}

其中使用io.Copy进行数据拷贝,测试过程中发现其性能不高,采用:

payload, err := ioutil.ReadAll(src)
n, err := dst.Write(payload)

性能提升很客观。

采用boom压力测试,io.Copy测试的QPS为160000,ioutil.ReadAll+write的方式测试的QPS为190000.不清楚官方是否发现这个问题。


jimmykuu 于 2015-03-28 10:59 修改
16 回复
snake117
#1 snake117 • 2015-03-28 17:41

你注意看缓存占用啊……

还有一点就是如果你的代理的网络状况不佳,就会发现io.Copy比ioutil.ReadAll的好处了。

hewei861124
#2 hewei861124 • 2015-03-31 10:09

回复#1,内存不是瓶颈;你说的好处是安全方面的好处还是别的?不妨细说一下。

hewei861124
#3 hewei861124 • 2015-03-31 11:00

回复#1,我在go的官方上提了这个bug,那边给了回复,说还需要进一步的信息,我向问一下,如果我直接修改go语言包中的源码,可不可以直接编译使用?

heimeil
#4 heimeil • 2015-03-31 13:44

@hewei861124 修改官方包代码好像可以生效

hewei861124
#5 hewei861124 • 2015-03-31 13:59

回复#4,是么?我一会儿试试

snake117
#6 snake117 • 2015-03-31 20:08

@hewei861124 如果网络状况不佳,资源的下载和转发需要较长时间的话,你的先全部下载再转发就会出问题,我自己写反向代理的时候就发现了,用你那种写法,打开视频站,视频是不会加载的,等好几分钟才会突然加载完毕视频。

hewei861124
#7 hewei861124 • 2015-04-01 10:53

回复#6,哦,我明白你的意思,就是说如果http响应中的payload较大的话,会出现你说的那种问题,但是我这边的需求比较简单,一般数据包的大小不大。只是io.Copy的CPU消耗太大,并且效率很低。

snake117
#8 snake117 • 2015-04-02 13:52

@hewei861124 这是必然的,io.Copy毕竟为了实时传输而实现的。

hewei861124
#9 hewei861124 • 2015-04-02 14:13

回复#8,其实如果负载比较小的时候,采用我的方式确实可以带来性能上提高,对于高负载的场景,使用io.Copy更加安全稳定。这个问题我也在go的github上提出来了,那边给的回复大意就是采用我的方式不是无条件的,也就是不适合你说的这种场景。

defia
#10 defia • 2015-04-03 11:15

其实io.Copy和你的代码都不好。

io.Copy,每次都需要make一个特定长度的[]byte,会增大gc负担 应该使用sync.Pool,或者类似shadowsocks-go中的LeakyBuff的实现来复用[]byte并自己实现Copy

你的代码除了上几个回帖提到的没有采用流处理思想的问题外,ioutil.ReadAll,这个函数底层是用bytes.Buffer实现的,bytes.Buffer是一个自己按需grow的[]byte,开销也很大,也应该用上述方法,使用复用[]byte的Copy。

owiga
#11 owiga • 2015-04-07 15:47
import (
    "io"
    "sync"
)

var bufpool *sync.Pool

func init() {
    bufpool = &sync.Pool{}
    bufpool.New = func() interface{} {
        return make([]byte, 32*1024)
    }
}

func Copy(dst io.Writer, src io.Reader) (written int64, err error) {
    if wt, ok := src.(io.WriterTo); ok {
        return wt.WriteTo(dst)
    }
    if rt, ok := dst.(io.ReaderFrom); ok {
        return rt.ReadFrom(src)
    }

    buf := bufpool.Get().([]byte)
    defer bufpool.Put(buf)

    for {
        nr, er := src.Read(buf)
        if nr > 0 {
            nw, ew := dst.Write(buf[0:nr])
            if nw > 0 {
                written += int64(nw)
            }
            if ew != nil {
                err = ew
                break
            }
            if nr != nw {
                err = io.ErrShortWrite
                break
            }
        }
        if er == io.EOF {
            break
        }
        if er != nil {
            err = er
            break
        }
    }
    return written, err
}
owiga
#12 owiga • 2015-04-07 15:48

用buffer 池性能可以上去非常多 主要是gc搞的鬼,buffer创建销毁,gc耗了太多资源

yufeng
#13 yufeng • 2015-04-07 16:18
    import ( 
        "io" 
        "sync"
    )

    var bufpool *sync.Pool

    func init() { 
        bufpool = &sync.Pool{} 
        bufpool.New = func() interface{} { 
            return make([]byte, 32*1024) 
            } 
        }

    func Copy(dst io.Writer, src io.Reader) (written int64, err error) { 
        if wt, ok := src.(io.WriterTo); ok { 
            return wt.WriteTo(dst) 
        } 
        if rt, ok := dst.(io.ReaderFrom); ok { 
            return rt.ReadFrom(src) 
        }

        buf := bufpool.Get().([]byte)
        defer bufpool.Put(buf)
        for {
            nr, er := src.Read(buf)
            if nr > 0 {
                nw, ew := dst.Write(buf[0:nr])
                if nw > 0 {
                    written += int64(nw)
                }
                if ew != nil {
                    err = ew
                    break
                }
                if nr != nw {
                    err = io.ErrShortWrite
                    break
                }
            }
            if er == io.EOF {
                break
            }
            if er != nil {
                err = er
                break
            }
        }
        return written, err
    }
hewei861124
#14 hewei861124 • 2015-04-09 14:03

回复#11,#12,我在百度的虚拟机上测试的结果确实是io,Copy的性能不佳,不过最近我在微软的虚拟机上测试的结果是,这几种方式的性能几乎没什么差异。

woniu
#15 woniu • 2015-10-28 20:06

https://github.com/golang/go/commit/492a62e945555bbf94a6f9dd6d430f712738c5e0

发现更新了,楼主试试看.

net/http/httputil: add hook for managing io.Copy buffers per request

Adds ReverseProxy.BufferPool for users with sensitive allocation
requirements. Permits avoiding 32 KB of io.Copy garbage per request.

Fixes #10277

ake_chend
#16 ake_chend • 2015-10-28 23:36

正如过去使用C的时候为了避免不断的malloc/free而使用自己的内存池 一样,在现在golang中为了避免过量的gc操作,在特定场景下使用内存池还是应该提倡的。

需要 登录 后方可回复, 如果你还没有账号你可以 注册 一个帐号。