一次压测时发现jedis连接池泄露的bug的经历

压测环境

  1. springboot版本为1.5.19,使用的spring data redis,其中jedis版本为2.9.1
  2. 压测工具使用jmeter,200个并发线程持续5分钟

现象

  1. 测试反映jemeter报大量的http超时

  2. 查看日志发现请求的响应时间随着时间的推移逐渐增大,应用重启之后报异常

    redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

问题排查

  1. 首先根据异常栈,定位到报错的jedis连接池的源码,梳理了一下获取连接的流程,代码如下:

    // 获取连接  
    public T getResource() {
        try {
          return internalPool.borrowObject();
        } catch (NoSuchElementException nse) {
          throw new JedisException("Could not get a resource from the pool", nse);
        } catch (Exception e) {
          // 是从这里抛出的异常
          throw new JedisConnectionException("Could not get a resource from the pool", e);
        }
      }
    
    // 从连接池中取一个连接
       public T borrowObject() throws Exception {
            return borrowObject(getMaxWaitMillis());
        }
    
    // borrowMaxWaitMillis 是一个超时时间,spring默认是-1,依旧是一直等待没有超时
    // 可以通过 spring.redis.pool.max-wait 配置
    public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
          ···省略部分代码
                if (blockWhenExhausted) {
                    if (p == null) {
                      //idleObjects是一个阻塞的队列
                      //private final LinkedBlockingDeque> idleObjects;
                        if (borrowMaxWaitMillis < 0) 
                            // 不带超时时间从队列取连接
                            p = idleObjects.takeFirst();
                        } else {
                        	// 根据超时间从队列取连接
                            p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                    TimeUnit.MILLISECONDS);
                        }
                    }
                    if (p == null) {
                        throw new NoSuchElementException(
                                "Timeout waiting for idle object");
                    }
               ···省略部分代码
        }
    
    
  2. 通过 jstack -l 导出应用的线程堆栈,发现所有的线程都阻塞在了redis连接池的队列

    “http-nio-8080-exec-200” #246 daemon prio=5 os_prio=0 tid=0x0000000026f8b000 nid=0x342c waiting on condition [0x000000002cd0b000]

    java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)

    • parking to wait for <0x00000000e3142768> (a java.util.concurrent.locks.AbstractQueuedSynchronizer C o n d i t i o n O b j e c t ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . L o c k S u p p o r t . p a r k ( L o c k S u p p o r t . j a v a : 175 ) a t j a v a . u t i l . c o n c u r r e n t . l o c k s . A b s t r a c t Q u e u e d S y n c h r o n i z e r ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer ConditionObject)atjava.util.concurrent.locks.LockSupport.park(LockSupport.java:175)atjava.util.concurrent.locks.AbstractQueuedSynchronizerConditionObject.await(AbstractQueuedSynchronizer.java:2039)
      at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:587)
  3. 分析应用的代码,通过skywalking查看链路,发现一次请求中大概有30多次的redis操作

  4. 从redis中 通过 慢查询日志slowlog get ,发现有些前缀为session的key比较慢,发现应用没有用到session却依赖了spring-session

​ 经过分析之后,做了一些调整

  1. spring.redis.pool.max-wait 配置连接池的等待时间为5000ms

  2. 增大连接池大小

    spring.redis.pool.max-active = 100

    spring.redis.pool.max-idle = 100

    spring.redis.pool.min-idle = 100

  3. 移除掉没有用的依赖spring-session

    ​ 经过调整之后继续压测,却发现当jemeter的并发数大了之后,后面还是会有一些报错.

    java.util.NoSuchElementException: Timeout waiting for idle object
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:447) ~[commons-pool2-2.4.3.jar:2.4.3]
    at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361) ~[commons-pool2-2.4.3.jar:2.4.3]
    at redis.clients.util.Pool.getResource(Pool.java:49) ~[jedis-2.9.1.jar:?]

    ​ 又排查了一遍,发现没有别的问题,推测可能是应用中redis使用不当造成的问题。
    后来又对别的应用进行了压测,发现一次请求只是对redis进行了一次操纵,却还是有获取不到连接的异常出现。出现问题后,继续排查,这次直接dump出了整个进程的堆栈,分析redis的连接池

    jmap -dump:format=b,file=[your file name] your pid

    然后通过jdk自带的jvisualvm导入dump文件,分析redis的连接池

一次压测时发现jedis连接池泄露的bug的经历_第1张图片

​ 从上图可以看到redis的连接池中idleObjects的数量是0,allObjects的数量是8,说明此时8个创建的连接还是使用状态,可 是从应用打印的日志情况来看,后面的应用一直没有获取到连接,而且从jstack中的线程堆栈来看,所有的线程同时阻塞在获 取连接的地方,没有一个线程有正在使用redis连接,所以此时推测,可能是redis的连接池发生了连接泄露,用完的连接在高 并发情况下没有归还到连接池,导致后面的请求一直获取不到redis连接

​ 之后在百度,google上搜索也找不到靠谱的答案,最后还是github的issue里找到了问题,具体可以在issue里自己去看

​ issue地址:https://github.com/xetorthio/jedis/issues/1920。然后将jedis的版本升级到了2.10.2最后终于找到了问题

总结

经过了这次的压测,对jedis的连接池和redis有了更深的了解。对java应用的一些问题排查方法也有了一些了解。总结了一点经验

  1. 出现问题首先查看日志,有异常的话,根据异常栈去分析异常的原因
  2. 通过jstasck查看线程的情况(在另一次问题排查过程中发现有线程由于异常原因直接退出的情况,还有一次发邮件的时候线程卡死在了socekt.read()方法)
  3. 通过jmap直接dump出java进程,分析问题
  4. 出现了问题,如果是开源的项目,优先去gitbub的issue里面去搜
  5. 开源项目也会有bug,初期发现了问题后一直对jedis太过信任,导致浪费了时间在其他地方

你可能感兴趣的:(other)