在使用 Go 语言开发高性能应用程序时,垃圾回收(GC)对性能的影响是一个不可忽视的问题。Go 的自动垃圾回收机制虽然方便,但其 Stop-The-World(STW)特性会导致程序暂停,影响性能。此外,频繁在堆上创建大量对象会增加垃圾回收的标记时间,进一步降低程序效率。因此,在性能优化时,采用对象池是一种有效的策略。通过对象池,可以回收不再使用的对象,避免它们被垃圾回收,从而在需要时无需重新在堆上创建对象。
除了对象池,管理数据库连接和 TCP 长连接也是提升性能的关键。这些连接的创建过程非常耗时,如果每次使用都重新创建连接,会大大增加业务处理时间。通过保存并复用这些连接,不仅可以减少业务耗时,还能显著提高应用程序的整体性能。
对象池包含一组已初始化且可重复使用的对象。用户可以从池中获取对象进行操作,并在使用完毕后归还对象,而不是直接销毁。当对象初始化成本高、实例化频繁但数量较少时,使用对象池可以显著提高性能。从池中获取对象的时间是可预测且较短的,而新建实例的时间则不确定,可能较长。
尽管对象池有诸多优点,但也存在一些潜在问题。例如,池中的对象可能会被垃圾回收,这对于数据库长连接等场景是不合适的。因此,在使用对象池时,需要确保对象的生命周期得到妥善管理,避免不必要的垃圾回收。
sync Pool 本身是线程安全,多个 goroutine 可以并发调用它的方法存取对象
sync.Pool 不可在使用之后再复制使用,它是有状态的对象,
sync.Pool 提供三个方法 New Get Put
New 创建池化对象
定义创建池化对象的方法,这样可以在有需要的还还好创建对象,当调用 Pool 的 Get方法从池中获取对象时候,没有空闲对象可用时候,会调用 New 方法创建新的对象,如果没有设置 New 字段则 没空闲对象可用时候, Get方法将返回 nil
Get 获取对象
调用 Get 方法,就会从池子取走一个对象,返回给调用者 ,也有可能是返回一个 nil (如果 Pool.New没设置,Pool里面没有空对象的情况下)
Put 返还对象
Put 方法将一个对象返回给 Pool,Pool 会把这个对象保存到池子中,并且可以复用,如果返还的对象是 nil, Pool 会忽略,如果你想弃用一个对象,不再重用,很简单,不要调用 Put 方法返还即可
package mainimport ("fmt""net/http""sync""time"
)func main() {var p sync.Poolp.New = func() any {return &http.Client{Timeout: 5 * time.Second,}}var wg sync.WaitGroupwg.Add(100)t1 := time.Now()go func() {for i := 0; i < 100; i++ {go func() {defer wg.Done()c := p.Get().(*http.Client)defer p.Put(c)resp, err := c.Get("https://bing.com")if err != nil {fmt.Println("err:", err)return}resp.Body.Close()fmt.Print("got bing.com")}()}}()wg.Wait()fmt.Println("cost", time.Since(t1))}
使用 sync.Pool 可以池化任意对象,经常用它池化 byte slice,创建一个字节池,避免频繁创建销毁 byte slice
github.com/PuerkitoBio/bytebufferpool 功能与 sync.Pool 相同,底层也是 sync.Pool ,
改进之处有
1、动态调整缓冲区大小:bytebufferpool 通过将缓冲区大小分为20个区间,根据放回对象的容量落在哪个区间来记录次数。当放回次数达到一定阈值时,会重新校准,计算哪个区间容量的对象最多,并将 defaultSize 设置为该区间的上限容量。这样可以避免在使用过程中的切片扩容,从而提升性能。:
2、防止内存碎片化:
通过限制最大缓冲区大小,防止过大的缓冲区占用过多内存,导致内存碎片化和GC异常。maxSize 参数可以动态调整,确保内存使用在合理范围内。
github.com/oxtoacart/bpool
bpool 设计的精妙之处
-
减少内存分配和回收:
bpool
通过创建一个固定数量的预分配缓冲区,减少了内存分配和回收的次数。这种机制显著提高了内存操作的效率,尤其是在高并发环境下。
-
智能缓冲区管理:
SizedBufferPool
提供了一种智能策略,可以限制缓冲区的最大增长,防止过度使用的内存占用。这有助于避免内存泄漏和过度消耗系统资源。
-
多种缓冲池类型:
bpool
提供了三种类型的缓冲池:- BufferPool:提供固定大小的
bytes.Buffer
缓冲池。 - BytePool:提供固定长度的
[]byte
切片池。 - SizedBufferPool:与
BufferPool
类似,但会预先设定缓冲区的容量,并在返回时丢弃增长过大的缓冲区。
- BufferPool:提供固定大小的
sync.Pool 使用陷阱
1.对象重用不当 sync.Pool 的主要目的是重用对象,但如果对象状态未正确重置,可能会导致不可预期的行为。
2. .忽略对象的生命周期
sync.Pool 中的对象可能在任何时候被垃圾回收器回收,因此不应依赖其持久性。
3. 并发安全问题
问题:虽然 sync.Pool 本身是线程安全的,但用户在获取对象后修改其状态时,必须小心其他 goroutine 可能会同时获取相同的对象。
4. 不适合长期存活的对象
问题:sync.Pool 设计用于临时对象,不适合用于长期存在的对象。解决方法:对于需要长期存活的对象,如数据库连接或 TCP 连接,应使用专门的连接池管理,而不是 sync.Pool。确保在使用完对象后正确关闭或释放资源。
5. 监控和调整池子大小
问题:sync.Pool 的大小是动态的,但如果不监控其使用情况,可能会导致资源浪费或不足。
比如说 底层是一个切片数组,取出来切片 buffer,往这个里面添加了大量数据,导致底层数组切片很大,即使调用 bytes.Buffer 的Reset方法 然后将其放回池子,底层容量没有改变,所占空间仍然很大。由于 pool 的回收机制,这些大 buffer 可能不被回收,一直占用很大空间,从而造成内存泄漏,浪费。解决办法是,将元素放回时候,增加检查逻辑,如果返还元素超过一定大小的 buffer,直接丢弃,不再放回池子。
tcp连接池,常用库
github.com/faith/pool
package mainimport ("context""fmt""log""net""github.com/fatih/pool"
)type TCPConnection struct {conn net.Conn
}func (conn *TCPConnection) Reset() {conn.conn.Close()conn.conn = nil
}func newTCPConnection(address string) (*TCPConnection, error) {conn, err := net.Dial("tcp", address)if err != nil {return nil, err}return &TCPConnection{conn: conn}, nil
}func main() {address := "localhost:8080"p := pool.New(func() interface{} {conn, err := newTCPConnection(address)if err != nil {log.Println("Failed to create new connection:", err)return nil}return conn})defer p.Close()for i := 0; i < 10; i++ {obj := p.Get()if obj == nil {log.Println("Failed to get connection from pool")continue}conn := obj.(*TCPConnection)// 使用连接_, err := conn.conn.Write([]byte("Hello, server!"))if err != nil {log.Println("Failed to write to connection:", err)continue}buffer := make([]byte, 1024)n, err := conn.conn.Read(buffer)if err != nil {log.Println("Failed to read from connection:", err)continue}fmt.Println("Received:", string(buffer[:n]))// 释放连接p.Put(obj)}
}
任务池
github.com/gammazero/workerpool
github.com/ivpusic/grpool