[alibaba/druid]连接池驱逐线程逻辑bug,可能会导致连接泄露,DestroyTask#shrink(boolean checkTime, boolean keepAlive)

2025-11-10 525 views
5

若checkTime=true: 顺序遍历连接时,有的连接有可能既不会放入evictConnections数组也不会放入keepAliveConnections数组;

但是完成遍历逻辑后,会将connections数组中遍历到的位点后面的连接对象前移,从而覆盖前面那些连接,然后对evictConnections数组和keepAliveConnections数组中的连接进行处理,但是未放入这两个数组的连接既不在connections数组中了也未进行处理,就会造成连接泄露,麻烦大佬看看~

回答

2

我理解shrink方法的System.arraycopy前移操作是否正确有两个前提:

连接建立时间(connectTimeMillis)旧的排在前面,当phyTimeoutMillis配置参数大于0时关闭超时连接; connections数组里的连接空闲时间(idleMillis=currentTimeMillis - connection.lastActiveTimeMillis)长的排在前面,用于关闭或保活处理。

因为phyTimeoutMillis这个配置用的应该不多,暂时不说它,只说第2个条件是否有问题: 为了简化描述,假设keepAlive配置为true,配置的keepAliveBetweenTimeMillis小于maxEvictableIdleTimeMillis。

connections数组在druid初始化时放入,因为先放入的连接的lastActiveTimeMillis小,所以数组里面的各个连接的idleMillis从大到小排列,符合条件2。 假设直到keepAliveBetweenTimeMillis超时前,这些连接均未使用,全部加入了keepAliveConnections数组,加入的顺序是空闲时间长的连接先加入; connections数组被System.arraycopy前移操作清空,poolingCount清零; 从keepAliveConnections数组尾部向前逆序检测连接状态,检测正常的再加入connections数组尾部,poolingCount++; 假设所有连接全部检测正常,检测完毕后connections数组的内容刚好和初始状态的顺序刚好反过来,因为检测并不更新lastActiveTimeMillis,所以idleMillis的顺序变成了从小到大排序,条件2不再成立。

connections里的这个System.arraycopy前移操作是不靠谱。

1

issue #4316 也有详细的描述,虽然后来的PR在put方法加了个判重处理,避免了 #4316 所说的重复添加Connection2问题,但还是还解决不了 #4316 的另一个问题,就是Connection1被搞丢了,因为connections数组空闲时间无法保证递减,当Connection1在Connection2前面时,因为minIdle=2,Connection1虽然超过minEvictableIdleTimeMillis但不会被清理,而Connection2已经超了keepAliveBetweenTimeMillis要做keepAlive探活,这样evictCount=0, keepAliveCount=1, 然后System.arraycopy前移1位直接把Connections2挪到了Connection1的位置,Connection1被直接搞丢了。 综上所述,我认为connections在复杂场景下需要排序才能维持空闲时间递减,但是connections数组还需要phyConnectTimeMillis递减,以支持onFatalError和phyTimeoutMillis这些处理,phyConnectTimeMillis和idleMillis能否同时递减?我深表怀疑。 shrink的System.arraycopy前移操作改为集合操作 #4726 应该可以根除这个问题。

4

connections里的连接保证不了phyConnectTimeMillis和idleMillis同时递减。 推理过程如下:

connections里依次有两个连接connection1和2,其中connection1的phyConnectTimeMillis是2s,idleMillis是1s,connection2的phyConnectTimeMillis是1s,idleMillis是0s 此时从连接池取出connection1执行sql,connections数组只剩下了connection2 1s后执行完毕归还connection1,connections数组的内容变成了connection2和connection1,此时connection1的idleMillis是0s,比connection2的小,但是它的phyConnectTimeMillis还是比connection2的大。 假设phyTimeoutMillis超时为3s,那么shrink方法就会将connection1关闭,诡异的是arraycopy前移操作会将connection1复制到connection2,connection数组变成了只有要被关掉的connection1,不仅会导致connection2连接泄露,还会导致从连接池获取到已经被关闭的connection1出现already closed的问题。
0

除了checkTime为true的那段逻辑外,在连接发生异常的时候,下边这段逻辑也会有问题

if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis))  {
                    keepAliveConnections[keepAliveCount++] = connection;
                    continue;
}

connections中连接的connectTimeMills无法保证是单调递增的,所以同样会出现connections中索引靠前的连接没有被添加到keepaliveConnections,而索引靠后的却被添加了,调用System.copyArray时,将数组后面的元素拷贝到前面,刚好keepAliveConnections数组中有已经放入connections数组的连接,不但会导致后续拿到相同的连接,还可能导致个别连接泄露

6

除了checkTime为true的那段逻辑外,在连接发生异常的时候,下边这段逻辑也会有问题

if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis))  {
                    keepAliveConnections[keepAliveCount++] = connection;
                    continue;
}

connections中连接的connectTimeMills无法保证是单调递增的,所以同样会出现connections中索引靠前的连接没有被添加到keepaliveConnections,而索引靠后的却被添加了,调用System.copyArray时,将数组后面的元素拷贝到前面,刚好keepAliveConnections数组中有已经放入connections数组的连接,不但会导致后续拿到相同的连接,还可能导致个别连接泄露

https://github.com/alibaba/druid/pull/4726 应该能解决此类问题。

6

这个是从什么版本开始的问题呢?最近从1.1.4升级到1.2.8后,发现了疑似内连接泄露这个问题。

6

我理解shrink方法的System.arraycopy前移操作是否正确有两个前提:

连接建立时间(connectTimeMillis)旧的排在前面,当phyTimeoutMillis配置参数大于0时关闭超时连接; connections数组里的连接空闲时间(idleMillis=currentTimeMillis - connection.lastActiveTimeMillis)长的排在前面,用于关闭或保活处理。

因为phyTimeoutMillis这个配置用的应该不多,暂时不说它,只说第2个条件是否有问题: 为了简化描述,假设keepAlive配置为true,配置的keepAliveBetweenTimeMillis小于maxEvictableIdleTimeMillis。

connections数组在druid初始化时放入,因为先放入的连接的lastActiveTimeMillis小,所以数组里面的各个连接的idleMillis从大到小排列,符合条件2。 假设直到keepAliveBetweenTimeMillis超时前,这些连接均未使用,全部加入了keepAliveConnections数组,加入的顺序是空闲时间长的连接先加入; connections数组被System.arraycopy前移操作清空,poolingCount清零; 从keepAliveConnections数组尾部向前逆序检测连接状态,检测正常的再加入connections数组尾部,poolingCount++; 假设所有连接全部检测正常,检测完毕后connections数组的内容刚好和初始状态的顺序刚好反过来,因为检测并不更新lastActiveTimeMillis,所以idleMillis的顺序变成了从小到大排序,条件2不再成立。

5.这里虽然没有更新lastActiveTimeMillis,但是会更新lastKeepTimeMillis;在拿连接时是从数组尾部去拿,并且如果lastActiveTimeMillis小于lastKeepTimeMillis,会操作lastActiveTimeMillis=lastKeepTimeMillis;不知道‘逆序检查keepAliveConnections数组连接‘这个逻辑是不是为了保证先创建的先使用~

connections里的这个System.arraycopy前移操作是不靠谱。

9

@zrlw @Pomelongan @OnePiecesu @messi612 各位大佬可以 review 一下我上面提交的PR。 https://github.com/alibaba/druid/pull/5083 1.使用成员变量在shrink过程中存储有效的connection,避免每次shrink new新的对象

shrinkConnections在shrink过程中只保存有效的链接(包括keepAlive),需要移除的链接依旧保存在evictConnections中 checkCount的意思应该是需要从当前链接池中移除的链接数量(为了保证连接数量尽可能接近min-idle设置的数量),和循环i做对比并不正确,应该和移除数量 evictCount 进行对比,在 minEvictableIdleTimeMillis 和 maxEvictableIdleTimeMillis 之间也可以从连接池中移除 System.arraycopy 的使用优化,仅填充数组非 null 值部分位置,提高效率
6

@zrlw @Pomelongan @OnePiecesu @messi612 各位大佬可以 review 一下我上面提交的PR。

抱歉,近期工作忙,一直没顾得上看,刚看了一下你提交的pr已经关闭了,是有什么改变么?

4

@huohuo 仔细看了一下 #5083 ,应该有问题,加入keepalive的连接会进行存活检测,检测通过的会重新加入connections数组,如果connections还保留,反而会触发close操作

2

简单解释一下shrink为什么用arraycopy搞不定: 假设只有两个连接,排在第一个位置的连接是后创建的,排在第二个位置的连接是先创建的,刚被应用交回连接池,所以排到了后面,两者空闲时间都不超清理阈值,但第二个连接的phyConnectTimeMillis已经过了清理阈值,shrink再次执行时将第二个连接加入待清理集合,arraycopy前移一个位置,第一个位置的连接替换成了第二个连接,但第二个连接被关闭,之后应用获取的连接是已经被关闭的连接,日志出现already close异常。 我们每天早上开始业务时常会看到这种日志,根源在于shrink想当然认为清理的连接都位于数组前部,但实际上并不是这样。

5

@huohuo 借鉴你的pr修订了一下pr #4726

3
5422 中用代码模拟了三个连接泄露的场景,三个场景分别是: 设置phyTimeoutMillis参数时候连接泄露场景 数据库操作出现FatalError情况下连接泄露场景 设置keepalive参数时候连接泄露场景