1.故障简述

8月30日 上午9:30 发现某核心服务开始告警,主动重启,影响线上成交和查看订单。持续影响3分钟

2.故障引入与处理的整个过程

时间

故障处理行动

2023-08-30 09:29:57

收到监控告警某核心 服务自动完成重启

2023-08-30 09:30:33

服务全部启动完成

2023-08-30 09:51:00

排查原因——通过日志看到有数据库关闭

2023-08-30 10:03:00

看到慢sql统计里有昨日上线的sql

2023-08-30 10:20:00

优化sql提交代码打包上线

2023-08-30 10:44:41

上线完成,观察日志已经没发现新产生对应的慢sql

3.故障原因分析

3.1 故障触发

故障根因:由于慢sql增多导致数据库连接池打满,所有dubbo服务涉及数据库操作的都需要等待获取链接,从而把dubbo线程池打满,

服务的健康检查发现dubbo服务不可用,所以发生了重启

3.2 故障排查过程

1.首先发现服务频繁重启,排查原因是因为健康检查没有过,健康接口出错日志是 dubbo线程池满了,目前最大是200

踩坑DruidDataSource导致的服务频繁重启_服务频繁重启

2.同时发现这期间有超过200个慢sql,猜测慢sql阻塞住了请求

3.这期间迅速修复慢sql

对大表进行查询,mysql优化器选择了带函数的检索字段,导致索引失效

4.同时排查慢sql阻塞其余请求的原因,连上服务器,发现服务状态都健康,内存CPU等都很稳定

5.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 java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:2029)
        at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1557)
        at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1337)
        at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1317)
        at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1307)
        at com.alibaba.druid.pool.DruidDataSource.getConnection(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.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:79)
        at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:82)
        at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:68)

我们的数据库连接池是DruidDataSource,配置基本都是默认值,现在最大8个连接,深挖代码发现:

踩坑DruidDataSource导致的服务频繁重启_慢sql_02

踩坑DruidDataSource导致的服务频繁重启_服务频繁重启_03

就算设置了超时等待时间,只能影响等待线程,不能中断被hold住的查询,如果要中断正在执行的查询,可以通过设置这两个参数:

踩坑DruidDataSource导致的服务频繁重启_DruidDataSource_04

那么防止慢sql拖垮整个服务的解决方式也很明了:

1.设置maxWait

2.设置removeAbandoned为true,设置removeAbandonedTimeout(应大于业务运行最长时间)

3.设置合理的最大连接数,默认为8,需根据业务自行调整

4.结论

1.上线之前反复确认sql是否正确命中索引,防止慢sql拖垮服务

2.检查和优化数据库配置:执行超时时间,获取链接超时时间等

5.推荐的配置

初始化大小,最大,最小连接等条件根据实际需要进行配置,以下配置相关参数皆为举例

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <!-- 基本属性 url、user、password -->
        <property name="url" value="${jdbc_url}" />
        <property name="username" value="${jdbc_user}" />
        <property name="password" value="${jdbc_password}" />

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="10" />
        <property name="minIdle" value="6" />
        <property name="maxActive" value="50" />
        <!-- 配置从连接池获取连接等待超时的时间 -->
        <property name="maxWait" value="10000" />

        <!-- 配置间隔多久启动一次DestroyThread,对连接池内的连接才进行一次检测,单位是毫秒。
            检测时:1.如果连接空闲并且超过minIdle以外的连接,如果空闲时间超过minEvictableIdleTimeMillis设置的值则直接物理关闭。2.在minIdle以内的不处理。
        -->
        <property name="timeBetweenEvictionRunsMillis" value="600000" />
        <!-- 配置一个连接在池中最大空闲时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <!-- 设置从连接池获取连接时是否检查连接有效性,true时,每次都检查;false时,不检查 -->
        <property name="testOnBorrow" value="false" />
        <!-- 设置往连接池归还连接时是否检查连接有效性,true时,每次都检查;false时,不检查 -->
        <property name="testOnReturn" value="false" />
        <!-- 设置从连接池获取连接时是否检查连接有效性,true时,如果连接空闲时间超过minEvictableIdleTimeMillis进行检查,否则不检查;false时,不检查 -->
        <property name="testWhileIdle" value="true" />
        <!-- 检验连接是否有效的查询语句。如果数据库Driver支持ping()方法,则优先使用ping()方法进行检查,否则使用validationQuery查询进行检查。(Oracle jdbc Driver目前不支持ping方法) -->
        <property name="validationQuery" value="select 1 from dual" />
        <!-- 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 -->
        <!-- <property name="validationQueryTimeout" value="1" />  -->

        <!-- 打开后,增强timeBetweenEvictionRunsMillis的周期性连接检查,minIdle内的空闲连接,每次检查强制验证连接有效性. 参考:https://github.com/alibaba/druid/wiki/KeepAlive_cn -->
        <property name="keepAlive" value="true" />  

        <!-- 连接泄露检查,打开removeAbandoned功能 , 连接从连接池借出后,长时间不归还,将触发强制回连接。回收周期随timeBetweenEvictionRunsMillis进行,如果连接为从连接池借出状态,并且未执行任何sql,并且从借出时间起已超过removeAbandonedTimeout时间,则强制归还连接到连接池中。 -->
        <property name="removeAbandoned" value="true" /> 
        <!-- 超时时间,秒 -->
        <property name="removeAbandonedTimeout" value="80"/>
        <!-- 关闭abanded连接时输出错误日志,这样出现连接泄露时可以通过错误日志定位忘记关闭连接的位置 -->
        <property name="logAbandoned" value="true" />

        <!-- 根据自身业务及事务大小来设置 -->
        <property name="connectionProperties"
            value="oracle.net.CONNECT_TIMEOUT=2000;oracle.jdbc.ReadTimeout=10000"></property>

        <!-- 打开PSCache,并且指定每个连接上PSCache的大小,Oracle等支持游标的数据库,打开此开关,会以数量级提升性能,具体查阅PSCache相关资料 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="20" />   

        <!-- 配置监控统计拦截的filters -->
        <!-- <property name="filters" value="stat,slf4j" /> -->

        <property name="proxyFilters">
            <list>
                <ref bean="log-filter" />
                <ref bean="stat-filter" />
            </list>
        </property>
        <!-- 配置监控统计日志的输出间隔,单位毫秒,每次输出所有统计数据会重置,酌情开启 -->
        <property name="timeBetweenLogStatsMillis" value="120000" />
    </bean>