美文网首页
Go - Pool: 性能提升大杀器

Go - Pool: 性能提升大杀器

作者: kyo1992 | 来源:发表于2021-05-13 14:02 被阅读0次

    特性

    sync.Pool 数据类型用来保存一组可独立访问的"临时"对象,它说明了 sync.Pool 这个数据类型的特点,也就是说,它池化的对象会在未来的某个时候被毫无预兆地移除掉。而且,如果没有别的对象引用这个被移除的对象的话,这个被移除的对象就会被垃圾回收掉。

    有两个知识点需要记住:

    1. sync.Pool 本身就是线程安全的,多个 goroutine 可以并发地调用它的方法存取对象;
    2. sync.Pool 不可在使用之后再复制使用。

    使用方法

    只提供了三个对外的方法:New、Get 和 Put

    New

    Pool struct 包含一个 New 字段,这个字段的类型是函数 func() interface{}。当调用 Pool 的 Get 方法从池中获取元素,没有更多的空闲元素可返回时,就会调用这个 New 方法来创建新的元素。如果你没有设置 New 字段,没有更多的空闲元素可返回时,Get 方法将返回 nil,表明当前没有可用的元素。

    Get

    如果调用这个方法,就会从 Pool取走一个元素,这也就意味着,这个元素会从 Pool 中移除,返回给调用者。不过,除了返回值是正常实例化的元素,Get 方法的返回值还可能会是一个 nil(Pool.New 字段没有设置,又没有空闲元素可以返回),所以你在使用的时候,可能需要判断。

    func (p *Pool) Get() interface{} {
        // 把当前goroutine固定在当前的P上
        l, pid := p.pin()
        x := l.private // 优先从local的private字段取,快速
        l.private = nil
        if x == nil {
            // 从当前的local.shared弹出一个,注意是从head读取并移除
            x, _ = l.shared.popHead()
            if x == nil { // 如果没有,则去偷一个
                x = p.getSlow(pid) 
            }
        }
        runtime_procUnpin()
        // 如果没有获取到,尝试使用New函数生成一个新的
        if x == nil && p.New != nil {
            x = p.New()
        }
        return x
    }
    

    首先,从本地的 private 字段中获取可用元素,因为没有锁,获取元素的过程会非常快,如果没有获取到,就尝试从本地的 shared 获取一个,如果还没有,会使用 getSlow 方法去其它的 shared 中“偷”一个。最后,如果没有获取到,就尝试使用 New 函数创建一个新的。

    这里的重点是 getSlow 方法,我们来分析下。看名字也就知道了,它的耗时可能比较长。它首先要遍历所有的 local,尝试从它们的 shared 弹出一个元素。如果还没找到一个,那么,就开始对 victim 下手了。

    在 vintim 中查询可用元素的逻辑还是一样的,先从对应的 victim 的 private 查找,如果查不到,就再从其它 victim 的 shared 中查找。

    下面的代码是 getSlow 方法的主要逻辑:

    func (p *Pool) getSlow(pid int) interface{} {
    
        size := atomic.LoadUintptr(&p.localSize)
        locals := p.local                       
        // 从其它proc中尝试偷取一个元素
        for i := 0; i < int(size); i++ {
            l := indexLocal(locals, (pid+i+1)%int(size))
            if x, _ := l.shared.popTail(); x != nil {
                return x
            }
        }
    
        // 如果其它proc也没有可用元素,那么尝试从vintim中获取
        size = atomic.LoadUintptr(&p.victimSize)
        if uintptr(pid) >= size {
            return nil
        }
        locals = p.victim
        l := indexLocal(locals, pid)
        if x := l.private; x != nil { // 同样的逻辑,先从vintim中的local private获取
            l.private = nil
            return x
        }
        for i := 0; i < int(size); i++ { // 从vintim其它proc尝试偷取
            l := indexLocal(locals, (pid+i)%int(size))
            if x, _ := l.shared.popTail(); x != nil {
                return x
            }
        }
    
        // 如果victim中都没有,则把这个victim标记为空,以后的查找可以快速跳过了
        atomic.StoreUintptr(&p.victimSize, 0)
    
        return nil
    }
    
    Put

    这个方法用于将一个元素返还给 Pool,Pool 会把这个元素保存到池中,并且可以复用。但如果 Put 一个 nil 值,Pool 就会忽略这个值。

    func (p *Pool) Put(x interface{}) {
        if x == nil { // nil值直接丢弃
            return
        }
        l, _ := p.pin()
        if l.private == nil { // 如果本地private没有值,直接设置这个值即可
            l.private = x
            x = nil
        }
        if x != nil { // 否则加入到本地队列中
            l.shared.pushHead(x)
        }
        runtime_procUnpin()
    }
    

    sync.Pool 最常用的一个场景:buffer 池

    因为 byte slice 是经常被创建销毁的一类对象,使用 buffer 池可以缓存已经创建的 byte slice。

    var buffers = sync.Pool{
      New: func() interface{} { 
        return new(bytes.Buffer)
      },
    }
    
    func GetBuffer() *bytes.Buffer {
      return buffers.Get().(*bytes.Buffer)
    }
    
    func PutBuffer(buf *bytes.Buffer) {
      // 清空buf内容
      buf.Reset()
      buffers.Put(buf)
    }
    

    sync.Pool 的坑

    内存泄漏

    上面byte.Buffer会有内存泄漏风险

    取出来的 bytes.Buffer 在使用的时候,我们可以往这个元素中增加大量的 byte 数据,这会导致底层的 byte slice 的容量可能会变得很大。这个时候,即使 Reset 再放回到池子中,这些 byte slice 的容量不会改变,所占的空间依然很大。而且,因为 Pool 回收的机制,这些大的 Buffer 可能不被回收,而是会一直占用很大的空间,这属于内存泄漏的问题。

    在使用 sync.Pool 回收 buffer 的时候,一定要检查回收的对象的大小。如果 buffer 太大,就不要回收了,否则就太浪费了。

    内存浪费

    除了内存泄漏以外,还有一种浪费的情况,就是池子中的 buffer 都比较大,但在实际使用的时候,很多时候只需要一个小的 buffer,这也是一种浪费现象。

    要做到物尽其用,尽可能不浪费的话,我们可以将 buffer 池分成几层。首先,小于 512 byte 的元素的 buffer 占一个池子;其次,小于 1K byte 大小的元素占一个池子;再次,小于 4K byte 大小的元素占一个池子。这样分成几个池子以后,就可以根据需要,到所需大小的池子中获取 buffer 了。

    总结

    总结Pool 是一个通用的概念,也是解决对象重用和预先分配的一个常用的优化手段。即使你自己没在项目中直接使用过,但肯定在使用其它库的时候,就享受到应用 Pool 的好处了,比如数据库的访问、http API 的请求等等。

    我们一般不会在程序一开始的时候就开始考虑优化,而是等项目开发到一个阶段,或者快结束的时候,才全面地考虑程序中的优化点,而 Pool 就是常用的一个优化手段。如果你发现程序中有一种 GC 耗时特别高,有大量的相同类型的临时对象,不断地被创建销毁,这时,你就可以考虑看看,是不是可以通过池化的手段重用这些对象。

    另外,在分布式系统或者微服务框架中,可能会有大量的并发 Client 请求,如果 Client 的耗时占比很大,你也可以考虑池化 Client,以便重用。

    如果你发现系统中的 goroutine 数量非常多,程序的内存资源占用比较大,而且整体系统的耗时和 GC 也比较高,我建议你看看,是否能够通过 Worker Pool 解决大量 goroutine 的问题,从而降低这些指标。

    相关文章

      网友评论

          本文标题:Go - Pool: 性能提升大杀器

          本文链接:https://www.haomeiwen.com/subject/jjkqjltx.html