一.引言
使用 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。