http://www.sohu.com/a/138978965_505779


在某次大范围网络变更中,有些应用出现了数据库连接在网络恢复后不能自动恢复的情况,只有重启解决。这一般是数据库连接池配置不合理引起的,那么我们就拿tomcat-jdbc为例看看数据库连接池配置的一些注意事项。

对于数据库连接这种比较重的资源,我们在使用中一般都会进行池化,也就是我们会将创建好的数据库连接放到池子里,当我们需要使用的时候从池子里取,而不是创建新的(当然,是不是创建新的也要看连接池的配置),我们用完连接后归还池子留着以后使用。

那么这样就有一个问题:我们放在池子里的连接还是有效的么?因为数据库连接其实归根结底也就是一个TCP连接,而TCP连接如果你不真的去操作它(读取或写入),你是不知道这条连接的状态的。比如我们现在要进行一次数据库操作,然后我们从池子里拿出一个连接,然后组织好SQL和参数,发起操作。发起之后出异常了,因为这个连接已经无效了。那么这就涉及数据库连接的有效性测试了,不过这个测试也是有讲究的。

就拿tomcat-jdbc(其他连接池基本有类似参数)这个连接池来说就有testOnBorrow,testOnReturn,testWhileIdle和testOnConnect这么多姿势,可见这个数据库连接的有效性检测也是有点猫腻,我们先从代码层面上来看看这几种测试具体的区别,然后再谈谈我的看法,最后我们可以进行性能测试,看看这些参数对性能的具体影响。

testOnBorrow

从字面意思上理解就是,从连接池拿连接的时候会进行有效性测试。那么看起来,一个数据库密集操作的应用来讲,如果每次从池里拿连接都要进行一下测试,成本好像太高了点。那真实的情况是这样么?

其实在有效性测试时我们还有另外两个参数需要关注:a. validationInterval, b. validationQuery。这两个参数分别是测试间隔和测试所使用的SQL(一般大家都会使用SELECT 1)。其实对于testOnBorrow来讲,也并不是每次拿连接的时候都会进行有效性测试,而也是要看validationInterval,如果上一次的测试时间到现在超过了这个时间,则会进行有效性测试。所以testOnBorrow正确的含义应该是这个有效性测试发生在拿连接的时候,但是不是要测试还要看测试间隔(validationInterval)。

了解了testOnBorrow后,另外几个也就没有什么了。

testOnReturn: 连接归还连接池的时候检测,但也会检查validationInterval

testWhileIdle: 后台有个线程会扫描连接池里没有正在使用的连接,然后如果上次检测时间到现在超过了validationInterval则对其进行检测。

testOnConnect: 连接创建的时候检测,连接创建时就不受validationInterval限制了。另外,即使testOnConnect没有设置,如果设置了initSQL也会进行测试,如果设置了initSQL则测试的SQL就是initSQL而不是validationQuery。不过一般来讲连接创立时感觉不需要测试(如果谁有更好的理由可以告诉我),连接能建立成功表示1: 网络是通的,并且mysql监听端口也是通的 2: TCP连接建立后,mysql client还需要和mysql server进行握手并获取server端一些环境变量,所以基本上可以认为连接是有效的,不过我们倒可以利用initSQL来做一些检查:比如关键数据库表是否存在等。

ok,我们现在基本上了解了这几个参数表达的含义,那么设置和不设置这几个参数的影响是什么呢?

如果这几个参数我们都不设置,那意思是我们不进行连接池里连接有效性检查,那么一旦因为网络等原因导致连接失效,则这个连接永远都无法恢复了,只有重启解决(不过也有一些其他参数可以让连接恢复,后文会提到),我想这不是我们想要的。那么,如果我们要设置该如何设置呢?testOnBorrow和testOnReturn都是在使用连接的路径上进行测试,所以如果进行测试的时候,会稍稍增加一些延迟,不过我觉得这个延迟并不太大,后面我会进行实际的测试。而testWhileIdle是后台线程对空闲连接测试,不在连接的使用链路上,不会增加延迟,看起来更合理。但是,对于一个繁忙的应用来讲,估计很少有连接是空闲的,我觉得对于繁忙的关键应用,仅设置testWhileIdle并不合适,而对于一个不太繁忙的应用可能连接大部分都是空闲的,进行testWhileIdle测试又有点浪费的感觉。而如果设置testOnReturn,当高峰期过后,经过有效性测试的连接变成空闲连接,这个期间很有可能连接失效了,当高峰期再次来临的时候拿到的是失效连接。所以,我觉得应该默认推荐testOnBorrow(千万不要被名字迷惑,并不是每次都测试)。

除了上面连接测试相关的几个参数外,还有几个参数在使用连接池的时候也要注意:

maxActive,maxIdle,minIdle,initialSize

这几个从字面上就能很好理解了。maxActive是最大活跃连接数,这个数字不宜设置过大,太多的并发连接对数据库的压力很大,甚至会导致雪崩,这是一定要注意的。但是如果设置过小,而应用的服务线程数有很高,可能会导致有的服务线程拿不到连接,所以服务的线程数和数据库连接数是需要经过配合调整的。minIdle是允许的最小空闲连接数,比如当高峰期过后,连接使用的少了,但是连接池还是会为你留着minIdle的连接,以备高峰期再次来临的时候不需要创建连接。而initialSize就是连接池初始化的时候就为你先创建这么多个连接出来,这个参数其实是很有用的。比如我们一个应用刚启动的时候,有的同学会发现为啥处理速度这么慢呢?有一个原因可能就是要新创建连接,那么我们可以设置一个初始值,确保应用启动后,流量进入的时候已经有一些可以服务的连接了。

maxAge

这个参数是一个时间值,当连接归还到连接池的时候会判断,如果开始连接的时间到现在超过maxAge,则会将该连接关闭。初看起来这个有点莫名其妙,连接池不就是为了连接复用么,为啥又要关闭连接呢?其实这个参数我也认为是有点打补丁的感觉。这个参数主要是为了防止连接上附加了太多的资源,比如我们一个应用,因为某些原因我在连接上附加一个attaments,这个attaments可能占用很大的内存,如果我们的连接从来不释放,即使是归还到连接池成为空闲连接,它还是会占用这么多资源。所以连接池就提供了一种方式,你可以设置一个时间,当连接归还到池里的时候检查一下,如果超时就关闭了,避免资源长时间泄露(这就像我们有一个系统,因为代码没写好会导致资源泄露,然后为了紧急修复我们就写了个脚本,每天高峰期来临之前先重启一下系统)。

这个参数看起来也能防止因为网络等原因导致连接失效而不能自动恢复的问题,但是这个参数是会关闭连接的,如果设置过短则会导致连接不断地重连,连接池失去作用,设置过长则不能解决连接失效的问题,所以连接失效还是要靠那几个test的参数。

removeAbandoned, logAbandoned, abandonWhenPercentageFull和removeAbandonedTimeout

这几个参数是用来控制,如果你从连接池里拿了一个连接,但是很久很久(>=removeAbandonedTimeout)都没归还(比如连接泄露),则这个时候连接池会强制的关闭连接(还受abandonWhenPercentageFull参数控制,这个参数是一个百分比,也就是busy/maxActive如果超过这个百分比并且超时则关闭,默认值是0,所以仅仅检测超时)。

logAbandoned顾名思义,就是会把上面的情况日志记录下来,并且日志里还有这个拿这个连接时候的调用栈,这对于定位连接在哪里泄露的挺有帮助的,但是记录调用栈是有成本的,每次拿连接的时候都会new一个Exception,然后拿到stackTrace,所以建议不要打开这个,即使要打开也不要在生产上打开。而removeAbandoned,现在大家都会用ORM来数据访问,一般不会有连接泄露,但是如果碰到那种长时间执行的SQL也是可以检测出来的,比如有的生成报表的SQL传说有跑个把小时的。

好了,我们已经了解了连接池连接有效性检测,然后还有其他几个参数的介绍。那么我们现在来实际测试一下,连接池有效性测试对我们的数据库和应用会有多少影响?我们会进行一下测试:

配置:

  1. mysql 5.6

  2. server: 4C4G虚拟机

  3. client: 4C4G虚拟机3台

三台机器,每台创建2个线程不断地执行SELECT 1,总QPS在2.2万左右,mysql的响应时间99%为0.000100秒,mysql机器CPU占用为20%左右。每台开启5个线程QPS能达到5.5万以上,响应时间分变化不大,但是出现了少数几个0.100000的查询,msyql机器CPU占用为50%,load达到5。

感觉还行,这么一台配置不咋地的机器能够到5.5万qps,生产上的数据库服务器配置应该在这10倍左右。假设我们一台数据库服务器为10个应用提供服务,每个应用开启100个线程,每个线程拿到1个连接,每30秒执行一次SELECT 1,那么QPS即30左右,这个对数据库几乎是没有任何影响的,而且这个100个线程也是高估的数字。

而且按照响应时间分布,在testOnBorrow时执行健康检查,可能会对进行检查的请求(不是所有请求,并且比例非常非常低)增加一点点延迟(0.000100),但是同时我们提高了可靠性,在出现网络抖动的时候连接可以自动恢复,我觉得这是非常值得的,所以强烈推荐给连接池配置上testOnBorrow