踩坑DruidDataSource导致的服务卡死
在我们项⽬中,Druid提供了⼀个⾼效、功能强⼤、可扩展性好的数据库连接池。我们使⽤他来替代C3P0作为数据库的连接池;
翻车背景
平台私有化给⼀个三⼗⼈的⼩团队使⽤,某天有⼈反馈平台⽆响应,接⼝全部超时⽆响应;
排查过程
1. 连上服务器,发现服务状态都健康,内存CPU等都很稳定;
2. jstack查看线程状态,发现所有容器⼯作线程都是wait状态,如下:
"XNIO-1 task-5" #178 prio=5 os_prio=0 tid=0x000000002a03e000 nid=0x350c waiting on condition [0x000000004013a000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000006c4f36dd0> (a urrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at urrent.locks.LockSupport.park(LockSupport.java:175)
at urrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:2029)
at com.alibaba.druid.ConnectionInternal(DruidDataSource.java:1557)
at com.alibaba.druid.ConnectionDirect(DruidDataSource.java:1337)
at com.alibaba.druid.Connection(DruidDataSource.java:1317)
at com.alibaba.druid.Connection(DruidDataSource.java:1307)
at com.alibaba.druid.Connection(DruidDataSource.java:109)
at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:158)
at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:116)
at org.springframework.jdbc.Connection(DataSourceUtils.java:79)
ansaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:82)
Connection(SpringManagedTransaction.java:68)
可以看见所有线程都卡在了DruidDataSource.takeLast⽅法的2029⾏,我们查看源码到这⾏代码:
// maxWait是获取连接等待超时时间默认是-1,即不超时,此时会⾛到takeLast⽅法
if (maxWait > 0) {
holder = pollLast(nanos);druid连接池配置详解
} else {
holder = takeLast();
}
DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
while (poolingCount == 0) {
emptySignal(); // send signal to CreateThread create connection
if (failFast && isFailContinuous()) {
throw new DataSourceNotAvailableException(createError);
}
notEmptyWaitThreadCount++;
if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) {
notEmptyWaitThreadPeak = notEmptyWaitThreadCount;
}
try {
// 因为数据库的连接都没有释放且被占⽤,连接池中⽆可⽤连接,导致请求被阻塞了
notEmpty.await(); // signal by recycle or creator
} finally {
notEmptyWaitThreadCount--;
}
notEmptyWaitCount++;
if (!enable) {
connectErrorCountUpdater.incrementAndGet(this);
throw new DataSourceDisableException();
}
}
} catch (InterruptedException ie) {
notEmpty.signal(); // propagate to non-interrupted thread
notEmptySignalCount++;
throw ie;
}
decrementPoolingCount();
DruidConnectionHolder last = connections[poolingCount];
connections[poolingCount] = null;
return last;
}
到问题所在后,我们到服务排查为什么会出现这样的情景,发现服务⾥使⽤了多数据源,这就导致了必须⾃⼰创建DataSource,继续查看创建DataSource的地⽅    @Bean("masterDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
ate().type(com.alibaba.druid.pool.DruidDataSource.class).build();
}
这⾥创建的时候直接使⽤了DruidDataSource的默认配置,没有⼿动修改任何配置,⽽在DruidDataSource中maxwait默认值为-1:
public final static int                            DEFAULT_MAX_WAIT                          = -1;
protected volatile int                            maxActive                                = DEFAULT_MAX_ACTIVE_SIZE;8个
这就会导致代码分叉到takeLast上,然后刚好如果所有线程都被使⽤且没有正常释放,那么就会导致⼀直await卡死;
场景复现
那么问题的发⽣场景就很明了了,需要两个条件,
maxWait使⽤默认值-1,即不会超时
poolingCount == 0,即线程池可⽤数量为0
其余参数均使⽤默认值;
我们尝试使⽤Jmeter来复现问题;
Jmeter设置100个并发,在300秒内启动,观察服务;发现在并发13个左右(多次)线程就会导致服务卡死,此时Jstack查看线程发现所有线程均在wait;
因为maxActive默认值为8,所以超过8个后,⼀旦数据库连接不及时释放,则会导致poolingCount ==0,此时刚好maxWait为-1,则进⼊takeLast复现此问题
尝试解决
在⽹上搜索‘ druid的takeLast导致卡死’,可以看到很多⼈遇到了同样的问题;⽽且在官⽅的issue也可以查到相关的提交;如下:
可以看到⾥⾯的回复⼤多是贴上⼀两个配置,其实这样都是不可解决问题的;知其然知其所以然,我们必须弄明⽩问题的本事在哪⾥,才可以解决问题;
就此问题本⾝来说,看着代码思考,那么我设置下这个参数使得参数不为默认值(-1),让他避免这个问题不就可以了吗?
dataSource.setMaxWait(100);
我们设置超时时间(毫秒)为100,此时我们通过压测再来验证⼀下这个问题,结果发现继续报错,错误为获取不到连接;
因为连接默认只有8个,设置超时时间后,因为连接不能及时的释放,所以有可能拿不到连接;
### Cause: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 101, active 8, maxActive 8, creating 0 at org.ptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
flect.GeneratedMethodAccessor268.invoke(Unknown Source)
flect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at flect.Method.invoke(Method.java:498)
batis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
... 104 common frames omitted
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 101, active 8, maxActive 8, creating 0所以,我们还需要配置⼀下连接数来保证可⽤,以下为druid的配置说明,可以酌情使⽤:
属性说明建议值
url数据库的jdbc连接地址。⼀般为连接oracle/mysql。⽰例如下:
mysql : jdbc:mysql://ip:port/dbname?option1&option2&…
oracle : jdbc:oracle:thin:@ip:port:oracle_sid
username登录数据库的⽤户名
password登录数据库的⽤户密码
initialSize启动程序时,在连接池中初始化多少个连接10-50已⾜够
maxActive连接池中最多⽀持多少个活动会话
maxWait程序向连接池中请求连接时,超过maxWait的值后,认为本次请求失败,即连接池100
没有可⽤连接,单位毫秒,设置-1时表⽰⽆限等待
minEvictableIdleTimeMillis池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,将回收该连接,要⼩于
防⽕墙超时设置
见说明部分
netfilter.nf_conntrack_tcp_timeout_established的设置
timeBetweenEvictionRunsMillis检查空闲连接的频率,单位毫秒, ⾮正整数时表⽰不进⾏检查
keepAlive程序没有close连接且空闲时长超过 minEvictableIdleTimeMillis,则会执true
⾏validationQuery指定的SQL,以保证该程序连接不会池kill掉,其范围不超过minIdle指定的连接
个数
minIdle回收空闲连接时,将保证⾄少有minIdle个连接.与initialSize相同
removeAbandoned要求程序从池中get到连接后, N 秒后必须close,否则druid 会强制回收该连接,不管该连接中是活
动还是空闲, 以防⽌进程不会进⾏close⽽霸占连接。false,当发现程序有未正常close 连接时设置为true
removeAbandonedTimeout设置druid 强制回收连接的时限,当程序从池中get到连接开始算起,超过此应⼤于业务运⾏最长时间
值后,druid将强制回收该连接,单位秒。
logAbandoned当druid强制回收连接后,是否将stack trace 记录到⽇志中true
testWhileIdle当程序请求连接,池在分配连接时,是否先检查该连接是否有效。(⾼效)true
validationQuery检查池中的连接是否仍可⽤的 SQL 语句,drui会连接到数据库执⾏该SQL, 如果
正常返回,则表⽰连接可⽤,否则表⽰连接不可⽤
testOnBorrow程序申请连接时,进⾏连接有效性检查(低效,影响性能)false
testOnReturn程序返还连接时,进⾏连接有效性检查(低效,影响性能)false poolPreparedStatements缓存通过以下两个⽅法发起的SQL:true
public PreparedStatement prepareStatement(String sql)
public PreparedStatement prepareStatement(String sql,
int resultSetType, int resultSetConcurrency) maxPoolPrepareStatementPerConnectionSize每个连接最多缓存多少个SQL20 filters这⾥配置的是插件,常⽤的插件有:stat,wall,slf4j
监控统计: filter:stat
⽇志监控: filter:log4j 或者 slf4j
防御SQL注⼊: filter:wall
connectProperties连接属性。⽐如设置⼀些连接池统计⽅⾯的配置。
Sql=true;druid.stat.slowSqlMillis=5000
⽐如设置⼀些数据库连接属性
了解了每个配置的作⽤,那么我们只需要合理的进⾏配置即可解决这个问题。以下为经过压测验证后的我们服务的配置,供参考:
// 初始连接数
dataSource.setInitialSize(10);
// 最⼩连接池数量
dataSource.setMinIdle(10);
// 最⼤连接池数量
dataSource.setMaxActive(100);
// 配置获取连接等待超时时间毫秒
dataSource.setMaxWait(100);
//缓存通过以下两个⽅法发起的SQL:
dataSource.setPoolPreparedStatements(true);
//每个连接最多缓存多少个SQL
dataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
//检查空闲连接的频率,单位毫秒, ⾮正整数时表⽰不进⾏检查
dataSource.setTimeBetweenEvictionRunsMillis(-1);
//池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,将回收该连接,要⼩于防⽕墙超时设置
dataSource.setMinEvictableIdleTimeMillis(300000);
//当程序请求连接,池在分配连接时,是否先检查该连接是否有效。(⾼效)
dataSource.setTestWhileIdle(true);
// 程序申请连接时,进⾏连接有效性检查(低效,影响性能)
dataSource.setTestOnBorrow(false);
//程序返还连接时,进⾏连接有效性检查(低效,影响性能)
dataSource.setTestOnReturn(false);
// 要求程序从池中get到连接后, N 秒后必须close,否则druid 会强制回收该连接,不管该连接中是活动还是空闲, 以防⽌进程不会进⾏close⽽霸占连接。
dataSource.setRemoveAbandoned(true);
// 设置druid 强制回收连接的时限,当程序从池中get到连接开始算起,超过此值后,druid将强制回收该连接,单位秒。
// 结合业务来看,存在jpa极⼤事务;不好设置暂时为设置两分钟
dataSource.setRemoveAbandonedTimeout(120);
//当druid强制回收连接后,是否将stack trace 记录到⽇志中
dataSource.setLogAbandoned(true);
题外话:
对于此类问题,官⽅给的回复是我们没有正确的关闭连接,导致连接泄漏;
这样通过配置的⽅法虽然解决了问题,但是为什么其他连接池就没有此类问题;也不需要⼈为的来通过配置避免此'BUG'呢?不得⽽知

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。