生产环境连接redis报错redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream
环境跟现象
redis 配置了timeout 10分钟,生产环境出现redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream,这个错误是偶尔出现,通常出现在redis一段时间没有操作后出现的,后面经常访问,就没有出现
原因跟问题重现
虽然使用了连接池,但是还是出现这个问题,是因为连接池里的跟redis连接出现问题,我们知道获取连接池实例,是通过getResource方法来获取的,具体实现代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Pool.java
public T getResource() {
try {
return internalPool.borrowObject();
} catch (NoSuchElementException nse) {
if (null == nse.getCause()) { // The exception was caused by an exhausted pool
throw new JedisExhaustedPoolException(
"Could not get a resource since the pool is exhausted", nse);
}
// Otherwise, the exception was caused by the implemented activateObject() or ValidateObject()
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);
}
}
common-pools2的GenericObjectPool中的borrowObject方法,这里也透露了jedispool使用的是依赖于common-pools2,这里我们先看下这个方法的解析,来自官方解析,
使用特定的等待时间从池中借用一个对象,该等待时间仅在BaseGenericObjectPool.getBlockWhenExhausted()为true时适用。
如果池中有一个或多个空闲实例可用,则将基于BaseGenericObjectPool.getLifo(),激活并返回的值选择一个空闲实例。如果激活失败,或者testOnBorrow设置为true且验证失败,则实例将被销毁并检查下一个可用实例。这将继续进行,直到返回有效实例或不再有可用的空闲实例为止。
如果池中没有可用的空闲实例,则行为取决于maxTotal(如果适用) BaseGenericObjectPool.getBlockWhenExhausted()和传递给borrowMaxWaitMillis参数的值 。如果从池中检出的实例数少于maxTotal,一个新实例的创建,激活和验证(如果适用),并返回给调用方。如果验证失败,NoSuchElementException 则抛出a。
如果池已用尽(没有可用的空闲实例,也没有创建新实例的能力),则此方法将阻塞(如果 BaseGenericObjectPool.getBlockWhenExhausted()为true)或抛出 NoSuchElementException(如果 BaseGenericObjectPool.getBlockWhenExhausted()为false)。此方法BaseGenericObjectPool.getBlockWhenExhausted()为true 时将阻止的时间长度由传递给borrowMaxWaitMillis 参数的值确定。
当池耗尽时,可能会同时阻止多个调用线程,以等待实例变为可用。已实现“公平”算法,以确保线程按请求到达顺序接收可用实例。
英文看这里, 这里可以了解pool的获取实力的过程
这里pool的获取实例的逻辑是这样的:
- 先从空闲连接列表获取可用连接
- 如果获取不了,就创建新的,如果创建不了就根据是否阻塞来获取连接
- 拿到非空连接之后(注意这里连接不一定是好的,有可能server已经关闭,消息不对称),进行如下代码,这里是否使用判断就是需要加入条件getTestOnBorrow,这个是通过配置参数testonborrow来开启的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 来自GenericObjectPool.java
if (p != null && getTestOnBorrow()) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (final Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
try {
destroy(p);
destroyedByBorrowValidationCount.incrementAndGet();
} catch (final Exception e) {
// Ignore - validation failure is more important
}
p = null;
if (create) {
final NoSuchElementException nsee = new NoSuchElementException(
"Unable to validate object");
nsee.initCause(validationThrowable);
throw nsee;
}
}
}
这里使用到factory.validateObject(p)的具体实现如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 来自jedisfactory.java
public boolean validateObject(PooledObject<Jedis> pooledJedis) {
final BinaryJedis jedis = pooledJedis.getObject();
try {
HostAndPort hostAndPort = this.hostAndPort.get();
String connectionHost = jedis.getClient().getHost();
int connectionPort = jedis.getClient().getPort();
return hostAndPort.getHost().equals(connectionHost)
&& hostAndPort.getPort() == connectionPort && jedis.isConnected()
&& jedis.ping().equals("PONG");
} catch (final Exception e) {
return false;
}
}
这里拆包从poolobject获取jedis对象,然后对比host以及port来判断是否同一个redisserver,然后还发送一个ping命令,这里就是关键了,如果jedis连接已经被server掐断了,就收不到pong的返回,从而返回false,这里也说明,每次获取jedis连接,都会发送一个ping命令。
如果校验是false,就进入到destroy方法,这里的操作就是common-pool2一套操作了,就是清理环境(包含空闲列表, 全局列表,计数器更新),destory之后,就会进入循环,重新之前的步骤,这样继续从空闲列表获取第一个连接,然后重新判断,如果他是好的,就返回,如果不好,继续操作,到后面空闲列表为空,就执行如下代码:1
2
3
4
5
6
7
8create = false;
p = idleObjects.pollFirst();
if (p == null) {
p = create();
if (p != null) {
create = true;
}
}
这个create变量就是用来控制是否要下文校验失败判断是否抛异常,也是用来跳出循环使用。
结论: 设置testonborrow=true可以保证业务获取到的jedis连接是可用的(排除pool满了或者redis服务死了的情况),这样业务不感知或者需要加重试代码。但是这个需要带来开销的,每次获取连接都需要ping命令发送来探测连接可用性。
延伸: testonreturn
testonreturn是什么,这个设置用在returnobject也就是使用jedis实例的close方法时,最终会调用returnObject方法,这里截取部分代码来看下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// genericobjectpool.java
if (getTestOnReturn() && !factory.validateObject(p)) {
try {
destroy(p);
} catch (final Exception e) {
swallowException(e);
}
try {
ensureIdle(1, false);
} catch (final Exception e) {
swallowException(e);
}
updateStatsReturn(activeTime);
return;
}
这里看到如果设置true之后,会执行validateObject函数,这个也就是刚才testonborrow也说到的函数,这里如果校验失败,也会执行destroy方法,也就是做之前说的相同动作。
延伸: common-pool2
其实jedis使用的池是通过集成apache common-pool2来实现的,其实刚才说的很多方法都是common-pool2来要求实现的
大家有兴趣可以阅读下 common-pool2 的实现细节 一文来加深理解。