前言
一个月前,笔者写了篇Flink维表关联方面的文章,其中将Flink异步I/O与Vert.x JDBC Client配合使用,以解决异步从MySQL获取维度数据的需求。但当时有个数据库连接方面的坑没来得及说,正好今天忙得很,随便写几句话来填一下。
发现问题
程序上到预发布环境的第二天,TaskManager日志中有时出现以下异常。
分析问题
MySQL服务器会自动关闭空闲时间超过wait_timeout
的连接,防止连接数过多。但是MySQL关闭连接之后,客户端可能会认为该连接仍然有效,当试图使用该连接通信时就会抛出上图所示的异常。
wait_timeout
参数的默认值是28800秒,所以这个现象也俗称“8小时问题”。
mysql> show variables like '%wait_timeout%';
+--------------------------+----------+
| Variable_name | Value |
+--------------------------+----------+
| innodb_lock_wait_timeout | 10 |
| lock_wait_timeout | 31536000 |
| wait_timeout | 28800 |
+--------------------------+----------+
3 rows in set (0.01 sec)
解决问题
根据异常内容的指示,有以下几种方案。
调大wait_timeout参数的值
不太靠谱。容易造成空闲连接堆积,甚至达到MySQL的连接数上限。
连接串添加autoReconnect=true
MySQL官方文档的描述如下图。
可见同样不靠谱,因为使用无效的连接通信仍然会抛出异常,并且在其他SQLException发生时无法保证session状态正常和数据的一致性。如果实在没办法,调大wait_timeout都比使用autoReconnect=true要好。
连接池添加空闲检测
空闲检测的目的是保活,即在连接空闲时间超过wait_timeout之前,向MySQL发送一个类似心跳的查询语句,让MySQL不会主动断开它。
C3P0连接池的配置方法如下。
c3p0.idleConnectionTestPeriod=1000
c3p0.preferredTestQuery=SELECT 1
参数c3p0.idleConnectionTestPeriod表示执行空闲检测的周期,单位为秒,比wait_timeout小一些就可以了。默认值为0,表示不启用。
c3p0.preferredTestQuery则表示扮演心跳角色的查询,像SELECT 1
这种不依赖于任何表的简单查询就很合适。如果不指定该参数,就会使用JDBC自带的isValid()方法来检测。
虽然各种连接池的配置方法各不相同(如DBCP连接池的对应参数是testWhileIdle等),但空闲检测基本都是异步的,对性能影响小,推荐使用。
连接池添加连接状态检测
所谓连接状态检测,就是客户端在从连接池获取或向连接池归还一个连接时检测其有效性(即是否已经被MySQL断开了),比上一种方式更安全,但是效率往往不如上一种方式高。
C3P0连接池的配置方法如下。
c3p0.testConnectionOnCheckin=true
c3p0.testConnectionOnCheckout=false
熟悉DBCP等连接池的看官可能对这两个参数感到摸不着头脑,其实testConnectionOnCheckin的语义就等于testOnReturn(归还连接时检测有效性),而testConnectionOnCheckout等于testOnBorrow(借用连接时检测有效性),但前者是异步的,后者是同步的。所以为了保证性能,C3P0官方建议把前者设为true,而后者设为false。
The End
Vert.x JDBC Client默认使用的连接池实现是C3P0(另外还有BoneCP和Hikari可选),但是在JDBCClient.create()/createShared()
方法中支持的参数并不多(见官方文档)。要设定上述空闲检测和连接状态检测的参数,得将它们写入c3p0.properties文件,并置于classpath下。内容就是:
c3p0.testConnectionOnCheckin=true
c3p0.testConnectionOnCheckout=false
c3p0.idleConnectionTestPeriod=1000
c3p0.preferredTestQuery=SELECT 1
重新启动Flink任务,观察TaskManager日志,可以发现设定都生效了。
民那晚安晚安。