📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
文章目录
- 写在前面的话
- 问题背景
- 问题分析
- 问题排查
- 问题结果
- 知识拓展
- 总结陈词
写在前面的话
博主所在公司的产品线,部署上线了多家客户,遇到的线上故障的场景也较多,这边继续更新一下故障复盘
系列,记录并分享一下这些故障的的定位、分析、解决过程。
这里分享的这篇,是由于Druid
连接异常关闭,进而引发大范围故障的问题。
问题背景
某天下午,被领导拉到一个群,要求协助开发排查一个由于前一天死循环的问题,导致积累了几天的未申请医嘱记录,数据量较大。最终导致定时器执行“医嘱自动申请”接口的时候,由于执行时间过长,进而报错,错误如下:
Tips:开局一张图,剩下全靠查。
问题分析
1、该报错的文字版:
Could not commit JDBC transaction; nested exception is java.sql.SQLException: Connection closed.
2、该错误的翻译版:
无法提交JDBC事务,异常原因是连接已关闭
3、继续解释一下:
应该是由于某个事务逻辑执行时间较长,导致提交事务的时候,连接已经被关闭了,无法提交。
4、和开发讨论后得出该医嘱自动申请的逻辑大致如下:
医嘱自动申请接口,在整体逻辑上有特殊性,先是取到一个医嘱列表List,然后遍历处理每条医嘱记录,这里的某条医嘱记录称呼为A,A记录处理的时候又会查出子列表,这里的某个子列表里面的数据称之为B,然后会新开事务去处理子逻辑B,因此导致A的耗时可能较长,又因为B都是新开事务,可能导致A的连接一直空闲,啥事没做。
问题排查
由于手头事情较多,暂无时间拉取实际代码测试。
那先直接从错误查起,既然说连接被关闭,那就查一下数据源等配置。
从项目现场Nacos拷贝了数据库连接池配置,
和连接数量相关的属性,可以看出最大连接是150,获取连接等待超时60秒,但该报错明显不是连接池满,以及获取连接超时。
继续看,和连接关闭的属性相关的,无非:
minEvictableIdleTimeMillis(300秒)
remove-abandoned(开启)
removeAbandonedTimeoutMillis(80秒)
database:
pool:
default:
# 最大连接池数量
max-active: 150
# 从连接池获取连接等待超时的时间
max-wait: 60000
# 最小连接池数量
min-idle: 1
# 配置一个连接在池中最大空闲时间,单位是毫秒
min-evictable-idle-time-millis: 300000
# 连接泄露检查,打开removeAbandoned功能 , 连接从连接池借出后,长时间不归还,将触发强制回连接。回收周期随timeBetweenEvictionRunsMillis进行,如果连接为从连接池借出状态,并且未执行任何sql,并且从借出时间起已超过removeAbandonedTimeout时间,则强制归还连接到连接池中。
remove-abandoned: true
# 回收超时时间
remove-abandoned-timeout-millis: 80000
# 打开后,增强timeBetweenEvictionRunsMillis的周期性连接检查,minIdle内的空闲连接,每次检查强制验证连接有效性. 参考:https://github.com/alibaba/druid/wiki/KeepAlive_cn
keep-alive: true
# 打开PSCache
pool-prepared-statements: true
# 指定每个连接上PSCache的大小,Oracle等支持游标的数据库,打开此开关,会以数量级提升性能,具体查阅PSCache相关资料
max-pool-prepared-statement-per-connection-size: 20
【知识补充】
Druid 部分属性说明:
1、minEvictableIdleTimeMillis:最小空闲时间,默认30分钟,如果连接池中非运行中的连接数大于minIdle,并且那部分连接的非运行时间大于minEvictableIdleTimeMillis,则连接池会将那部分连接设置成Idle状态并关闭;也就是说如果一条连接30分钟都没有使用到,并且这种连接的数量超过了minIdle,则这些连接就会被关闭了。
**2、removeAbandonedTimeoutMillis:**检查连接泄露依据(超时时间),默认5分钟,连接回收的超时时间;设置了removeAbandoned为true,Druid会定期检查线程池溢出的情况,如果不是运行状态,且超过设置的时间就会被回收;
**3、removeAbandoned:**代表是否清理removeAbandonedTimeout秒没有使用的活动连接,清理后并没有放回连接池
4、logAbandoned:连接池收回空闲的活动连接时是否打印消息
Tips:minEvictableIdleTimeMillis 与 removeAbandonedTimeout 这两个参数针对的连接对象不一样,前者针对连接池中的连接对象,后者针对未被close的活动连接,因此针对业务处理逻辑太长,比较重要的还是后者。
【领导继续提问,为啥其他医院量有时候也很大,又正常?】
有什么好说的?那上这家医院的配置?如下所示:
这家医院是旧架构,即SSM框架。可以看到removeAbandonedTimeout配置了900秒,同时removeAbandoned是false,即为不检查。
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${datasource.driver}"/>
<!-- 基本属性 url、user、password --> <property name="url" value="${datasource.url}"/>
<property name="username" value="${datasource.username}"/>
<property name="password" value="${datasource.password}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${datasource.initialSize}"/>
<property name="minIdle" value="${datasource.minIdle}"/>
<property name="maxActive" value="${datasource.maxActive}"/> <!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="5000"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="${jdbc.testSql}"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/> <!-- 长时间不使用的连接强制关闭 -->
<property name="removeAbandoned" value="false"/>
<!-- 配置开始关闭空闲连接的时间,单位是秒 -->
<property name="removeAbandonedTimeout" value="900"/>
<!-- 是否将关闭操作进行日志打印 -->
<property name="logAbandoned" value="false"/>
<!--保证连接池中的连接是真实有效的连接,并且空闲连接数维持为minIdle-->
<property name="keepAlive" value="true"/>
<property name="filters" value="stat,config"/>
<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${publickey}" />
</bean>
问题结果
鉴于医嘱的逻辑可能涉及较长时间,因此,80秒可能不够,建议改大。
removeAbandoned 属性依然建议打开,否则会增加连接池泄露风险。
知识拓展
【错误模拟】
晚上有点时间,就想来模拟一下,口说无凭。
其实本地很好重现这个错误,准备如下代码和配置:
1、事务代码里面执行一段更新语法,阻塞一段时间;
2、设置间隔2秒检查,回收打开,超时3秒;
可以看到报错如下:
【源码相关】
总结陈词
此篇文章介绍了连接异常关闭
的故障复盘,仅供学习参考。
出现问题并不可靠,主要是能从问题中总结出什么东西,这些不断积累的过程才是令人兴奋的。
Tips:青海长云暗雪山,孤城遥望玉门关。黄沙百战穿金甲,不破楼兰终不还。