Golang数据库连接池原理及调优实践

背景

在产品交付客户的过程中,客户现场的基础组件(如mysql、redis等)与厂内测试环境有可能会有较大差异。日前遇到某客户的mysql服务端配置如下:

  • 最大连接数为10;
  • 主动关闭连接时间为10分钟。

现象来看,连接经常被服务端主动关闭,偶现database连接异常。究其原因是业务模块配置的MaxConn和MaxLifeTime较大,超过了服务端的配置大小。

本文基于这个case,分析Golang中mysql连接池技术的原理,以及基于maxIdle、maxOpen和maxLifeTime进行调优的实践。

池化技术带来的优势

我们编程中常见的池化(Pool)技术有对象池、线程池、数据库连接池等,它们的特点都是将开销较大的资源,维护在一个特定的“池”中,在请求量或访问量大时,能降低系统、连接频繁建立销毁的开销,从而明显优化程序的性能。池化技术定义了最小对象(连接)数、最大对象(连接)数、存活时间和阻塞队列等配置,方便进行统一的管理与复用。通常还会有一些探活机制、gc回收、监控配置等功能。

本文主要基于Golang数据库标准库,介绍分析数据库连接池的基本工作原理。数据库连接池负责对已经建立好的数据库连接进行分配、管理、释放和回收;允许上层应用程序在已有连接的基础上复用,而不需要再重新建立;并且释放超过最大空闲时间的连接,从而避免连接泄漏。能明显提高数据库操作的性能。

数据库连接池带来的优势:

  • 资源重用:避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,另一方面也增进了系统运行环境的平稳性(减少内存碎片以及数据库临时进程/线程的数量);
  • 更快的系统响应速度:数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间;
  • 新的资源分配手段:对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接池技术。通过对应用最大可用数据库连接数的限制,避免某一应用独占所有数据库资源;
  • 统一的连接管理,避免数据库连接泄漏:可根据预先的连接占用超时设定,强制收回被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄漏。

Golang数据库连接池的设计

Golang中对于数据库的操作主要借助标准库database/sql,对上层应用暴露了标准的操作接口与方法,对下层驱动暴露了简单的驱动接口,且内部实现了连接池。从而使得不同的驱动可以方便简单地实现这些驱动接口,而不需关心连接池的细节。

sql.DB数据库连接池的实现逻辑为:

sql-db-workflow

上图中最重要的几个步骤:建立连接、释放连接以及清理连接,下面针对这三个方面进行分析。

建立连接

sql.DB中数据库的连接并不是在sql.Open返回db对象时,连接就建立的,这一步仅仅开了个接收建连请求的channel,实际建连步骤要等到执行具体SQL语句时才会进行。

在database/sql对上层应用暴露的操作接口中,比较常用的是Exec和Query,前者常用于执行写SQL,后者可以用于读SQL。但是不论走哪个方法,都会调用到建连逻辑db.conn方法,附带建连上下文和建连策略两个参数。

create-conn

其中建联策略分为alwaysNewConn和cachedOrNewConn,前者永远走新建连接的策略,后者会从缓存的空闲连接中取出连接,如果没有,则新建一个。

create-conn-code

使用cachedOrNewConn策略的建连逻辑中,会先判断是否有空闲连接:

  • 如果有取出首个空闲连接,紧接着判断该连接是否过期需要被回收:
    • 如果没有过期则可以正常使用进入后续逻辑;
    • 如果没有空闲连接则判断连接数是不是已经达到最大;
  • 若没有可以新建连接,反之就得阻塞这个请求让它等待可用连接。

sql.DB会优先尝试cachedOrNewConn策略,只有在失败了一定次数之后,才会尝试alwaysNewConn建连策略。

cached-or-new-conn

释放连接

连接使用完毕之后,需要被放入连接池,并同时进行对连接的可靠性、合法性检测,如果连接异常,则不应加入连接池,而是应该新建一个健康的连接并替换。该逻辑在db.putConn()中:

release-conn

清理连接

database/sql提供了三个关键参数进行连接池配置,分别是maxIdle、maxOpen和maxLifeTime。

数据库连接无法保证长期有效,比较浪费资源。

  • MySQL服务端默认会强制断连8h的空闲连接,对应客户端在sql.DB中提供maxLifeTime选项设置连接复用的最大时间,这里并非是连接空闲时间,而是从连接建立到这个时间点就会被断连回收,从而保证连接的有效性;
  • sql.DB的清理机制是异步完成的,默认每秒从freeConn池中遍历检查空闲的连接,判断是否超过了maxLifeTime,超出会加入closing数组,并在后续被close掉。

clean-conn-code

参数性能优化

SetMaxOpenConns

默认情况下,sql.DB同时打开的连接数没有限制,若不加限制,在极端情况会把DB连接打满(未加索引,大事务阻塞等情况)。所以可以通过SetMaxOpenConns来配置数据库连接池的最大连接数。

1
2
3
4
5
6
7
8
9
10
// Initialize a new connection pool
db, err := sql.Open("mysql", dbConf)
if err != nil {
log.Fatal(err)
}

// Set the maximum number of concurrently open connections (in-use + idle)
// to 5. Setting this to less than or equal to 0 will mean there is no
// maximum limit (which is also the default setting).
db.SetMaxOpenConns(5)

在此示例代码中,连接池最大限制为5个同时打开的连接。后续如果所有5个连接都已标记为正在使用,并且需要另一个新连接,则应用程序将被迫等待,直到5个连接中的一个被释放并变为空闲。

SetMaxIdleConns

默认情况下sql.DB最多允许在连接池中保留2个空闲连接,可以通过SetMaxIdleConns方法更改空闲连接数。

1
2
3
4
5
6
7
8
9
// Initialize a new connection pool
db, err := sql.Open("mysql", dbConf)
if err != nil {
log.Fatal(err)
}

// Set the maximum number of concurrently idle connections to 5. Setting this
// to less than or equal to 0 will mean that no idle connections are retained.
db.SetMaxIdleConns(5)

MaxIdleConn如何设置?官方推荐idleConn与maxConn数量相同,理由是连接的建立和断连需要开销,且比预想要频繁得多。可以搭配maxLifeTime进行空闲连接的断连配置。

important-things

SetConnMaxLifetime

SetConnMaxLifetime设置了连接可重用的最大时间长度。

1
2
3
4
5
6
7
8
9
10
// Initialize a new connection pool
db, err := sql.Open("mysql", dbConf)
if err != nil {
log.Fatal(err)
}

// Set the maximum lifetime of a connection to 1 hour. Setting it to 0
// means that there is no maximum lifetime and the connection is reused
// forever (which is the default behavior).
db.SetConnMaxLifetime(time.Hour)

在如上示例中,设置了连接可重用的最大时间长度为一小时,则所有的连接将在首次创建后一小时“过期”,并且在它们过期后将无法重用,需重新创建。但需要注意的是:

  • 不能硬性保证连接将在池中存在一个小时;很可能由于某种原因连接不可用,并在此之前自动关闭;
  • 建立连接后,在一个多小时后仍然可以使用,只是无法开始重用;
  • 不是连接空闲超时。连接将在第一次创建后一小时到期,而不是在上一次空闲后一小时;
  • 每秒执行一次清理操作,以自动删除池中的“过期”连接。

从理论上讲,maxLifetime连接越短,连接终止的频率就越高,因此,需要从头开始创建连接的频率就越高。所以考虑到重新建连的开销,不应把maxLifetime时间设置过短,但建议小于服务端主动关闭连接的时间(默认8小时)。

总结

基于本文分析,对上述3个参数进行一个简单的总结:

  1. 根据经验,应该明确设置一个MaxOpenConns值;
  2. 通常更高的MaxOpenConns和MaxIdleConns值理论上会有更好的性能;但太大的空闲连接池(连接长时间未被重用并最终被标记回收)可能会导致性能下降;
  3. 为了规避2中的风险,需要设置一个相对较短的ConnMaxLifetime,但也要考虑连接不被太频繁断连和重建;
  4. MaxIdleConns建议与MaxOpenConns设置相同。

例如:

1
2
3
db.SetMaxOpenConns(32)
db.SetMaxIdleConns(32)
db.SetConnMaxLifetime(5*time.Minute)