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中数据库的连接并不是在sql.Open返回db对象时,连接就建立的,这一步仅仅开了个接收建连请求的channel,实际建连步骤要等到执行具体SQL语句时才会进行。
在database/sql对上层应用暴露的操作接口中,比较常用的是Exec和Query,前者常用于执行写SQL,后者可以用于读SQL。但是不论走哪个方法,都会调用到建连逻辑db.conn方法,附带建连上下文和建连策略两个参数。
其中建联策略分为alwaysNewConn和cachedOrNewConn,前者永远走新建连接的策略,后者会从缓存的空闲连接中取出连接,如果没有,则新建一个。
使用cachedOrNewConn策略的建连逻辑中,会先判断是否有空闲连接:
- 如果有取出首个空闲连接,紧接着判断该连接是否过期需要被回收:
- 如果没有过期则可以正常使用进入后续逻辑;
- 如果没有空闲连接则判断连接数是不是已经达到最大;
- 若没有可以新建连接,反之就得阻塞这个请求让它等待可用连接。
sql.DB会优先尝试cachedOrNewConn策略,只有在失败了一定次数之后,才会尝试alwaysNewConn建连策略。
释放连接
连接使用完毕之后,需要被放入连接池,并同时进行对连接的可靠性、合法性检测,如果连接异常,则不应加入连接池,而是应该新建一个健康的连接并替换。该逻辑在db.putConn()中:
清理连接
database/sql提供了三个关键参数进行连接池配置,分别是maxIdle、maxOpen和maxLifeTime。
数据库连接无法保证长期有效,比较浪费资源。
- MySQL服务端默认会强制断连8h的空闲连接,对应客户端在sql.DB中提供maxLifeTime选项设置连接复用的最大时间,这里并非是连接空闲时间,而是从连接建立到这个时间点就会被断连回收,从而保证连接的有效性;
- sql.DB的清理机制是异步完成的,默认每秒从freeConn池中遍历检查空闲的连接,判断是否超过了maxLifeTime,超出会加入closing数组,并在后续被close掉。
参数性能优化
SetMaxOpenConns
默认情况下,sql.DB同时打开的连接数没有限制,若不加限制,在极端情况会把DB连接打满(未加索引,大事务阻塞等情况)。所以可以通过SetMaxOpenConns来配置数据库连接池的最大连接数。
1 | // Initialize a new connection pool |
在此示例代码中,连接池最大限制为5个同时打开的连接。后续如果所有5个连接都已标记为正在使用,并且需要另一个新连接,则应用程序将被迫等待,直到5个连接中的一个被释放并变为空闲。
SetMaxIdleConns
默认情况下sql.DB最多允许在连接池中保留2个空闲连接,可以通过SetMaxIdleConns方法更改空闲连接数。
1 | // Initialize a new connection pool |
MaxIdleConn如何设置?官方推荐idleConn与maxConn数量相同,理由是连接的建立和断连需要开销,且比预想要频繁得多。可以搭配maxLifeTime进行空闲连接的断连配置。
SetConnMaxLifetime
SetConnMaxLifetime设置了连接可重用的最大时间长度。
1 | // Initialize a new connection pool |
在如上示例中,设置了连接可重用的最大时间长度为一小时,则所有的连接将在首次创建后一小时“过期”,并且在它们过期后将无法重用,需重新创建。但需要注意的是:
- 不能硬性保证连接将在池中存在一个小时;很可能由于某种原因连接不可用,并在此之前自动关闭;
- 建立连接后,在一个多小时后仍然可以使用,只是无法开始重用;
- 不是连接空闲超时。连接将在第一次创建后一小时到期,而不是在上一次空闲后一小时;
- 每秒执行一次清理操作,以自动删除池中的“过期”连接。
从理论上讲,maxLifetime连接越短,连接终止的频率就越高,因此,需要从头开始创建连接的频率就越高。所以考虑到重新建连的开销,不应把maxLifetime时间设置过短,但建议小于服务端主动关闭连接的时间(默认8小时)。
总结
基于本文分析,对上述3个参数进行一个简单的总结:
- 根据经验,应该明确设置一个MaxOpenConns值;
- 通常更高的MaxOpenConns和MaxIdleConns值理论上会有更好的性能;但太大的空闲连接池(连接长时间未被重用并最终被标记回收)可能会导致性能下降;
- 为了规避2中的风险,需要设置一个相对较短的ConnMaxLifetime,但也要考虑连接不被太频繁断连和重建;
- MaxIdleConns建议与MaxOpenConns设置相同。
例如:
1 | db.SetMaxOpenConns(32) |