一段时间以来,XXX 部门开放平台 OPENXXX 系统在业务高峰频繁出现 MySQL 线程数升高的现象。升高自己不是问题,问题是随着业务高峰过去,QPS 下来后 ,MySQL 线程数却依然居高不下,这是什么缘由?java
思考方向上,你们都知道,MySQL 是经过线程池来进行线程管理的,基于过往经验,上述状况极可能是线程池的配置策略不合理致使线程建立后没法及时释放,而实际上线程的利用率是很低的————这一点经过分析系统线程也能够看到,waiting 态线程占据 MySQL 总线程数的一半有余(见下图)。mysql
落地实践上,方向虽然是明确的,但具体是 MySQL 的哪一项策略配置不合理、又该作怎样的调整,须要作细致的调研分析才能回答。由此发起 MySQL 线程的优化治理专项。sql
对比业务高峰先后的 MySQL 线程,发现飙升的主要是 [MySQL Statement Cancellation Timer] ,由此引出第一阶段问题,[MySQL Statement Cancellation Timer] 线程是从哪里来的?数据库
走读代码流程,梳理获得 Timer 线程的生命周期,以下图所示(Timer 节点以及问题节点已标识)——网络
dump 现场线程,配合线程 stack 走读 mysql connector jar 的代码。session
一、定位代码,java.util.TimerThread#run—— TimerThread 是 mysql-connector-java-xxx.jar 中的 Timer 的一个内部类,等待 Timer 队列中的任务以执行、调度mybatis
二、顺藤摸瓜,能够追到 [MySQL Statement Cancellation Timer] 线程的生成链路app
com.mysql.jdbc.ConnectionImpl#getCancelTimer异步
三、查看 getCancelTimer 的上游调用 ,主要是 mysql-connector-java-xxx.jar 中的主管 sql 查询的 Statementasync
com.mysql.jdbc.StatementImpl#executeQuery
小结
走读 [MySQL Statement Cancellation Timer] 线程的调用链逻辑,能够抽象 3 点核心信息——
能够推断 OPENXXX 应用一定开启了 queryTimeout。查看 mybatis-config.xml,肯定在每次 DB 查询的时候,均插上了 queryTimeout——defaultStatementTimeout 设置对全局 sql 生效,包括 insert、select、update
jdk 规范保证,任何线程都有自身的退出机制。查看 Statement 中 cancelTask 的执行过程,依次追溯。
一、com.mysql.jdbc.StatementImpl.CancelTask#run——调用 Connection 进行 cancel
二、com.mysql.jdbc.ConnectionImpl#close
三、com.mysql.jdbc.ConnectionImpl#realClose——关闭 Timer 线程
小结
至此获取到 [MySQL Statement Cancellation Timer] 线程的 cancel 链路,走读代码逻辑,抽象核心信息——链接关闭时,会调用 Connection.close 方法 cancel 掉 Timer 线程,即 [MySQL Statement Cancellation Timer] 线程。
链接建立时,queryTimeout 会使 jdbc driver 新建 cancelTask 并使用 Timer 进行调度,一旦 sql 查询超时则执行 cancel 动做;链接关闭时,调用 Connection 以 cancel 掉 Timer 线程。
问题来到第二个阶段:既然链接超时关闭的时候,才会将 Timer 线程 cancel 掉,那么控制超时的具体是哪些策略呢?
对于选型关系型数据库的应用而言,数据库的链接关闭策略自上而下由两层组成:一、JDBC;二、Mysql,经由各层的一系列超时参数进行控制。须要注意的是,网络文档对各层各参数的释义大多不够精准,甚至相互矛盾。如下参数分析均来自官方文档,并随载官方连接以便详细查阅。
链接超时参数
有效性检测参数
解读一下
关于参数的 Q&A
设置完链接超时参数 maxIdleTime 以后,有必要设置有效性检测参数么——二者的关系是:链接空闲超过 maxIdleTime 后,就会被 mysql server 断开。但此时链接池并无回收这个链接,直到链接池检测到该链接已被废弃后,才会进行回收。在这个时间段内,若是客户端使用了这个链接,就会报错:Communications link failure。
wait_timeout:mysql server 关闭链接以前,容许链接闲置多少秒。默认是 28800,单位秒,即 8 个小时
既然 jdbc 层面以及 mysql 层面都有完备的链接关闭策略,那么问题来到第三个阶段:OPENXXX 系统自身的配置策略是怎样的?
依据上文调研的链接关闭策略,摸查 OPENXXX 应用,一、JDBC;二、Mysql。
OPENXXX 在 jdbc 层面未配置链接关闭策略(无 maxIdleTime),如此一来,只能依赖下层 mysql 的 timeout 机制进行链接的关闭。但实际上,mysql server 可否关掉链接呢?
一、查询 mysql server 的 wait_timeout 参数,观察 DB 设定的链接超时配置——[select variable_name,variable_value from information_schema.session_variables where variable_name like 'wait_timeout']
二、查询 mysql server 的 Threads_connected 参数,观察 DB 当前打开的链接数——[show status where variable_name
= 'Threads_connected']
三、查询 DB 平常的 qps
四、汇总信息:connectionSize~800,qps~800,keepAliveTime~28800s。由此计算线程释放的几率:(qps keepAliveTime) / connectionSize,即 80028800/800=28800——意味着每一个链接在关闭以前,有 28800 次机会拿到任务而不被终止。这种几率下,链接是不可能释放的,链接空置率也会很高。
经过配置 jdbc 层的链接关闭策略,及时关掉空闲链接,从而确保 timer 线程的 cancle。问题来到第四个阶段:如何配置 OPENXXX 的链接关闭策略?
实际上,官方已经给出了建议:
The most reliable time to test Connections is on check-out. But this is also the most costly choice from a client-performance perspective. Most applications should work quite reliably using a combination of idleConnectionTestPeriod and testConnectionOnCheckin. Both the idle test and the check-in test are performed asynchronously, which can lead to better performance, both perceived and actual.
最可靠的链接测试时机是在 connection 回收时进行(testConnectionOnCheckout),但从系统性能的角度来看,这也是最耗费性能的选择。大多数应用程序应该组合使用 idleConnectionTestPeriod 和 testconConnectionCheckin,一方面能够保证系统很是可靠地运行,另外一方面空闲测试和提交测试都是异步执行的,这会带来更好的系统性能。
Set idleConnectionTestPeriod to 30, fire up you application and observe. This is a pretty robust setting, all Connections will tested on check-in and every 30 seconds thereafter while in the pool. Your application should experience broken or stale Connections only very rarely, and the pool should recover from a database shutdown and restart quickly
将 idleConnectionTestPeriod 设置为 30,启动系统并观察。这是一个很是健壮的设置,全部链接都将在提交时进行测试,以后每隔 30 秒在池中进行一次测试。这样应用程序能够不多拿到断开或过期的链接,而且能够在 DB 重启以后支持链接的快速恢复。
<property name="preferredTestQuery">SELECT 1</property> <!-- 有效性检测语句 -->
<property name="testConnectionOnCheckin">true</property> <!-- 提交链接时校验链接的有效性。Default: false -->=
<property name="idleConnectionTestPeriod">30</property> <!-- 每 30 秒检查链接池中的空闲链接。若为 0 则永不检测。Default: 0 -->
<property name="maxIdleTime">30</property> <!-- 最大空闲时间,30 秒内未使用链接被丢弃。若为 0 则永不丢弃。Default: 0 -->
test 环境进行测试,验证配置策略的有效性,三步走:
一、高 qps,mock db 流量,重复发起 query 请求——观察 cat 堆栈,是否生成大量的 [MySQL Statement Cancellation Timer]
二、低 qps,mock db 流量,间隔发起 query 请求——观察 cat 堆栈,是否开始缩减 [MySQL Statement Cancellation Timer]
三、无 qps,关闭 db 流量——观察 cat 堆栈,无 [MySQL Statement Cancellation Timer]
@Controller @RequestMapping("/dbtimer") public class DBTimerController { @Resource private PushCallbackService callbackService; @Autowired private MccClient mccClient; private static final Logger LOGGER = LoggerFactory.getLogger(DBTimerController.class); private static final ExecutorService executorService = Executors.newFixedThreadPool(50);//建立线程池 @ResponseBody @RequestMapping(value = "/dbtest", method = RequestMethod.GET) @Ignore("工具接口,无需鉴权") public void dbTest() { TimerQ timerQ = new TimerQ(); for (int i = 0; i < 50; i++) { executorService.execute(new TimerR(timerQ)); } } @ResponseBody @RequestMapping(value = "/dbtestdown", method = RequestMethod.GET) @Ignore("工具接口,无需鉴权") public void dbTestDown() { executorService.shutdown(); } class TimerQ { public void queryTimer() throws InterruptedException { int i = 1; while (Boolean.valueOf(mccClient.getValue("mcc_timer_query_switch"))) { CallbackLog callbackLog = callbackService.querybyid(i); if (callbackLog==null) { continue; } LOGGER.warn("query timer, callbackInfo:{}", callbackLog.getId()); ++i; if (Boolean.valueOf(mccClient.getValue("mcc_timer_sleep_switch"))) { Thread.sleep(Long.valueOf(mccClient.getValue("mcc_timer_time_switch"))); } } } } class TimerR implements Runnable { private TimerQ timerQ; public TimerR(TimerQ timerQ) { this.timerQ = timerQ; } @Override public void run() { try { timerQ.queryTimer(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
一、初始阶段
MySQL Statement Cancellation Timer 线程数为 0
二、高 qps——生成大量的 [MySQL Statement Cancellation Timer]
MySQL Statement Cancellation Timer 线程数为 50
三、低 qps——[MySQL Statement Cancellation Timer] 开始缩减
MySQL Statement Cancellation Timer 线程数为 35
四、无 qps——无 [MySQL Statement Cancellation Timer]
MySQL Statement Cancellation Timer 线程数为 0
详见 OPENXXX 系统 DB 异常线程优化案——总结报告。一句话总结:DB 链接超时策略的引入,能够及时有效的关闭链接,进而关闭 [MySQL Statement Cancellation Timer],使得 OPENXXX 系统线程表现出了良好的业务弹性,且未损失原有的 sql 性能。
本次问题的表象是明确的,但掩藏的内核是艰深的。历经「三方包代码(原理)— jdbc(c3p0 文档)— mysql server(manual 文档) — openXXX(分析) — 测试(验证)」,我的尽力呈现本次优化实践从调研到上线的完整过程,亦收获良多。同时在这里抽象、提炼一下,主要是我的对于 DB 线程调优的提纲式整理,方便各位同窗进行参考,寻找优化思路——