一次redis連接配置修改引發的redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.異常


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM