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

原创
2017/03/27 20:32
阅读数 9.2K

最近开发反馈了一个问题,说系统使用了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、要清楚的知道技术实现的原理,才能融会贯通,找出问题的关键

 

展开阅读全文
加载中

作者的其它热门文章

打赏
0
1 收藏
分享
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部