背景
我们先回顾上一篇《DAS解决访问数据库延时突高的案例分享》。上一次为了解决数据库偶发连接高耗时的问题,我们将参数minIdle从0改为1,在连接池中始终保留一个连接。然后为了确保连接的有效性,又把参数testOnBorrow改为true。成功解决偶发高延时问题。随后,我们DAS团队对Tomcat数据源做了更多的了解和研究,包括研读它的源代码,发现它仍旧有优化的空间。
我们上次调优主要是将testOnBorrow配置为true。虽然保证了返回连接的有效性,但是这也意味着多了一次数据库连接执行'SELECT 1'的检查。我们进一步分析性能消耗时,发现Tomcat数据源并非每次都会做检查,如果在最近一个时间段内(validationInterval参数)这个连接已经被检查过,那么就不会再做检查。虽然这个机制减小了检查操作的几率,但还是存在做检查可能性,不够完美。毕竟作为一款几百个应用都使用的产品,哪怕一次细小的改进都会有巨大的积累效应。
怎么样才能在testOnBorrow=false的配置下,最大程度保证返回连接的有效性呢?要达到这样的效果,就需要对Tomcat数据源有更深入的理解。因此本文会从介绍Tomcat数据源的内部构造和原理入手来介绍这次优化工作。
Tomcat数据源的定位
Tomcat数据源是对JDBC DataSource的一个具体实现。根据官方对DataSource的解释,DataSource除了提供数据库连接Connect的功能(pooled connections)之外,它还能提供分布式事务的支持(distributed transaction)。
在实际工作中,我们主要把它作为connection pool来用,因此它本质是一个缓存系统,它缓存对象的是数据库连接Connect。
缓存系统的evict
对于一个缓存系统来说,它最核心功能是能够自动剔除(evict)不再需要的缓存对象,否则它只是个简单的map。所以如果你能搞清楚缓存的evict机制,那么你对这个缓存系统就有了最核心的理解。那我们就来看看Tomcat数据源作为缓存系统,它的evict机制是什么样的?
先来看evict的策略。一般evict机制有两种策略:time based和size based。time based是根据缓存对象的过期时间来判断,譬如规定存在超过1分钟的对象就需要从缓存中被剔除。size based是一旦缓存对象数目超出阈值系统就开始evict,否则会产生内存或者资源的泄漏。Tomcat数据源同时采用了这种两种策略。做evict的时候,既要判断数据库连接对象存在的时间,同时又要保证数据库连接的总数保持在阈值之内。
那evict在什么时机触发呢?触发evict可以有两种做法:
一:在取get或者存put的时候,检查缓存内容,剔除不再需要的缓存对象。有些本地缓存软件就用这种做法,它比较简单,不需要额外线程,但是存取的操作需要花费额外检查的逻辑。
二:后台启动一个检查缓存的线程,定期检查剔除不再需要的缓存对象。Tomcat数据源使用的是第二种做法。
Tomcat数据源数据结构
我们再结合Tomcat数据源内部基本的数据结构来看evict机制。
Tomcat数据源内部数据结构大致会分为两个集合:idle集合和busy集合,两个集合里放的都是数据库连接。Idle集合就是一个缓存集合,专门存放未被被应用使用的数据库连接,而busy集合指的是被应用正在使用的连接。一般情况下,当应用从数据源获取数据库连接的时候,一个数据库连接会从idle集合中去获取,然后进入busy集合。当使用结束后再从busy集合回到idle。
数据库连接就在这两个集合之间移动。连接从idle集合移到busy集合叫做borrow,会触发testOnBorrow的事件;反之,连接从busy集合移到idle集合叫做return,会触发testOnReturn的事件。用户可以利用这两个事件,对连接的有效性做检查,将失效连接剔除出数据源。
一图胜千言:
了解了静态数据结构之外,很重要的是理解它后台线程做的工作。这个线程的名字是PoolCleaner,从这个命名也能猜出它的功能。它会定期检查idle集合,剔除超过那些时间超出minEvictableIdleTimeMillis,以及通不过testWhileIdle检测的连接。
对Tomcat数据源的内部基本结构有了了解之后,我们可以看一下这一次我们具体的优化点。
- 将testOnBorrow改为false:
这个修改是这次优化主要的重点。testOnBorrow事件发生在数据库连接从idle集合移动到busy集合过程中,也就是准备向应用提供数据库连接的时候。文章开头提到过,如果是true的话,testOnBorrow事件有可能会触发做一次正真的‘SELECT 1’检查动作。经过测试,在极端情况下这个检查动作会产生大约10%的额外时间开销。当然,由于之前提到的validationInterval参数的作用,实际上的额外开销会小得多。
回到我们的问题:既想把testOnBorrow设置为false,又要最大程度保证返回连接的有效性。而问题的答案就是需要其他参数的配合。 - 减小maxAge:
这是Tomcat数据源独有一个配置参数。这里的age指的就是一个数据库连接从连接开始到目前时间点的时间长度(now - time when connected)。这个参数的设定可以有效防止数据库连接的生命周期过长导致失效。当一个数据库连接从idle集合borrow到busy集合,或者从busy集合return回idle集合的时候,它的age会被检查。如果这个age大于设定的maxAge值,那这个连接就会被抛弃。我们将这个参数从默认的7个多小时减小到15分钟。减小maxAge可以有效提高连接池返回有效连接的概率。
注意,这个参数在Tomcat数据源不同版本有不同的定义。在7.x的版本中maxAge只会在return的时候被检查,在8.x之后,maxAge会在return和borrow的时候都检查。 - 将testWhileIdle改为true:
testOnBorrow=false的副作用就是不能保证返回的数据库连接的有效性,怎么办?Tomcat数据源还提供了另外一个test检查的配置,叫做testWhileIdle。这个test同样会用SELECT 1查询的方式来检查idle集合里的缓存的数据库连接的有效性,通不过test的连接会从idle集合里被剔除。这个动作就是之前提到的那个PoolCleaner线程做的事情。有了testWhileIdle的检查既能提高数据库连接返回的有效性,又不会影响取连接的性能。 - 减小minEvictableIdleTimeMillis:
这个参数决定了呆在idle集合里数据库连接的时间长度,也就是evict策略中的time based策略。我们将这个时间长度从10分钟减小到了30秒。减小了缓存连接的时间,就是提高了剩余连接的有效性。
验证与上线
和之前的流程一样,我们先在本地开发环境做了充分的测试,然后上测试环境观察了一周左右。最后上预发和生产环境观察连续观察几天,没有发生任何问题,优化成功。
我们这两次的优化结果都是成功的,但是从另外的角度来看本质又有不同:上一次是被动的用户问题驱动的优化,这一次是主动技术优化。主动技术优化,我们也是出于两方面的考量:
首先,从技术上来看,虽然这次做的是比较小的优化,但是作为一个服务于几百个应用的组件来说,它的积累效应会被放大。这也是我们中间件团队有别于其他技术团队的一个特点。中间件产品的一个细小的优化,为公司带来的可能就是成千上万个服务器上的CPU,内存和网络开销的节省。常言道 “勿以善小而不为”,对于我们中间件团队来说就是“勿以优化小而不为”。其次,从人的学习的规律来看,当第一次进入一个领域研究学习之后,称热打铁也更有利于知识的深度学习和积累,学无止境。这些就是我们再次做优化的动力!
DAS项目从去年开始已经在GitHub上开源,欢迎同学们去clone,fork,提问和加星★:https://github.com/ppdaicorp/das
作者介绍
Shengyuan,信也科技布道师、框架中间件资深专家、目前主要从事DAS相关的研发、运维工作。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。