Jedis连接池竟然会资源泄露

Jedis

Posted by MistRay on August 21, 2020

1.前言

Jedis是我们经常使用的Redis Java客户端.在SpringBoot2.X将lettuce作为默认Redis Java客户端之前,Jedis几乎是具备统治地位的.今天我会通过复盘一个压测时遇到的问题来解析Jedis 2.9.1版本一个必现的连接池资源泄露BUG.

2.问题描述

在某次压测中,某服务中产生了这样一条异常日志,期初我们猜测这可能是Jedis连接池负载较高,导致的连接资源紧张的情况.但在持续的压力下,Jedis连接池内的资源竟然大部分都不可用了,最终测试结果以连接池中所有的资源都不可用而告终.

image

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,意思是并没有损坏.

image

我又注意到了另一个很诡异的问题,明明是从连接池中取出的资源,资源与连接池绑定的映射字段dataSource却是null.难道资源在使用过程中,有什么操作导致资源和池之间解除了绑定关系么?

image

排查到这里基本就可以确定,这个状态不一致的问题就是导致线程池资源耗尽的元凶了,因为连接池认为资源并没有broken,但socket其实已经closed了,连接池也没办法对这些不可用的资源做回收.但想知道为什么会产生这种情况,还是需要去读Jedis的源码.

4.分析源码

从上面的代码可以看到,从JedisPool中获取资源首先要调用getResource()函数.

  @Override
  public Jedis getResource() {
    // 获取资源
    Jedis jedis = super.getResource();
    // 将JedisPool与获取到的资源关联
    jedis.setDataSource(this);
    return jedis;
  }

然后释放资源的时候调用的是Jedisclose()函数.

  @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,最后归还资源到池中,是不会有问题的.但这里有一个非常不明显的线程安全问题.

image

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 版权协议,转载请附上原文出处链接和本声明。