DAS性能优化之Tomcat数据源调优
本文转载于 SegmentFault 社区
背景
我们先回顾上一篇《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 上开源:
https://github.com/ppdaicorp/das