JedisPool - Java.net.SocketException: Broken pipe (write failed)

一.引言

使用 JedisPoll 多线程写入时,阶段性报错 broken pipe,重启后任务正常,一段时间后再次出现该报错。

JedisPool 配置如下:

  val config = new JedisPoolConfig
  config.setMaxIdle(20)
  config.setMinIdle(20)
  config.setNumTestsPerEvictionRun(-2)
  config.setTimeBetweenEvictionRunsMillis(30000)
  config.setSoftMinEvictableIdleTimeMillis(3600000)
  config.setMinEvictableIdleTimeMillis(-1)
  config.setTestOnBorrow(false)
  config.setTestOnReturn(false)
  config.setTestWhileIdle(false)

二.问题分析与解决

1.问题场景

任务很简单,采取 JedisPool 在集群多线程写入数据,该报错会在任务运行一段时间后抛出,做了 Try catch 发现任务是偶发报错,并非全部写入失败;且重启任务后短时间内无该报错。初步分析就是 getResource 方法会存在获取无效连接的情况,从而导致偶发的写入失败,而重启后任务运行正常是因为任务刚启动获取的 redis 连接都有效,从而没有 Broken pipe。

2.问题解决

通过配置可以看到下述三个参数设置均为 false:

  config.setTestOnBorrow(false)
  config.setTestOnReturn(false)
  config.setTestWhileIdle(false)

将 TestOnBorrow 设置为 true 或者把 testWhileIdle 设置为 true,如果还不行则都设置为 true。

3.参数含义

· setTestOnBorrow

borrow 即从 JedisPool 连接池中获取连接,该参数控制在获取 redis 连接时检查该连接的有效性,如果检查到该链接已失效,则会清理掉并重新获取新连接,这里可以参考源码中 getResource 方法,该方法从 Pool 中执行 borrowObject 方法获取连接,该类所在位置为 redis.client.util.Pool,底层实现参考了 org.apache.commons.pool2.impl.GenericObjectPool。

  public T getResource() {
    try {
      return internalPool.borrowObject();
    } catch (Exception e) {
      throw new JedisConnectionException("Could not get a resource from the pool", e);
    }
  }

配置该参数后,borrow 调用获取新连接时,如果连接失效则会调用 activeObject 方法重置连接内部状态,相当于获取新连接:

    try {
        this.factory.activateObject(p);
    } catch (Exception var15) {
        try {
            this.destroy(p);
        } catch (Exception var14) {
        }

        p = null;
        if (create) {
            NoSuchElementException nsee = new NoSuchElementException("Unable to activate object");
            nsee.initCause(var15);
            throw nsee;
        }
    }

· setTestOnReturn

return 是在调用 returnResource 时配置的参数,该参数控制向连接池返回 resource 连接时,检查该连接的有效性,如果该链接已经失效则会清理该连接,该方法从 Pool 中执行 returnResourceObject 方法返还连接,该类所在位置为 redis.client.util.pool,底层实现同样参考 GenericObjectPool。

  public void returnResourceObject(final T resource) {
    if (resource == null) {
      return;
    }
    try {
      internalPool.returnObject(resource);
    } catch (Exception e) {
      throw new JedisException("Could not return the resource to the pool", e);
    }
  }

如果配置该参数后,在 returnObjectResource 时代码会对连接调用 ValidateObject 方法判断有效性,如果无效则 destory :

        if (this.getTestOnReturn() && !this.factory.validateObject(p)) {
            try {
                this.destroy(p);
            } catch (Exception var10) {
                this.swallowException(var10);
            }

            try {
                this.ensureIdle(1, false);
            } catch (Exception var9) {
                this.swallowException(var9);
            }

            this.updateStatsReturn(activeTime);
        }

· setTestWhileIdle

该参数控制对空闲的连接进行测试。该参数与 config.setTimeBetweenEvictionRunsMillis(30000) 相对应,这里空闲的判断依据即为该参数内设置的毫秒时间。这里检验空闲连接的有效性,真正执行清除的是 evict 方法 :

    public void evict() throws Exception {
        this.assertOpen();
        if (this.idleObjects.size() > 0) {
            PooledObject<T> underTest = null;
            EvictionPolicy<T> evictionPolicy = this.getEvictionPolicy();
            synchronized(this.evictionLock) {
                EvictionConfig evictionConfig = new EvictionConfig(this.getMinEvictableIdleTimeMillis(), this.getSoftMinEvictableIdleTimeMillis(), this.getMinIdle());
                boolean testWhileIdle = this.getTestWhileIdle();
                int i = 0;
                int m = this.getNumTests();

                while(true) ...

这里第 7 行还涉及到 MinEvictableIdleTimeMillis 和 SoftMinEvictableIdleTimeMillis 两个时间参数,这里涉及线程池 idleEvictTime 的选取,优先读取前者,如果前者为负数则默认为 maxLong、如果前者未配置则读取后者,所以线程池的一些配置参数会出现 -1,-2 的情况,实际上并不存在 -1 ms 的时间。

    public EvictionConfig(long poolIdleEvictTime, long poolIdleSoftEvictTime, int minIdle) {
        if (poolIdleEvictTime > 0L) {
            this.idleEvictTime = poolIdleEvictTime;
        } else {
            this.idleEvictTime = 9223372036854775807L;
        }

        if (poolIdleSoftEvictTime > 0L) {
            this.idleSoftEvictTime = poolIdleSoftEvictTime;
        } else {
            this.idleSoftEvictTime = 9223372036854775807L;
        }

        this.minIdle = minIdle;
    }

真正判断开始执行 evict 逻辑是在下述函数:

    public boolean evict(EvictionConfig config, PooledObject<T> underTest, int idleCount) {
        return config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() && config.getMinIdle() < idleCount || config.getIdleEvictTime() < underTest.getIdleTimeMillis();
    }

满足下述任意条件则执行逐出:

A. 连接空闲时间大于 softMinEvictableIdleTimeMillis 且 连接池的空闲数小于 minIdleNum 即 EvictionConfig 内的第三个参数

B. 连接空闲时间大于 minEvictableIdleTimeMillis 

当然不会只进行逐出,逐出到一定程度,线程池也会进行 ensureIdle 去新建新的连接,保证线程池中有足够多可用的连接。

三.总结

上述报错与场景设置 TestOnBorrow = true 和 TestWhileIdle = true 后完美解决,但是也会带来一定的性能损耗,可以根据自己的场景进行调整,一般情况可以尝试只开启 TestOnBorrow。

猜你喜欢

转载自blog.csdn.net/BIT_666/article/details/123499279
今日推荐