在一次修改了redis配置之后,出现了大批量的redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
原配置
1 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
2 jedisPoolConfig.setMaxTotal(150);
3 jedisPoolConfig.setMaxIdle(30);
4 jedisPoolConfig.setMinIdle(10);
5 jedisPoolConfig.setNumTestsPerEvictionRun(1024);
6 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
7 jedisPoolConfig.setMinEvictableIdleTimeMillis(1800000);
8 jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(1800000);
9 jedisPoolConfig.setMaxWaitMillis(1500);
10 jedisPoolConfig.setTestOnBorrow(true);
11 jedisPoolConfig.setTestWhileIdle(true);
12 jedisPoolConfig.setBlockWhenExhausted(false);
修改后的配置
1 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
2 jedisPoolConfig.setMaxTotal(60);
3 jedisPoolConfig.setMaxIdle(60);
4 jedisPoolConfig.setMinIdle(60);
5 jedisPoolConfig.setNumTestsPerEvictionRun(1024);
6 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(30000);
7 jedisPoolConfig.setMinEvictableIdleTimeMillis(1800000);
8 jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(1800000);
9 jedisPoolConfig.setMaxWaitMillis(500);
10 jedisPoolConfig.setTestOnBorrow(false);
11 jedisPoolConfig.setTestWhileIdle(true);
12 jedisPoolConfig.setBlockWhenExhausted(false);
如果一开始你就看出异常出现的问题所在了,那么这篇文章你就不用看了,是在浪费你的时间
而我看到这个异常最开始的想法是redis-server出现了问题,在不停debug的时候猜测是redis-server掐断了jedis连接,导致数据没有传输完成就断开了连接,但是这个想法怎么都没办法说服自己,虽然后来还是一直往这个方向方向查,因为福尔摩斯说过,排开所有不可能,剩下的那个原因再不可能那也是真相。可是福尔摩斯没有说怎么去找到最后的那个不可能,作为菜鸟的我一开始就把那个真相的不可能给排出了。对,我一开始就跑偏了,凤梨和菠萝傻傻分不清。
这几天有点空闲时间,所以想着吧jedis的源码都好好看一遍,看一下我对这个配置的修改到底引发了jedis的怎么不满,让它那么想弄死我。
一开始查找的方向是testOnBorrow和testWhileIdle,因为我的理解来看,其它几个修改应该不会引发redis-server对我的不满。不会那么随意掐断我的jedis连接。
testOnBorrow有原来的true改为了false,即在每次查询获取链接的时候,都会检查一下获取到的这个链接是否有效,也即是在borrow一个jedis链接之前,会用这个链接向redis-server发送一个PING命令,如果能得到一个PONG响应则返回此链接给调用者,而我为了节约这一次请求,所以就吧testOnBorrow改成了false。因为testWhileIdle改为了true,所以理所当然的认为在空闲的时候会触发pool中失效的连接的释放。
所以从现在开始,去彻底理解修改了的这两个参数的工作流,首先去阅读testOnBorrow参数源码如下:
org.apache.commons.pool2.impl.GenericObjectPool<T>
1 if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
2 boolean validate = false;
3 Throwable validationThrowable = null;
4 try {
5 validate = factory.validateObject(p);
6 } catch (final Throwable t) {
7 PoolUtils.checkRethrow(t);
8 validationThrowable = t;
9 }
10 if (!validate) {
11 try {
12 destroy(p);
13 destroyedByBorrowValidationCount.incrementAndGet();
14 } catch (final Exception e) {
15 // Ignore - validation failure is more important
16 }
17 p = null;
18 if (create) {
19 final NoSuchElementException nsee = new NoSuchElementException(
20 "Unable to validate object");
21 nsee.initCause(validationThrowable);
22 throw nsee;
23 }
24 }
25 }
在第5行调用了factory.validateObject(p),这一行代码的作用则是保证获取到的实例是安全的,对于jedis来说,就是保证获取到的连接是可用的,而且验证方法实现,则是利用获取到的jedis连接想redis-server发送了一个PING命令,判断是否能得到一个PONG响应,是则连接可用,反之则不可用。源码如下:
redis.clients.jedis.JedisFactory
1 @Override
2 public boolean validateObject(PooledObject<Jedis> pooledJedis) {
3 final BinaryJedis jedis = pooledJedis.getObject();
4 try {
5 HostAndPort hostAndPort = this.hostAndPort.get();
6 String connectionHost = jedis.getClient().getHost();
7 int connectionPort = jedis.getClient().getPort();
8 return hostAndPort.getHost().equals(connectionHost)
9 && hostAndPort.getPort() == connectionPort && jedis.isConnected()
10 && jedis.ping().equals("PONG");
11 } catch (final Exception e) {
12 return false;
13 }
14 }
在修改testOnBorrow为false之后,每次获取jedis连接时就不会再进行以上的连接验证了。从这样的源码看来,对于testOnBorrow参数的理解好像也没有什么问题。
而此时脑子里突然迸处一个想法,也许redis-server并没有对我有什么不满,也许出现的异常并不是在数据传输过程中,连接被掐断,而是连接一开始就断了。因为borrow一个连接的时候,没有对连接做有效性检查,那么可能borrow获取到的连接本身就是无效的,而testWhileIdle,可能根本就没有空闲的时候。为此,有去看testWhileIdle的工作流程。最后还是在org.apache.commons.pool2.impl.GenericObjectPool<T>的public abstract void evict() throws Exception;实现中找到了关于testWhileIdle的调用。
1 // User provided eviction policy could throw all sorts of
2 // crazy exceptions. Protect against such an exception
3 // killing the eviction thread.
4 boolean evict;
5 try {
6 evict = evictionPolicy.evict(evictionConfig, underTest,
7 idleObjects.size());
8 } catch (final Throwable t) {
9 // Slightly convoluted as SwallowedExceptionListener
10 // uses Exception rather than Throwable
11 PoolUtils.checkRethrow(t);
12 swallowException(new Exception(t));
13 // Don't evict on error conditions
14 evict = false;
15 }
16 if (evict) {
17 destroy(underTest);
18 destroyedByEvictorCount.incrementAndGet();
19 } else {
20 if (testWhileIdle) {
21 boolean active = false;
22 try {
23 factory.activateObject(underTest);
24 active = true;
25 } catch (final Exception e) {
26 destroy(underTest);
27 destroyedByEvictorCount.incrementAndGet();
28 }
29 if (active) {
30 if (!factory.validateObject(underTest)) {
31 destroy(underTest);
32 destroyedByEvictorCount.incrementAndGet();
33 } else {
34 try {
35 factory.passivateObject(underTest);
36 } catch (final Exception e) {
37 destroy(underTest);
38 destroyedByEvictorCount.incrementAndGet();
39 }
40 }
41 }
42 }
43 if (!underTest.endEvictionTest(idleObjects)) {
44 // TODO - May need to add code here once additional
45 // states are used
46 }
47 }
这是一个空闲线程移除的方法,Jedis中默认的触发频率是30s(我的配置也是30s),但timeBetweenEvictionRunsMillis和minEvictableIdleTimeMillis配置的时间都是1800s,所以,空闲超过半个小时的连接才会被回收,没有超过半小时的连接会走20行的逻辑,而在testWhileIdle为true时,虽然有destroy无效连接的时候,但是在debug时却从来没有看到被调用过,在第30行的连接校验也是通过校验了的。也就是说连接池中的连接始终有效,直到配置的MinEvictableIdleTimeMillis和SoftMinEvictableIdleTimeMillis的条件(半个小时)触发第6行使得evict为true的时候,批量destroy旧的连接,然后新建新的连接重新放入pool中。
到现在为止,猜测有两点,一、borrow从pool中拿到的连接就是无效的;二、testWhileIdle空闲检查时,所有的连接都有效。这是一个矛盾的结论,所以让我先想想静静。
后来想了一下,服务器会断开空闲客户端,更何况我已经把redis-server的连接数占满了。问了一下运维的同学,说的是服务的会主动断开超过5s的空闲连接,后来想了一下,因为服务实例比较多,每个实例60的连接,很容易超过redis允许的最大连接数,而实际上应用可能用不到60个连接,使用旧的连接配置时,每个容器的连接数也就在10个,就是最小空闲连接数,所以使用新配置时,最小空闲连接数时60,也就是说每个容器至少有50个连接都是浪费的,所以在pool中可能会存在大量的连接没使用到,导致大量的连接被redis-server主动断开,导致实例中会存在很多失效的连接,虽然每隔30s会进行一次清理,但是redis-server断开的频率可能更高(因为连接满了不够用),导致Jedis获取到的连接大概率无效。
当然最后的猜测我没有实验成功,主要是环境一直没有弄好,如果以后弄好了,我在补上。