JedisCluster中應用的Apache Commons Pool對象池技術

對象池技術在服務器開發上應用廣泛。在各種對象池的實現中,尤其以數據庫的連接池最爲明顯,可以說是每個服務器必須實現的部分。
 
apache common pool 官方文檔可以參考:https://commons.apache.org/proper/commons-pool/
 
結合JedisPool看Commons Pool對象池技術
 
結合JedisPool,我們來了解一下commons pool的整體設計:
 


 
 
面向用戶的往往是ObjectPool,用戶看到的是一個對象池,對於使用Redis連接的用戶來說,就是JedisPool。對象池ObjectPool提供了借用對象,返還對象,驗證對象等API,需要具體的配置GenericObjectPoolConfig來確定池的大小,以及創建具體池化對象的工廠接口PooledObjectFactory來根據需要創建,銷燬,激活,鈍化每個對象。
 
PooledObjectFactory接口,用來創建池對象(makeObject),將不用的池對象進行鈍化(passivateObject),對要使用的池對象進行激活(activateObject),對池對象進行驗證(valiateObject),將有問題的池對象銷燬(destroyObject)。
 
如果需要使用commons-pool,那麼就需要提供一個PooledObjectFactory接口的具體實現,一個比較簡單的辦法是使用BasePooledObjectFactory這個抽象類,只需要實現兩個方法:create()和wrap(T obj)。JedisFactory也就是用來創建每個Jedis連接的對象工廠類,其中直接實現了PooledObjectFactory,makeObject的過程中,直接創建了PooledObject<Redis>。
 
當我們使用JedisPool.getResource(),用於返回jedis連接時,實際調用的是其中GenericObjectPool的borrowObject方法,在Jedis連接池中借用一個對象。
 
借用對象時,先去idleObjects(LinkedBlockingDeque<Pooled<Jedis>>)列表中查看是否有空閒的對象,如果存在則直接使用;如果不存在,則需要考慮在沒有超出連接池最大數量的情況下,使用PooledObjectFactory進行初始化,這裏使用的是JedisFactory.makeObject來創建連接,並將其激活。
 
 
對於Jedis對象,不能總是重用同一個對象,在使用一段時間後其就會產生失效,連接出現異常。此時就需要使用JedisPool來獲取資源,注意在最後要回收資源,實際上就是returnObject,以下面的代碼作爲實例:
 
 
Jedis jedis = jedisPool.getResource();
        try {
            while (true) {
                String productCountString = jedis.get("product");
                if (Integer.parseInt(productCountString) > 0) {
                    if (acquireLock(jedis, "abc")) {
                        int productCount = Integer.parseInt(jedis.get("product"));
                        System.out.println(String.format("%tT --- Get product: %s", new Date(), productCount));
//                        System.out.println(productCount);
                        jedis.decr("product");
                        releaseLock(jedis, "abc");
                        return "Success";
                    }
                    Thread.sleep(1000L);
                } else {
                    return "Over";
                }
            }
        } finally {
            jedis.close();
        }
 
 
 
 
JedisCluster的連接/執行源碼研究
 
 
我們使用的JedisCluster(Redis集羣模式)需要初始化並使用JedisCluster對象,通過該對象來進行Redis的相關操作,下面就列舉出了JedisCluster的基本類圖結構:
 


 
 
在執行任務BinaryJedisCluster的相關命令 set/get/exist 等redis命令時,都採用回調的方式:
 
 
@Override
  public String set(final byte[] key, final byte[] value) {
    return new JedisClusterCommand<String>(connectionHandler, maxRedirections) {
      @Override
      public String execute(Jedis connection) {
        return connection.set(key, value);
      }
    }.runBinary(key);
  }
 
 
 
初始化一個JedisClusterCommand對象,執行runBinary方法,進行execute(Jedis connection)回調,其實可以看出執行回調之前的作用是將使用到的Jedis連接在內部統一管理起來。
 
可以猜想使用了JedisSlotBasedConnectionHandler中實現了父類定義的getConnection()獲取Redis連接的方法:
 
 
@Override
  public Jedis getConnection() {
    // In antirez's redis-rb-cluster implementation,
    // getRandomConnection always return valid connection (able to
    // ping-pong)
    // or exception if all connections are invalid
 
    List<JedisPool> pools = getShuffledNodesPool();
 
    for (JedisPool pool : pools) {
      Jedis jedis = null;
      try {
        jedis = pool.getResource();
 
        if (jedis == null) {
          continue;
        }
 
        String result = jedis.ping();
 
        if (result.equalsIgnoreCase("pong")) return jedis;
 
        pool.returnBrokenResource(jedis);
      } catch (JedisConnectionException ex) {
        if (jedis != null) {
          pool.returnBrokenResource(jedis);
        }
      }
    }
 
    throw new JedisConnectionException("no reachable node in cluster");
  }
 
 
 
 
其中調用的方法 getShuffledNodesPool(),就是從JedisClusterInfoCache中包含的所有JedisPool,執行shuffle操作,隨機拿到對應的JedisPool,去其中getResource拿到連接。
 
這屬於隨機去獲取connection,但事實上並不是這樣處理的,我們可以通過slot來獲得其對應的Connection,在JedisClusterCommand.run方法的最後一行中,其中第三個參數爲是否爲tryRandomMode,調用方式顯示爲非random Mode。
 
return runWithRetries(SafeEncoder.encode(keys[0]), this.redirections, false, false);
  
 
可以根據slot來定位到具體的JedisPool,getResource拿到對應的Jedis Connection,但該方法也標明瞭不能保證一定能夠拿到可用的連接。
 
@Override
public Jedis getConnectionFromSlot(int slot) {
  JedisPool connectionPool = cache.getSlotPool(slot);
  if (connectionPool != null) {
    // It can't guaranteed to get valid connection because of node
    // assignment
    return connectionPool.getResource();
  } else {
    return getConnection();
  }
}
 
 
 
在JedisClusterInfoCache緩存了Map<String,JedisPool>(host:port->JedisPool)和Map<Integer, JedisPool>(slot->JedisPool),用於查詢連接,那麼這兩個緩存是如何查詢出來的,這就需要用到Jedis.clusterNodes,它可以通過該Redis連接找到其他連接的相關配置,例如可以發現整個集羣的配置,其中三個master,三個slave,並且能夠識別出自身連接,可參考文檔:http://redis.io/commands/cluster-nodes
 
 
5974ed7dd81c112d9a2354a0a985995913b4702c 192.168.1.137:6389 master - 0 1468809898374 26 connected 0-5640
d08dc883ee4fcb90c4bb47992ee03e6474398324 192.168.1.137:6390 master - 0 1468809898875 25 connected 5641-11040
ffb4db4e1ced0f91ea66cd2335f7e4eadc29fd56 192.168.1.138:6390 slave 5974ed7dd81c112d9a2354a0a985995913b4702c 0 1468809899376 26 connected
c69b521a30336caf8bce078047cf9bb5f37363ee 192.168.1.137:6388 master - 0 1468809897873 28 connected 11041-16383
532e58842d001f8097fadc325bdb5541b788a360 192.168.1.138:6389 slave c69b521a30336caf8bce078047cf9bb5f37363ee 0 1468809899876 28 connected
aa52c7810e499d042e94e0aa4bc28c57a1da74e3 192.168.1.138:6388 myself,slave d08dc883ee4fcb90c4bb47992ee03e6474398324 0 0 19 connected
 
 
 
分配slot只可能在master節點上發生,而不會在slave節點上發生,這意味着Redis集羣並未進行類似讀寫分離的形式。當Redis集羣的slot發生改變時,會重新初始化該Cache,重置slot。
 
而執行每個get/set等Redis操作時,真正的核心入口,其實是JedisClusterCommand.runWithRetries方法:
 
 
private T runWithRetries(byte[] key, int redirections, boolean tryRandomNode, boolean asking) {
    if (redirections <= 0) {
      throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
    }
 
    Jedis connection = null;
    try {
 
      if (asking) {
        // TODO: Pipeline asking with the original command to make it
        // faster....
        connection = askConnection.get();
        connection.asking();
 
        // if asking success, reset asking flag
        asking = false;
      } else {
        if (tryRandomNode) {
          connection = connectionHandler.getConnection();
        } else {
          connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
        }
      }
 
      return execute(connection);
    } catch (JedisConnectionException jce) {
      if (tryRandomNode) {
        // maybe all connection is down
        throw jce;
      }
 
      // release current connection before recursion
      releaseConnection(connection);
      connection = null;
 
      // retry with random connection
      return runWithRetries(key, redirections - 1, true, asking);
    } catch (JedisRedirectionException jre) {
      // if MOVED redirection occurred,
      if (jre instanceof JedisMovedDataException) {
        // it rebuilds cluster's slot cache
        // recommended by Redis cluster specification
        this.connectionHandler.renewSlotCache(connection);
      }
 
      // release current connection before recursion or renewing
      releaseConnection(connection);
      connection = null;
 
      if (jre instanceof JedisAskDataException) {
        asking = true;
        askConnection.set(this.connectionHandler.getConnectionFromNode(jre.getTargetNode()));
      } else if (jre instanceof JedisMovedDataException) {
      } else {
        throw new JedisClusterException(jre);
      }
 
      return runWithRetries(key, redirections - 1, false, asking);
    } finally {
      releaseConnection(connection);
    }
  }
 
 
 
出現的Redis Retries問題
 
可以參考:http://carlosfu.iteye.com/blog/2251034,講的非常好。同樣,我們的出現的異常堆棧:
 
- 2016-06-04 00:02:51,911 [// - - ] ERROR xxx - Too many Cluster redirections?
redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException: Too many Cluster redirections?
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:97)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:152)
at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:131)
 
 
直譯過來就是出現過多的redirections異常,出現過JedisConnectionException,完整的堆棧內容:
 
 
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
    at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:198)
    at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
    at redis.clients.jedis.Protocol.process(Protocol.java:141)
    at redis.clients.jedis.Protocol.read(Protocol.java:205)
    at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:297)
    at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:216)
    at redis.clients.jedis.Connection.getBulkReply(Connection.java:205)
    at redis.clients.jedis.Jedis.get(Jedis.java:101)
    at redis.clients.jedis.JedisCluster$3.execute(JedisCluster.java:79)
    at redis.clients.jedis.JedisCluster$3.execute(JedisCluster.java:76)
    at redis.clients.jedis.JedisClusterCommand.runWithRetries(JedisClusterCommand.java:119)
    at redis.clients.jedis.JedisClusterCommand.run(JedisClusterCommand.java:30)
    at redis.clients.jedis.JedisCluster.get(JedisCluster.java:81)
    at redis.RedisClusterTest.main(RedisClusterTest.java:30)
 
 
 
 
調試狀態下的異常信息:
 
 
jce = {redis.clients.jedis.exceptions.JedisConnectionException@1014} "redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream."
 detailMessage = "Unexpected end of stream."
 cause = {redis.clients.jedis.exceptions.JedisConnectionException@1014} "redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream."
 stackTrace = {java.lang.StackTraceElement[0]@1017}
 suppressedExceptions = {java.util.Collections$UnmodifiableRandomAccessList@1018}  size = 0
 
 
 
關於這個問題,可以參考:http://blog.csdn.net/jiangguilong2000/article/details/45025355
 
客戶端buffer控制。在客戶端與server進行的交互中,每個連接都會與一個buffer關聯,此buffer用來隊列化等待被client接受的響應信息。如果client不能及時的消費響應信息,那麼buffer將會被不斷積壓而給server帶來內存壓力.如果buffer中積壓的數據達到閥值,將會導致連接被關閉,buffer被移除。
 
 開發環境上執行查詢該參數的命令:config get client-output-buffer-limit
 
 
1) "client-output-buffer-limit"
2) "normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60"
  
 
關於Redis上的所有參數詳解,可以參考:http://shift-alt-ctrl.iteye.com/blog/1882850
 
JedisMovedDataException
 
jre = {redis.clients.jedis.exceptions.JedisMovedDataException@2008} "redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 8855 192.168.1.137:6390"
 targetNode = {redis.clients.jedis.HostAndPort@2015} "192.168.1.137:6390"
 slot = 8855
 detailMessage = "MOVED 8855 192.168.1.137:6390"
 cause = {redis.clients.jedis.exceptions.JedisMovedDataException@2008} "redis.clients.jedis.exceptions.JedisMovedDataException: MOVED 8855 192.168.1.137:6390"
 stackTrace = {java.lang.StackTraceElement[0]@1978}
 suppressedExceptions = {java.util.Collections$UnmodifiableRandomAccessList@1979}  size = 0
 
 
 
日誌中出現超時異常:
 
4851:S 18 Jul 11:05:38.005 * Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
  
可以參考github上關於redis的討論:https://github.com/antirez/redis/issues/641,關閉AOF,可以暫時解決問題。JedisCluster中應用的Apache Commons Pool對象池技術 
 
  • 1af68c22-40a5-378b-aa6a-ac6a1808b84a-thumb.png
  • 大小: 91.2 KB
  • ae9e9b4f-1792-3b77-9eea-6771b51498b0-thumb.png
  • 大小: 65.2 KB
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章