您的位置:首页 > 数据库 > Redis

一次jedis使用不规范,导致redis客户端close_wait大量增加的bug

2017-03-27 00:00 1071 查看
摘要: Timeout waiting for idle object

最近开发反馈了一个问题,说系统使用了codis之后,发现当并发量上来之后,会抛出异常:could not get resource from pool,更底层的原因是:Timeout waiting for idle object,然后开始查问题。

1、可能是配置问题?

我们对jodis进行了一层浅封装,将配置进行独立,开放给开发人员的配置比较少,也就几个:

codis.pool.maxTotal=1000 //对象池最大数
codis.pool.maxIdle=1000 //idle对列最大数
codis.pool.minIdle=0 //idle队列最小数
codis.pool.maxWaitMillis=20000 //获取连接超时时间

没发现什么问题,因为其他系统也是就配置这几个参数,然后调整这些参数,没有任何改善,仍然会出现问题

2、代码分析

RoundRobinJedisPool代码中有如下代码:

public Jedis getResource() {
ImmutableList<PooledObject> pools = this.pools;
if (pools.isEmpty()) {
throw new JedisException("Proxy list empty");
}

for (;;) {
int current = nextIdx.get();//1.获取上次使用哪个proxy
int next = current >= pools.size() - 1 ? 0 : current + 1;//2.轮询到另外一个proxy
if (nextIdx.compareAndSet(current, next)) {// 3.设置本次使用的proxy,以供下次使用
return pools.get(next).pool.getResource();//获取该proxy的JedisPool并获取Jedis实例
}
}
}

(提出问题的同事发现的,感谢)for循环内部的代码不是原子的,尝试添加了同步块,突然发现,问题解决了,此问题不报了,到此,以为解决了(其实这是牵出另外一个问题的关键),修改之后代码如下:

public Jedis getResource() {
ImmutableList<PooledObject> pools = this.pools;
if (pools.isEmpty()) {
throw new JedisException("Proxy list empty");
}
/**
* 增加同步,防止高并发下的could not get resource from pool的异常
*/
synchronized (this.pools) {
for (;;) {
int current = nextIdx.get();
int next = current >= pools.size() - 1 ? 0 : current + 1;
if (nextIdx.compareAndSet(current, next)) {
return pools.get(next).pool.getResource();
}
}
}
}

3、系统开始产生大量的close_wait

2小时不到,产生了28000+,而且还在继续上升,持续下去,系统就会因为文件句柄被耗尽而宕机,此问题更紧急。

3.1、close_wait产生的原因

网上有大量的解释管理close_wait和time_wait产生的原因,我就不多讲,自己Google,简单来说就是“被关闭”了,比如这里的问题就是,codis_proxy主动断开了客户端的Jedis连接,而Jedis连接无法感知到此连接被关闭了,此时网络状态就是close_wait了,所以就这方面入手

3.2、codis_proxy中的主动关闭配置?

我们使用的是codis2.0版本,其中proxy有一个配置:session_max_timeout=1800,此配置的含义是:当proxy发现某个客户端连接超过1800s还没有数据发送过来,就主动的关闭该连接。但有一点推测不通:jedis采用的common-pool2来管理Jedis实例,默认设置超过30s就会扫描idle队列,凡是idle时间超过60s的Jedis对象都会被回收,而proxy的设置为1800s,所以没有这种可能,代码如下:

public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
// defaults to make your life with connection pool easier :)
setTestWhileIdle(true);
setMinEvictableIdleTimeMillis(60000);//对象存活时间
setTimeBetweenEvictionRunsMillis(30000);//清除对象的线程执行间隔
setNumTestsPerEvictionRun(-1);//每次扫描会扫描多少个对象,-1为不限制
}
}

3.3、common-pool2本身的bug?

开始分析common-pool2的代码,代码本身不复杂,可以参考我转载的别人的blog:https://my.oschina.net/u/1178805/blog/867730
关键流程:

borrowObject:当调用Jedis的getResource()方法时候,底层是去调用Pool类的getResource方法:

public T getResource() {
try {
return internalPool.borrowObject();
} catch (Exception e) {
throw new JedisConnectionException("Could not get a resource from the pool", e);
}
}
internalPool:为GenericObjectPool类型的成员变量,此类就是common-pool2的对象池

borrowObject的源码:

public T borrowObject(long borrowMaxWaitMillis) throws Exception {
//忽略了部分和本文无关的代码
PooledObject<T> p = null;

// Get local copy of current config so it is consistent for entire
// method execution
boolean blockWhenExhausted = getBlockWhenExhausted();

boolean create;
long waitTime = System.currentTimeMillis();

while (p == null) {
create = false;
if (blockWhenExhausted) {//blockWhenExhausted的默认配置为true
p = idleObjects.pollFirst();
if (p == null) {
p = create();//这里并发情况下有可能返回为null
if (p != null) {
create = true;
}
}
if (p == null) {
if (borrowMaxWaitMillis < 0) {//我们配置的为20000ms
p = idleObjects.takeFirst();
} else {
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
}
}
if (p == null) {//这里就是我们文初贴出来的错误信息
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
}
}
//忽略了部分和本文无关的代码

return p.getObject();
}

以上两段代码就是文初贴出来的两个问题,并发量上来的时候确实有可能出现这个问题。分析如下:

从idleObjects中获取Jedis实例,如果为空则进行创建,如果不为空则返回;当并发调用create方法的时候,且idleObjects一直为空的情况下,就开始报错了:Timeout waiting for idle object,也就产生了我们文初的问题。但什么情况下会导致idleObjects一直为空呢?

3.4、idleObjects一直为空?

由于jedis底层采用common-pool2来进行Jedis实例的管理,而common-pool2的玩法就是:有借有还,再借不难,只借不还,要你好看。通过borrowObject获取对象,通过returnObject归还对象,Jedis也对此做了封装,前面展现了getResource的源码,内部是调用borrowObject,归还是通过close方法执行的:

public void close() {
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);//内部调用invalidObject方法
} else {
this.dataSource.returnResource(this);//内部调用returnObject方法
}
} else {
client.close();
}
}

所以,答案终于找到了,没有调用close方法来进行归还。

4、水落石出

开发没有使用我们封装好的缓存操作代码,而是自己封装了jedis代码来操作:

public synchronized Jedis getJedis () {
return jedisPool.getResource();//就是这里,只借不还,没有调用close方法
}

当没有调用close方法返回Jedis实例的情况下,idleObjects一直是空的,超时是必然的,然后proxy发现大量的连接没有数据进入,开始大批量的关闭连接,客户端close_wait至此开始增加,将这里调用close方法修改之后问题解决

5、经验

5.1、经验证:线上出现的不可思议的问题往往都是非常SB的问题(^_^)

5.2、在没有绝对的把握情况下,不要随意修改经过线上验证的代码

5.3、要清楚的知道技术实现的原理,才能融会贯通,找出问题的关键
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息