pool 的使用方法

像数据库连接、TCP 的长连接,这些连接在创建的时候是一个非常耗时的操 作。如果每次都创建一个新的连接对象,耗时较长,很可能整个业务的大部分耗时都花在 了创建连接上。 所以,如果我们能把这些连接保存下来避免每次使用的时候都重新创建,不仅可以大大 减少业务的耗时,还能提高应用程序的整体性能。

不过,这个类型也有一些使用起来不太方便的地方,就是它池化的对象可能会被垃圾回收 掉,这对于数据库长连接等场景是不合适的。所以在这一讲中,我会专门介绍其它的一些 Pool,包括 TCP 连接池、数据库连接池等等。 除此之外,我还会专门介绍一个池的应用场景: Worker Pool或者叫做 goroutine pool,这也是常用的一种并发模式,可以使用有限的 goroutine 资源去处理大量的业务数 据。

sync.Pool 的使用方法 知道了 sync.Pool 这个数据类型的特点,接下来,我们来学习下它的使用方法。其实,这 个数据类型不难,它只提供了三个对外的方法:New、Get 和 Put。

因为 byte slice 是经常被创建销毁的一类对象,使用 buffer 池可以缓存已经创建的 byte slice,比如,著名的静态网站生成工具 Hugo 中,就包含这样的实现  bufpool,你可以 看一下下面这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.Reset()
	buffers.Put(buf)
}

sync.Pool 的坑 到这里,我们就掌握了 sync.Pool 的使用方法和实现原理,接下来,我要再和你聊聊容易 踩的两个坑,分别是内存泄漏和内存浪费。

内存浪费 除了内存泄漏以外,还有一种浪费的情况,就是池子中的 buffer 都比较大,但在实际使用 的时候,很多时候只需要一个小的 buffer,这也是一种浪费现象。接下来,我就讲解一下 这种情况的处理方法。 要做到物尽其用,尽可能不浪费的话,我们可以将 buffer 池分成几层。首先,小于 512 byte 的元素的 buffer 占一个池子;其次,小于 1K byte 大小的元素占一个池子;再次, 小于 4K byte 大小的元素占一个池子。这样分成几个池子以后,就可以根据需要,到所需 大小的池子中获取 buffer 了。

除了这种分层的为了节省空间的 buffer 设计外,还有其它的一些第三方的库也会提供 buffer 池的功能。接下来我带你熟悉几个常用的第三方的库。

第三方库

除了这种分层的为了节省空间的 buffer 设计外,还有其它的一些第三方的库也会提供 buffer 池的功能。接下来我带你熟悉几个常用的第三方的库。

  1.  bytebufferpool 这是 fasthttp 作者 valyala 提供的一个 buffer 池,基本功能和 sync.Pool 相同。它的底层 也是使用 sync.Pool 实现的,包括会检测最大的 buffer,超过最大尺寸的 buffer,就会被 丢弃。 valyala 一向很擅长挖掘系统的性能,这个库也不例外。它提供了校准(calibrate,用来动 态调整创建元素的权重)的机制,可以“智能”地调整 Pool 的 defaultSize 和 maxSize。 一般来说,我们使用 buffer size 的场景比较固定,所用 buffer 的大小会集中在某个范围 里。有了校准的特性,bytebufferpool 就能够偏重于创建这个范围大小的 buffer,从而节 省空间。
  2.  oxtoacart/bpool 这也是比较常用的 buffer 池,它提供了以下几种类型的 buffer。 bpool 最大的特色就是能够保持池子中元素的数量,一旦 Put 的数量多于它的阈值,就会 自动丢弃,而 sync.Pool 是一个没有限制的池子,只要 Put 就会收进去。 bpool 是基于 Channel 实现的,不像 sync.Pool 为了提高性能而做了很多优化,所以,在 性能上比不过 sync.Pool。不过,它提供了限制 Pool 容量的功能,所以,如果你想控制 bpool.BufferPool: 提供一个固定元素数量的 buffer 池,元素类型是 bytes.Buffer, 如果超过这个数量,Put 的时候就丢弃,如果池中的元素都被取光了,会新建一个返 回。Put 回去的时候,不会检测 buffer 的大小。 bpool.BytesPool:提供一个固定元素数量的 byte slice 池,元素类型是 byte slice。 Put 回去的时候不检测 slice 的大小。 bpool.SizedBufferPool: 提供一个固定元素数量的 buffer 池,如果超过这个数量, Put 的时候就丢弃,如果池中的元素都被取光了,会新建一个返回。Put 回去的时候, 会检测 buffer 的大小,超过指定的大小的话,就会创建一个新的满足条件的 buffer 放 回去。

Pool 的容量的话,可以考虑这个库。

TCP 连接池

最常用的一个 TCP 连接池是 fatih 开发的 fatih/pool,虽然这个项目已经被 fatih 归档 (Archived),不再维护了,但是因为它相当稳定了,我们可以开箱即用。即使你有一些 特殊的需求,也可以 fork 它,然后自己再做修改。

Worker Pool 最后,我再讲一个 Pool 应用得非常广泛的场景。 你已经知道,goroutine 是一个很轻量级的“纤程”,在一个服务器上可以创建十几万甚 至几十万的 goroutine。但是“可以”和“合适”之间还是有区别的,你会在应用中让几 十万的 goroutine 一直跑吗?基本上是不会的。 一个 goroutine 初始的栈大小是 2048 个字节,并且在需要的时候可以扩展到 1GB(具体 的内容你可以课下看看代码中的配置:  不同的架构最大数会不同),所以,大量的 goroutine 还是很耗资源的。同时,大量的 goroutine 对于调度和垃圾回收的耗时还是会 有影响的,因此,goroutine 并不是越多越好。 有的时候,我们就会创建一个 Worker Pool 来减少 goroutine 的使用。比如,我们实现 一个 TCP 服务器,如果每一个连接都要由一个独立的 goroutine 去处理的话,在大量连接 的情况下,就会创建大量的 goroutine,这个时候,我们就可以创建一个固定数量的 goroutine(Worker),由这一组 Worker 去处理连接,比如 fasthttp 中的  Worker Pool。

综合下来,精挑细选,我给你推荐三款易用的 Worker Pool,这三个 Worker Pool 的 API 设计简单,也比较相似,易于和项目集成,而且提供的功能也是我们常用的功能。 类似的 Worker Pool 的实现非常多,比如还有  panjf2000/ants、  Jeffail/tunny 、  benmanns/goworker、  go-playground/pool、  Sherifabdlnaby/gpool等第三方 库。  pond也是一个非常不错的 Worker Pool,关注度目前不是很高,但是功能非常齐 全。 其实,你也可以自己去开发自己的 Worker Pool,但是,对于我这种“懒惰”的人来说, 只要满足我的实际需求,我还是倾向于从这个几个常用的库中选择一个来使用。所以,我 建议你也从常用的库中进行选择

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