¶性能优化-连接池排队问题
数据库连接池中还有很多空闲连接,为什么应用的数据库操作都在排队等待获取和归还连接?
¶背景
生产环境系统上线前进行压测,应用共4个节点,每个节点4核8G,每个节点连接池大小1000,200业务并发(非单接口绝对并发);从监控发现Druid连接池获取连接和释放连接都需要300ms左右的时间,这对于整个系统的吞吐量影响特别大。
某次查询数据库操作中方法调用堆栈耗时监控:
com.alibaba.druid.pool.DruidDataSource.getConnection():耗时327ms
com.oceanbase.jdbc.JDBC4PreparedStatement.execute():耗时3ms
com.alibaba.druid.pool.DruidPooledConnection.close():耗时294ms
¶问题排查过程
¶线程堆栈分析
dump应用的线程信息,发现有217个线程在等待获取数据库连接,有156个线程在等待释放数据库连接,关键线程信息如下所示:
1 | 217个线程在等待获取数据库连接 |
¶怀疑数据库连接泄露
数据库连接泄露的问题主要有两类:
- 手动获取数据库连接,未释放。
- 手动开始事务,未结束(提交或回滚)。
全局搜索项目源码,未发现以上两种情况。根据压测结束后堆dump信息(没有开连接池信息打印),从堆信息中发现DruidDataSource对象的池中连接数poolingCount=1000,也就是说所有的连接已全部归还到连接池中。
因此数据库连接泄露的嫌疑被排除。
¶怀疑有慢sql或者有大事务长时间占用连接
根据线程信息分析,正在执行数据库操作com.oceanbase.jdbc.JDBC4PreparedStatement.execute的线程仅仅只有一个。因此有大量慢sql,导致数据库连接池耗尽的嫌疑被排除。
从线程信息中不能直接发现是否有大事务长时间占用连接,根据源码分析又仿佛大海捞针,这里再看看堆dump信息,发现DruidDataSource对象的活跃连接数峰值activePeak=303,也就是说连接池中始终都有空闲可用的连接。因此有大事务长时间占用连接,导致数据库连接池耗尽的嫌疑被排除。
¶怀疑连接池本身的性能问题
从线程信息中发现,所有的获取和释放连接的线程都在等待同一把锁(公平锁):- parking to wait for <0x000000055d50cf68> (a java.util.concurrent.locks.ReentrantLock$FairSync),对应DruidDataSource的关键源码如下:
1 |
|
查阅Druid的文档,发现有介绍Druid锁的公平模式问题:https://github.com/alibaba/druid/wiki/Druid锁的公平模式问题
| 版本 | 处理方式 | 效果 |
|---|---|---|
| 0.2.3之前 | unfair | 并发性能很好。 maxWait>0的配置下,出现严重不公平现象 |
| 0.2.3 ~ 0.2.6 | fair | 公平,但是并发性能很差 |
| 0.2.7 | 通过构造函数传入参数指定fair或者unfair,缺省fair | 按需要配置,但是比较麻烦 |
| 0.2.8 | 缺省unfair,通过构造函数传入参数指定fair或者unfair; 如果DruidDataSource还没有初始化,修改maxWait大于0,自动转换为fair模式 |
智能配置,能够兼顾性能和公平性 |
应用确实配置了maxWait参数,从线程信息中看也确实是使用的公平锁ReentrantLock$FairSync,在高并发下性能表现很差。
到此为止,基本可以确定公平锁的并发性能差导致连接池排队等待获取和归还连接问题,下面让我们来验证下公平锁和非公平锁对性能的影响到底有多大。
¶锁的公平模式性能验证
- 测试接口每次请求中并发查询30次简单sql,具体代码如下:
1 | public String selectTest() { |
-
应用配置:
- 单节点4核16G
- JVM内存:-Xms5120m -Xmx5120m -Xmn1706m
-
压测、监控工具
- JMeter
- javaagent:性能优化利器-JavaAgent
-
压测结果:
| 锁模式 | 数据库连接池大小 | 并发数 | 获取连接:getConnectionInternal 平均耗时ms |
归还连接:recycle 平均耗时ms |
连接池最大活跃数:activePeak | 接口请求样本数 | 接口请求响应平均ms | 接口请求响应90%百分位ms | 接口请求响应最小值ms | 接口请求响应最大值ms | 接口请求吞吐量 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 非公平锁 | 200 | 100 | 5 | 1 | 200 | 631733 | 534 | 972 | 7 | 5689 | 187.2/sec |
| 公平锁 | 200 | 100 | 107 | 62 | 200 | 292050 | 959 | 1324 | 354 | 3533 | 104.1/sec |
| 非公平锁 | 500 | 100 | 1.8 | 2.2 | 500 | 220793 | 525 | 950 | 7 | 2976 | 189.0/sec |
| 公平锁 | 500 | 100 | 71 | 70 | 500 | 152745 | 785 | 1207 | 347 | 5407 | 127.2/sec |
| 非公平锁 | 1000 | 100 | 1 | 1.5 | 510 | 232726 | 520 | 947 | 8 | 2547 | 193.9/sec |
| 公平锁 | 1000 | 100 | 83 | 82 | 557 | 134607 | 891 | 1367 | 68 | 5440 | 112.1/sec |
- 压测结论:非公平锁模式下获取和归还连接的性能遥遥领先公平锁模式。
¶解决方案
数据库连接池推荐配置(连接池大小需根据实际情况调整)
- initialSize:500
- minIdle:500
- maxActive:500
- maxWait:6000
- keepAlive:true
- 在连接池初始化之前,手动设置:dataSouce.setUseUnfairLock(true)
更多推荐配置见:https://github.com/alibaba/druid/wiki/DruidDataSource配置
修改Druid连接池配置后,更新应用到生产环境复测(200并发),javaagent监控druid相关方法耗时从几百ms降低到不足个位数,事务操作耗时从平均10s降低到平均2s,至此数据库操作都在排队等待获取和归还连接问题得以解决。
尽管极端情况下,在连接池中的连接不够用大量线程争用连接时,unfair模式的ReentrantLock.tryLock方法存在严重不公的现象,个别线程会等到超时了还获取不到连接。
个人观点:数据库连接池的锁调整为非公平锁整体来看利远大于弊,如果真的有这么大的并发量,更应该增加应用节点数量,缓解单节点的压力。
¶参考文档
- 性能优化利器-JavaAgent
- https://github.com/alibaba/druid/wiki/DruidDataSource配置
- https://github.com/alibaba/druid/wiki/Druid锁的公平模式问题
- https://jmeter.apache.org/usermanual/component_reference.html#Aggregate_Report