在一次修改了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獲取到的連接大概率無效。
當然最后的猜測我沒有實驗成功,主要是環境一直沒有弄好,如果以后弄好了,我在補上。
