使用buffer对象池(sync.Pool)问题(转+总结)
sync.Pool可以在高并发场景下提高吞吐能力,但是如果使用不当会导致严重的问题。这里详细梳理一下这次遇到的问题。
前言
之前测试仿真环境总是发生丢body的问题,经过长时间的排查,终于发现了原因。我们网关的Client用的是fasthttp,测试仿真环境是使用nginx自建的LB。当业务发版的时候nginx会reload配置文件,本来这个过程是没有问题的。但是,由于fasthttp实现的原因无法感知到server端的连接断开,从而导致client丢失了body。具体的详情可见这个
原始代码
为了解决丢body的问题,我们只能改为使用buffer。具体的实现如下:
func (c *Context) BodyBuffer() (*bytes.Buffer, error) { if v, ok := c.Get("BB"); ok { return v.(*bytes.Buffer), nil } buff := initvar.BufPool.Get().(*[]byte) buffer := initvar.BufferPool.Get().(*bytes.Buffer) buffer.Reset() _, err := io.CopyBuffer(buffer, c.Request.Body, *buff) if err != nil { return nil, err } initvar.BufPool.Put(buff) initvar.BufferPool.Put(buffer) c.Set("BB", buffer) return buffer, nil }
结果,在线上有一个服务反馈他们有一个接口收到的RequestBody中混入了其他的数据。收到反馈后我们重新Review这段代码,迅速想到这里sync.Pool的使用存在问题。
这里我们在Put后又返回了buffer的指针,就造成多个Request公用一个buffer,自然就会导致某个Request中混入其他Request的Body。
修正
定位到问题后,我们迅速的进行了修改。 但是,万万没想到,又大意了!
func (c *Context) BodyBytes() ([]byte, error) { if v, ok := c.Get("BB"); ok { return v.([]byte), nil } buff := initvar.BufPool.Get().(*[]byte) buffer := initvar.BufferPool.Get().(*bytes.Buffer) defer initvar.BufPool.Put(buff) defer initvar.BufferPool.Put(buffer) buffer.Reset() _, err := io.CopyBuffer(buffer, c.Request.Body, *buff) if err != nil { return nil, err } c.Set("BB", buffer.Bytes()) return buffer.Bytes(), nil }
可以看到,我们为了解决原始代码的问题,想着在Put之前取出Bytes返回。但是,这里的Bytes函数读取的是Buffer中的属性里的一个[]byte,所以就导致仍然存在问题。
重现
为了验证我们的猜想,我们写了下面的代码进行验证,最终的输出确实可以看到Body里混入了其他数据。
解决
最终,我们将buffer放到Context中,问题得到了解决。
func (c *Context) ReadBodyBytes() ([]byte, error) { if c.bodyBuffer.Len() > 0 { return c.bodyBuffer.Bytes(), nil } written, err := io.Copy(&c.bodyBuffer, c.Request.Body) if err != nil { return nil, fmt.Errorf("read body error: %s, written: %d, buffer: %s", err, written, c.bodyBuffer.String()) } return c.bodyBuffer.Bytes(), nil }
总结
在使用sync.Pool的时候需要注意引用问题。 切记要保证Put回去对象已经使用完毕。
下一篇:
Java 产生内存溢出的几种情况