sync.Pool
文章目录
pool 的使用方法
像数据库连接、TCP 的长连接,这些连接在创建的时候是一个非常耗时的操 作。如果每次都创建一个新的连接对象,耗时较长,很可能整个业务的大部分耗时都花在 了创建连接上。 所以,如果我们能把这些连接保存下来,避免每次使用的时候都重新创建,不仅可以大大 减少业务的耗时,还能提高应用程序的整体性能。
不过,这个类型也有一些使用起来不太方便的地方,就是它池化的对象可能会被垃圾回收 掉,这对于数据库长连接等场景是不合适的。所以在这一讲中,我会专门介绍其它的一些 Pool,包括 TCP 连接池、数据库连接池等等。 除此之外,我还会专门介绍一个池的应用场景: Worker Pool,或者叫做 goroutine pool,这也是常用的一种并发模式,可以使用有限的 goroutine 资源去处理大量的业务数 据。
sync.Pool 的使用方法 知道了 sync.Pool 这个数据类型的特点,接下来,我们来学习下它的使用方法。其实,这 个数据类型不难,它只提供了三个对外的方法:New、Get 和 Put。
因为 byte slice 是经常被创建销毁的一类对象,使用 buffer 池可以缓存已经创建的 byte slice,比如,著名的静态网站生成工具 Hugo 中,就包含这样的实现 bufpool,你可以 看一下下面这段代码:
|
|
sync.Pool 的坑 到这里,我们就掌握了 sync.Pool 的使用方法和实现原理,接下来,我要再和你聊聊容易 踩的两个坑,分别是内存泄漏和内存浪费。
内存浪费 除了内存泄漏以外,还有一种浪费的情况,就是池子中的 buffer 都比较大,但在实际使用 的时候,很多时候只需要一个小的 buffer,这也是一种浪费现象。接下来,我就讲解一下 这种情况的处理方法。 要做到物尽其用,尽可能不浪费的话,我们可以将 buffer 池分成几层。首先,小于 512 byte 的元素的 buffer 占一个池子;其次,小于 1K byte 大小的元素占一个池子;再次, 小于 4K byte 大小的元素占一个池子。这样分成几个池子以后,就可以根据需要,到所需 大小的池子中获取 buffer 了。
除了这种分层的为了节省空间的 buffer 设计外,还有其它的一些第三方的库也会提供 buffer 池的功能。接下来我带你熟悉几个常用的第三方的库。
第三方库
除了这种分层的为了节省空间的 buffer 设计外,还有其它的一些第三方的库也会提供 buffer 池的功能。接下来我带你熟悉几个常用的第三方的库。
- bytebufferpool 这是 fasthttp 作者 valyala 提供的一个 buffer 池,基本功能和 sync.Pool 相同。它的底层 也是使用 sync.Pool 实现的,包括会检测最大的 buffer,超过最大尺寸的 buffer,就会被 丢弃。 valyala 一向很擅长挖掘系统的性能,这个库也不例外。它提供了校准(calibrate,用来动 态调整创建元素的权重)的机制,可以“智能”地调整 Pool 的 defaultSize 和 maxSize。 一般来说,我们使用 buffer size 的场景比较固定,所用 buffer 的大小会集中在某个范围 里。有了校准的特性,bytebufferpool 就能够偏重于创建这个范围大小的 buffer,从而节 省空间。
- 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 的问题,从而降低这些指标。
文章作者 LYR
上次更新 2021-08-17