教程集 www.jiaochengji.com
教程集 >  数据库  >  mysql  >  正文 mysql driver的bug的深入分析

mysql driver的bug的深入分析

发布时间:2017-02-04   编辑:jiaochengji.com
教程集为您提供mysql driver的bug的深入分析等资源,欢迎您收藏本站,我们将为您提供最新的mysql driver的bug的深入分析资源
mysql driver指的是数据库连接驱动了,我们下面来为各位深入分析mysql driver的bug了,此文章非常的专题我们一起来看看吧。

1 问题描述:

下面是应用上线后的线程个数的曲线图:

 

<center></center>

通过观察线程数:从0.2k增加到1.4k,然后突然又变成0.2k。 所有增加的线程均为Damon的线程。

有如下几个疑问:

   1:为什么线程数会一直增加,出现了线程泄露?

   2:在某个时间点线程数突然降了1.2k,why?

1.1 线程泄露排查
   通过jstack打印栈信息,发现大部分的线程描述如下。(省略了部分栈信息)


"MySQL
 Statement Cancellation Timer"
#2952daemonin
 Object.wait()

java.lang.Thread.State: TIMED_WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

  at
 java.util.TimerThread.run(Timer.java:505)
 
通过线程名字可知该线程为mysql的线程(这个也说明了自定义线程的时候,起一个容易识别的名字是多么重要)。

从信息描述可知是JDK的一个Timer线程,并且该线程处于TIMED_WAITING的状态。

疑问:

    Statement Cancellation Timer 线程是干嘛的,为什么是一个Timer的线程?

1.2 线程数突降排查

首先去观察当前系统的各种资源消耗(比如 cpu,内存,jvm信息),看能否从中得到一些信息,通过查看GC时发现,YGC的时间点和线程数突降的时间点完全一致。如下图(GC的曲线图)

疑问:

    难道YGC会把TIMED_WAITING状态的线程给GC掉? 非常挑战之前对GC的理解。


2 基础知识
2.1 数据库连接池的心跳检测
 1:数据连接池是对连接进行管理和维护,保证连接的可用性。为了保证连接的可用性 ,需要对连接进行心跳。

 2:心跳一般分为两个层面:tcp层面的心跳,应用层面的心跳。 tcp层面是使用keepalive进行心跳,而应用层面是通过应用层协议进行心跳。

 3:有同学会问:TCP有keepalive的心跳机制,为什么还需要上层应用的心跳。(这里笔者推荐使用应用层的心跳来进行保活)

keepalive的局限:

    保证TCP层面的保活,如果应用挂掉,但是端口还正常,则连接还是有效。

    对于IO应用,如果TCP层保活失败,会关闭掉socket,但是上层应用感知不到,会认为socket还是有效的。

    操控性比较弱。

应用层保活优点:

     灵活,可操控性强。

     可以针对业务特性设置心跳策略。

     可以保证连接是有效的。

 4:数据库连接池采用的是应用层心跳来进行保活,主要有如下三种心跳方式。

 Statement

     数据库的查询方式进行心跳。连接池会提供心跳检查的sql配置,比如配置为:select 1 。那么心跳的时候,就会执行statement.execute("select 1") 。

 Ping

      mysql driver提供了一个ping的方法。mysql协议提供了一个ping的命令(类似于select命令),专门用于心跳检测。

 isValid

       jdbc提供了isValid接口,mysql driver对该接口实现,是采用mysql原生的ping命令。

 对比

  statement ping接口 isValid接口
参数 select 1 无参 有timeout参数
性能 低 高 高
接口提供 JDBC原生 mysql driver私有 JDBC4原生
返回值 boolean void boolean
心跳失败 抛SQLException 抛SQLException 返回false
总结:

   注意:ping协议是指mysql的协议, ping接口是driver提供的接口内部用来实现ping协议的发送

    1:mysql原生ping协议的性能是statement的将近2倍(笔者进行过测试),推荐使用原生的ping协议

    2:ping接口并不是JDBC接口,内部实现了发送ping协议的功能。如果出错,会将错误异常抛出

    3:isValid的内部实现也是ping方法,只是进行了try catch的封装。如果try通过,则返回true,在catch处返回false,同时会将当前Connection的状态置为false(该处后面会重点讲解)。

2.2 业务超时处理方案
一般访问第三方接口或者可能执行会比较长的接口,一般都会设置一个超时时间,进行容错的处理。

比如:

    1:访问http接口,设置超时时间,在规定的时间未返回数据,则会报timeout异常。

    2:执行数据库命令,设置超时(调用statement的setQueryTimeout接口),在规定的时间未返回数据,则会报timeout异常。

2.2.1 实现超时控制流程:
    1:访问接口前,将当前状态封装为一个定时任务,通过定时框架设定任务的启动时间。

    2:收到返回数据后,则会取掉定时任务。

    3:如果在规定的时间内,数据还未返回,定时任务便会启动。处理策略:(1)业务处理 (2)抛出timeout的异常。

    4:业务处理一般为:记录日志,发送取消命令到服务端(mysql driver便是发送kill query connectionid到数据库端取消执行的sql)。

2.2.2 定时框架 
定时框架JDK实现的主要有两种可选择:Timer和ScheduledExecutorService

(1) Timer 

主要有两个方法:schedule和scheduleAtFixedRate,其中参数均为:(TimerTask task, long delay, long period) 。

区别: 比如period为5s,schedule是当前任务执行完成后再过5s开始执行下一次。scheduleAtFixedRate为任务间隔为固定的5s,包含任务执行的时间。

主要包含三个组件:

    TimerThread:用于运行task的线程

    TaskQueue:存放所有的task,该队列会保证存放的task顺序是按照执行时间先后存放

    TimerTask:运行的task,是一个Runnable,主要控制任务的状态和执行的时间点。

 

<center></center>


上图仅大概描述Timer的结构,逻辑并不是很严谨。

  1:调用schedule方法进行任务的调度。将任务添加到queue中,queue会对队列进行重排,保证最先执行的任务在队列的最前面。

  2:对task进行cancel时,仅仅是设置task的状态为cancel,在thread线程读取queue的时候,如果发现task状态为cancel时,则从queue中移除掉。(很多取消操作均是这样设计)

  3:timerThread异步读取queue(任务是按照执行的先后进行顺序存放),如果queue为empty,则wait。否则执行任务。

  4:调用Timer的cancel,则会设置一个标记位,TimerThread执行完成(即TimerThread线程的退出)。如果执行任务的时候出现异常,则TimerThread线程也会进行退出(使用Timer一定要注意任务的异常处理)

  5:对于?处的信息,后面会详细描述。

 

 (2) ScheduledExecutorService

ScheduledExecutorService 具体原理本文将不做描述(其实定时实现的原理和Timer差不多)。

下面主要描述一下两者的区别:

   1:线程模型:Timer是单线程,一个线程轮询执行所有的任务。 ScheduledExecutorService是多线程,其实本质是一个Executor。

   2:异常检查:Timer未对运行的任务进行异常检查,如果任务出现异常,Timer线程会运行结束。ScheduledExecutorService会对异常捕获处理

   如果需要设置定时任务,笔者这里还是推荐ScheduledExecutorService 。

  

 2.3 Caelus连接池
唯品会平台架构部自研的分布式数据库连接池。主要解决如下问题:

    1:高性能:基于无锁的连接池设计模型来提升连接池性能;

    2:在分库较多的场景下,减少线程数。 假如有128个分库,现有连接池模型下则需要使用128个独立的连接池,每个连接池都需要线程(1-4个,不同的连接池不同)处理任务。则总共需要维护128到128*4个线程,开销巨大。而Caelus连接池会大大减少线程数。

    3:连接复用。 对于 一个mysql 的instance上面有多个schema场景下。现有连接池不同的schema的连接不可复用。而Caelus可以复用不同schema的连接,提升性能。

    4:过多的事务指令。如果是事务语句,则从连接池拿到连接后,需要先开启事务(set autocommit=0),归还时需要再设置(set autocommit=1)。每使用一次连接,均需要额外执行两条事务指令。Caelus能有效减少事务指令。

    具体Caelus细节在这里先不做描述。

3 线程泄露解析
    下面将解析线程不断增加的原因。

3.1 问题描述
     1:使用JDBC接口时,如果设置queryTimeout,则driver会启动一个Timer线程,来进行超时控制。Timer线程便是前面通过jstack抓取到一直在增加的线程:MySQL Statement Cancellation Timer 。

     2:可以推测应用端肯定设置了queryTimeout。 通过查看用户配置,确定了在每个查询的时候,均设置了queryTimeout。

     3:Timer线程是连接维度的。每新建一个连接,如果进行了超时控制,则会新建一个线程;在连接物理关闭时,再将该线程给close掉。(一般系统连接超时控制均是全局的设置,只需要开启一个Timer线程运行超时任务)。mysql driver是每一个连接开启一个Timer线程,效率较低,存在频繁的开启和关闭线程的操作。如果连接数过多,也会带来线程数过多的问题。 此处可以考虑进行优化。

     4:连接物理关闭的方式为:调用Connection.close方法,该方法会先判断当前Connection不是close状态,不是close状态,才会cancel掉Timer线程。

     既然连接关闭的时候,会将Timer线程给cancel掉,那为什么Timer线程数一直在增长,并且所有的线程都处于等待状态。下面先分析一下那些操作会对连接进行关闭。

3.2 连接关闭的场景
    1:连接池一般会设置minIdle,空闲连接超过minIdle的话,则会关闭连接。(关于连接池的配置可参考:http://blog.csdn.net/hetaohappy/article/details/51861015)

    2:连接池设置minEvictableIdleTimeMillis,即如果在规定的时间内一直空闲,则会关闭连接。

    3:如果访问发现是IO异常,则会关闭掉连接。mysql会设定wait_timeout,也就是mysql对连接的空闲超时时间。如果空闲超时,mysql则会关闭掉连接。 由于访问端是io机制,无法感知socket关闭,所以当拿到该socket进行访问时,便会报IO的异常。

   前面两个场景并未复现bug;第三个场景可以复现bug。 复现的流程为;

    1:新建连接,设置queryTimeout,然后访问数据库。

    2:在数据库端把对应的连接给kill掉(类似空闲超时的处理)

    3: 客户端使用该连接再进行访问。发现多了一个Timer的线程。

3.3 原因分析
    执行业务sql之前,会先进行心跳检查。心跳采用的是isValid接口(具体可参考前面描述) ,isValid方法先进行心跳,发现心跳失败,会将Connection的连接状态置为close。

    连接池发现心跳失败,则会调用Connection的close接口,由于之前已经将Connection状态设置为close 。close接口发现连接状态已经是close,则直接返回。导致没有cancel 掉Timer线程。

现有连接池排查:

连接池 是否有线程泄露
DBCP 有
Druid 无
HicariCP 有
Caelus 有
 有线程泄露的连接池,均是使用了isValid来进行心跳检查。而Druid是采用的mysql driver的ping方法,所以没有问题。这个可以更改为和Druid一样的机制(ping方法)进行心跳检查。

4 线程数突降解析
    在YGC的时候,线程数突然下降很多。理论上GC和线程是没有关系,应该是YGC触发了什么条件,然后该条件将线程给关闭了。

4.1 Timer线程关闭场景
     1:调用Timer的cancel方法;

     2:任务运行异常(Timer未对异常做封装处理)。

   通过排查确定YGC和上述两个场景没有关闭。

4.2 原因分析
    先分析一下GC和应用主要有哪些交互的场景:

        1:weak引用:GC后,会将weak引用进行回收掉;

        2:finalize:GC的时候会调用需要被GC对象的finalize 方法。

   通过分析Timer源码,未发现有weak引用,却发现Timer有一个 threadReaper 属性实现了finalize 方法,该方法便是操作让Timer线程运行结束(也就是关闭)。这里也就清楚为什么YGC的时候,线程数会跟着下降。(可见JDK源码对各种异常情况考虑的确实很多,研究JDK源码的设计思想,能给我们写代码带来很大的借鉴)

    YGC导致线程关闭的流程:

        YGC-->回收Connection对象->回收Timer对象-->回收threadReaper 对象->执行threadReaper的finalize 方法->设定线程结束状态,并进行notify->Timer线程运行结束。


5 总结

问题出现在两个地方:

   1:mysql driver的isValid方法存在bug,导致Timer线程未关闭。

   2:连接池选择了isValid方法进行心跳处理,可以考虑使用mysql driver的ping方法。

   该问题也是可以通过配置进行避免的。

您可能感兴趣的文章:
mysql driver的bug的深入分析
mysql索引的建立原则
select语句实现mysql分页查询
mysql查看索引方法
JDBC连接MySQL数据库
mysql索引管理 mysql创建索引
mysql和mysqli的区别分析
sqlserver,oracle,mysql数据库的直连的参数
php高级程序员该学什么
Mysql 自增字段设定基值的sql语句

[关闭]
~ ~