1.前言
Jedis是我们经常使用的Redis Java客户端.在SpringBoot2.X将lettuce作为默认Redis Java客户端之前,Jedis几乎是具备统治地位的.今天我会通过复盘一个压测时遇到的问题来解析Jedis 2.9.1版本一个必现的连接池资源泄露BUG.
2.问题描述
在某次压测中,某服务中产生了这样一条异常日志,期初我们猜测这可能是Jedis连接池负载较高,导致的连接资源紧张的情况.但在持续的压力下,Jedis连接池内的资源竟然大部分都不可用了,最终测试结果以连接池中所有的资源都不可用而告终.
3.排查过程
我们马上紧急dump了堆内存,开始分析为什么连接池所有的资源都不可用了,虽然是压测,但压力还没大到把Redis连接池所有资源都繁忙的才对.所以我们一致猜测,应该是某个地方在使用JedisPool中Jedis后没有释放资源导致的.
public static boolean setNXEXString(String key, String value, int seconds) throws Exception {
if (StringUtils.isBlank(key)) {
return false;
}
Jedis jedis = null;
try {
jedis = getJedis();
String result = jedis.set(key, value, "NX", "EX", seconds);
return StringUtils.equals("OK", result);
} catch (Exception ex) {
logger.error("[setNXString]保存String资源失败,key:{}, key:{}", key, value, ex);
throw ex;
} finally {
// 怀疑没有release导致了资源泄露
release(jedis);
}
}
public static Jedis getJedis() {
if (jedisPool != null) {
Jedis resource = jedisPool.getResource();
return resource;
} else {
logger.error("获取Redis连接出错");
}
return null;
}
private static final void release(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
但在排查了工具类中所有的函数后,发现并没有发现未释放资源的情况.这和我们预想的问题起因不太一致,只能继续排查堆dump,看有没有别的发现.
果不其然,我们发现了堆中Jedis对象都很奇怪,几乎所有Jedis对象里面的socket连接都是closed的状态.socket都close了,当然这个连接就用不了了.但比较诡异的一个点是明明socket已经close了,但表示连接是否损坏的broken字段却是false,意思是并没有损坏.
我又注意到了另一个很诡异的问题,明明是从连接池中取出的资源,资源与连接池绑定的映射字段dataSource却是null.难道资源在使用过程中,有什么操作导致资源和池之间解除了绑定关系么?
排查到这里基本就可以确定,这个状态不一致的问题就是导致线程池资源耗尽的元凶了,因为连接池认为资源并没有broken,但socket其实已经closed了,连接池也没办法对这些不可用的资源做回收.但想知道为什么会产生这种情况,还是需要去读Jedis的源码.
4.分析源码
从上面的代码可以看到,从JedisPool
中获取资源首先要调用getResource()
函数.
@Override
public Jedis getResource() {
// 获取资源
Jedis jedis = super.getResource();
// 将JedisPool与获取到的资源关联
jedis.setDataSource(this);
return jedis;
}
然后释放资源的时候调用的是Jedis
的close()
函数.
@Override
public void close() {
// 判断是否是从连接池中取出的资源
if (dataSource != null) {
// 判断资源是否损坏
if (client.isBroken()) {
// 损坏了,释放资源
this.dataSource.returnBrokenResource(this);
} else {
// 未损坏,放回池中复用
this.dataSource.returnResource(this);
}
// 将资源与池解除锁定,这行就是导致问题的元凶
this.dataSource = null;
} else {
// 如果不是从池中取出的,关闭socket,释放
client.close();
}
}
如果是正常情况下,获取到资源,操作Jedis,最后归还资源到池中,是不会有问题的.但这里有一个非常不明显的线程安全问题.
1.线程1在某个资源刚归还到池中并且还没执行到this.DataSource = null
2.同一资源被线程2从池里面获取出来,并将资源与JedisPool
绑定
3.线程1执行到this.DataSource = null
,将同一资源解绑
4.线程2使用结束后,释放资源,发现dataSource是null认为资源不是从池里取出来的,关闭了socket.
总结
Jedis团队已经在2.10.2
版本将该bug修复,详情可见PR.
理论上只要并发量够大并且服务启动时间足够长,这个问题几乎是100%复现的.
所以希望看到的小伙伴关注下自己负责的项目中Jedis的版本,如果看到Jedis的close()
函数中有this.DataSource = null;
这行代码,要尽快把Jedis版本升级到2.10.2
及以上版本.
转载
本文遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。