druid释放空闲连接的问题

一、问题背景


  最近在某个项目的生产环境碰到一个数据库连接问题,使用的连接池是alibaba的druid_1.1.10,问题表现为:DBA监测到应用集群到oracle的连接数总会在半夜降低,并且大大低于每个节点druid配置的minIdle总和。

  一开始怀疑此问题产生的原因是oracle侧主动关闭了连接,但很难去验证这个点,一方面是和DBA沟通起来比较麻烦,另一方面是没有确切的证据,纯粹靠猜想很难服众,所以退而求其次,尝试在druid连接池上去找原因。既然是半夜这种交易量小的时间点降低连接数,那么应该和druid对空闲连接的处理有关。

  在github拉取了druid源码后,载入idea,使用minEvictableIdleTimeMillis进行了全局搜索,在结果列表中找到了一些可能与连接回收有关的类,最终定位到了DruidDataSource的内部类DestoryTask,简单的扫了一眼代码之后,基本就能确定DestroyTask是用于负责检测和销毁空闲连接的类了。

  由于druid源码编译还得花时间研究,我直接搭建了一个简单的springboot工程,引入druid后对DruidDataSource的init()方法打断点,启动应用开始一步步调试...

二、源码分析


  DruidDataSource init时会启动一个销毁连接的线程,由于destoryScheduler为空,因此创建了DestroyConnectionThread线程去执行,如下图:

druid释放空闲连接的问题_第1张图片

   DestroyConnectionThread做的事情很简单,就是每隔固定的时间去执行一下DestoryTask的run方法,执行的间隔时间基于druid配置timeBetweenEvictionRunsMillis的值:

druid释放空闲连接的问题_第2张图片

   DestoryTask的run方法调用shrink方法,该方法是空闲连接检查的核心方法,至于removeAbandoned方法是用于回收借出去但一直未归还的连接(这种连接可能导致连接泄露),它与druid的配置removeAbandoned有关,这里就不细讲了:

druid释放空闲连接的问题_第3张图片

  shrink方法逻辑如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

public void shrink(boolean checkTime, boolean keepAlive) {

    //加锁

    try {

        lock.lockInterruptibly();

    catch (InterruptedException e) {

        return;

    }

    int evictCount = 0//需要剔除的个数

    int keepAliveCount = 0//需要保持会话的个数

    try {

        if (!inited) {

            return;

        }

        //要检查的个数=连接池当前连接个数 - 最小空闲连接数

        final int checkCount = poolingCount - minIdle;

        //检查时间点

        final long currentTimeMillis = System.currentTimeMillis();

        //遍历当前连接池的所有连接

        for (int i = 0; i < poolingCount; ++i) {

            DruidConnectionHolder connection = connections[i];

            //DestroyThread调用shrink时,checkTime=true,keepAlive基于配置的值(默认为false)

            if (checkTime) {

                //phyTimeoutMillis参数(默认值为-1)设定了一条物理连接的存活时间,

                //不同的数据库对一个连接有最大的维持时间,比如mysql是8小时,设置该

                //参数是为了防止应用获取某连接时,该连接在数据库侧已关闭而导致异常。

                if (phyTimeoutMillis > 0) {

                    //如果某条连接已超过phyTimeoutMillis,则将其放入需要剔除的连接数组evictConnections中

                    long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;

                    if (phyConnectTimeMillis > phyTimeoutMillis) {

                        evictConnections[evictCount++] = connection;

                        continue;

                    }

                }

                //获取连接空闲时间

                long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                 

                //如果某条连接空闲时间小于minEvictableIdleTimeMillis,则不用继续检查剩下的连接了

                if (idleMillis < minEvictableIdleTimeMillis) {

                    break;

                }

                //判断此连接的状态,将其放入不同处理的连接数组中

                if (checkTime && i < checkCount) {

                    //这里checkTime有点多余,一定为true,因为它是if(checkTime)分支中的逻辑

                    //如果此连接仍在checkCount范围之内,即它是一个多出最小空闲连接数的连接,

                    //那么就将它加入到需要剔除的连接数组evictConnections中

                    evictConnections[evictCount++] = connection;

                else if (idleMillis > maxEvictableIdleTimeMillis) {

                    //如果连接空闲时间已经大于maxEvictableIdleTimeMillis,也将它加入到需要

                    //剔除的连接数组evictConnections中

                    evictConnections[evictCount++] = connection;

                else if (keepAlive) {

                    //如果连接超过checkCount范围,并且空闲时间小于maxEvictableIdleTimeMillis,

                    //并且开启了keepAlive,那么就将它加入到需要维持的连接数组keepAliveConnections中

                    keepAliveConnections[keepAliveCount++] = connection;

                }

            else {

                //对于不需要checkTime的情形,就非常简单了,将比minIdle连接数多的连接放入

                //需要剔除的连接数组evictConnections中

                if (i < checkCount) {

                    evictConnections[evictCount++] = connection;

                else {

                    break;

                }

            }

        }

        //剔除连接和需要维持的连接都作为被移出连接,然后对连接池中的connections元素进行移动,

        //使得有用的连接重新放在连接数组connections的头部,并将其余元素置为null

        int removeCount = evictCount + keepAliveCount;

        if (removeCount > 0) {

            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);

            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);

            poolingCount -= removeCount;

        }

        keepAliveCheckCount += keepAliveCount;

    finally {

        lock.unlock();

    }

    //处理需要剔除的连接数组evictConnections,对其中的连接进行关闭,

    //并维护监控指标:destroyCountUpdater,然后将evictConnections清空

    if (evictCount > 0) {

        for (int i = 0; i < evictCount; ++i) {

            DruidConnectionHolder item = evictConnections[i];

            Connection connection = item.getConnection();

            JdbcUtils.close(connection);

            destroyCountUpdater.incrementAndGet(this);

        }

        Arrays.fill(evictConnections, null);

    }

    //处理需要维持连接的连接数组keepAliveConnections

    if (keepAliveCount > 0) {

        //维护监控指标

        this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);

        for (int i = keepAliveCount - 1; i >= 0; --i) {

            DruidConnectionHolder holer = keepAliveConnections[i];

            Connection connection = holer.getConnection();

            //更新连接的keepAlive检查计数器

            holer.incrementKeepAliveCheckCount();

            boolean validate = false;

            try {

                //使用配置的validationQuery Sql检查当前连接是否有效,validateConnection

                //方法非常简单,如果检查过程中抛出异常都会被此处catch住并处理

                this.validateConnection(connection);

                validate = true;

            catch (Throwable error) {

                if (LOG.isDebugEnabled()) {

                    LOG.debug("keepAliveErr", error);

                }

                // skip

            }

             

            if (validate) {

                //如果连接有效性检查成功,则更新连接的最近活跃时间,并尝试将连接放回连接池,

                //put(holder)不一定保证放回成功,在连接池已满的情况下将不会放入,方法中通过

                //使用条件变量以及poolingPeak等机制保证了连接不会被泄露

                holer.lastActiveTimeMillis = System.currentTimeMillis();

                put(holer);

            else {

                //如果连接有效性检查失败,则关闭此连接

                JdbcUtils.close(connection);

            }

        }

        //清空连接数组keepAliveConnections

        Arrays.fill(keepAliveConnections, null);

    }

}

三、验证结论


  根据调试过程中的源码分析,可知druid_1.1.10判断连接是否销毁还是保活的逻辑如下(只讨论checkTime为true的情况):

druid释放空闲连接的问题_第4张图片

  到这里,我们就可以下一个结论了:druid对于空闲连接还是有可能回收的,只要它未开启keepAlive并且闲置时间过长就会回收空闲连接,从而使得连接池中的连接数小于配置的minIdle值。

  为了验证结论,我开启了druid monitor的web页面访问,然后在如下的页面中去观察池中连接的情况:

druid释放空闲连接的问题_第5张图片

  与druid空闲连接回收的相关参数配置如下图:

  首先不开启keepAlive功能(druid也是默认关闭的),在应用启动的时候,从druid monitor中观察到连接池中的连接数如下:

druid释放空闲连接的问题_第6张图片

  等待大约2~3分钟之后(再此期间不要发起任何数据库请求),再次观察连接池中的连接数,可以发现连接数为0:

druid释放空闲连接的问题_第7张图片

  接着配置"spring.datasource.druid.keep-alive=true"以打开keepAlive,重启应用并重复上述过程,结果如下:

druid释放空闲连接的问题_第8张图片

  可以发现keepAlive起作用了,池中连接数维持在20,结论得到验证。接着回过头去查看了一下maxEvictableIdleTimeMillis这个参数的默认值为25200000,刚好7个小时,差不多能和DBA监测到的连接降低时间对上。

四、其他发现


  在解决问题的过程中,参考了官方文档以及他人在druid项目中提的issue,经历了怀疑问题、确认问题、解决问题三个阶段,不过个人在调试过程中仍然发现有如下问题:

(1)官方的配置文档中对属性minEvictableIdleTimeMillis做了如下描述:

druid释放空闲连接的问题_第9张图片

  然而实际上代码体现出来的逻辑并不是这么一回事,maxEvictableIdleTimeMillis更像起到了决定性的作用。

(2)timeBetweenEvictionRunsMillis、minEvictableIdleTimeMillis、maxEvictableIdleTimeMillis这三者设置的大小如果满足一定条件,也会导致keepAlive失效。根据源码,如果在某一轮扫描中(间隔时间timeBetweenEvictionRunsMillis),检测到连接的空闲时间小于minEvictableIdleTimeMillis,那么这些连接不需要keepAlive,自然也不会更新lastActiveTimeMillis,这里存在一个临界条件,使得连接空闲时间同时大于minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis,这个临界条件触发的前提是:

1

2

3

//1.满足下面不等式

maxEvictableIdleTimeMillis - minEvictableIdleTimeMillis <= timeBetweenEvictionRunsMillis

//2.连接一直处于未使用状态,那么在空闲时间小于minEvictableIdleTimeMillis之前,连接的lastActiveTimeMillis都不会被更新

  下面是我的一个测试,druid相关配置情况如图:

  启用应用并静静等待1~2分钟,通过druid monitor查看连接池状态:

druid释放空闲连接的问题_第10张图片

  通过浏览器调用一个http查询接口,连接池连接数恢复:

druid释放空闲连接的问题_第11张图片

  静静等待1~2分钟,可以看到连接池中的连接又被清空:

druid释放空闲连接的问题_第12张图片

  结论:虽然maxEvictableIdleTimeMillis这个参数我们一般不配置,它的默认值也比较大(7小时),但是实际在配置druid时,还是建议考虑keepAlive失效的因素,作为配置的一个考量。

五、参考资料


  • issue链接:Druid数据源 , 池中连接数poolingCount 小于 最小空闲连接数 minIdle · Issue #2323 · alibaba/druid · GitHub
  • 配置文档链接:DruidDataSource配置 · alibaba/druid Wiki · GitHub

你可能感兴趣的:(Druid,数据库,spring,linux,docker)