註冊中心 Eureka 源碼解析 —— 應用實例註冊發現(五)之過時

摘要: 原創出處 http://www.iocoder.cn/Eureka/instance-registry-evict/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!html

本文主要基於 Eureka 1.8.X 版本java


🙂🙂🙂關注**微信公衆號:【芋道源碼】**有福利:segmentfault

  1. RocketMQ / MyCAT / Sharding-JDBC 全部源碼分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文註釋源碼 GitHub 地址
  3. 您對於源碼的疑問每條留言將獲得認真回覆。甚至不知道如何讀源碼也能夠請教噢
  4. 新的源碼解析文章實時收到通知。每週更新一篇左右
  5. 認真的源碼交流微信羣。

1. 概述

本文主要分享 Eureka-Server 過時超時續租的租約數組

FROM 《深度剖析服務發現組件Netflix Eureka》
微信

推薦 Spring Cloud 書籍網絡

推薦 Spring Cloud 視頻架構

2. 爲何須要過時

正常狀況下,應用實例下線時候會主動向 Eureka-Server 發起下線請求。但實際狀況下,應用實例可能異常崩潰,又或者是網絡異常等緣由,致使下線請求沒法被成功提交。app

介於這種狀況,經過 Eureka-Client 心跳延長租約,配合 Eureka-Server 清理超時的租約解決上述異常。dom

3. EvictionTask

com.netflix.eureka.registry.AbstractInstanceRegistry.EvictionTask,清理租約過時任務。在 Eureka-Server 啓動時,初始化 EvictionTask 定時執行,實現代碼以下:ide

// AbstractInstanceRegistry.java
/**
* 清理租約過時任務
*/
private final AtomicReference<EvictionTask> evictionTaskRef = new AtomicReference<EvictionTask>();

protected void postInit() {
   // .... 省略無關代碼

   // 初始化 清理租約過時任務
   if (evictionTaskRef.get() != null) {
       evictionTaskRef.get().cancel();
   }
   evictionTaskRef.set(new EvictionTask());
   evictionTimer.schedule(evictionTaskRef.get(),
           serverConfig.getEvictionIntervalTimerInMs(),
           serverConfig.getEvictionIntervalTimerInMs());
}
  • 配置 eureka.evictionIntervalTimerInMs ,清理租約過時任務執行頻率,單位:毫秒。默認,60000 毫秒。

  • EvictionTask 實現代碼以下:

    class EvictionTask extends TimerTask {
    
       @Override
       public void run() {
           try {
               // 獲取 補償時間毫秒數
               long compensationTimeMs = getCompensationTimeMs();
               logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
               // 清理過時租約邏輯
               evict(compensationTimeMs);
           } catch (Throwable e) {
               logger.error("Could not run the evict task", e);
           }
       }
    
    }
    • 調用 #compensationTimeMs() 方法,得到補償時間毫秒數。計算公式 = 當前時間 - 最後任務執行時間 - 任務執行頻率。爲何須要補償時間毫秒數,在 「4. 過時邏輯」Lease#isisExpired(additionalLeaseMs) 方法 揭曉。#compensationTimeMs() 實現代碼以下:

      /**
      * 最後任務執行時間
      */
      private final AtomicLong lastExecutionNanosRef = new AtomicLong(0L);
      
      long getCompensationTimeMs() {
          long currNanos = getCurrentTimeNano();
          long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
          if (lastNanos == 0L) {
              return 0L;
          }
          long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
          long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
          return compensationTime <= 0L ? 0L : compensationTime;
      }
      • 因爲 JVM GC ,又或是時間偏移( clock skew ) 等緣由,定時器執行實際比預期會略有延遲。筆者在本機低負載運行,大概 10 ms 內。

        compute a compensation time defined as the actual time this task was executed since the prev iteration, vs the configured amount of time for execution. This is useful for cases where changes in time (due to clock skew or gc for example) causes the actual eviction task to execute later than the desired time according to the configured cycle.

    • 調用 #evict(compensationTime) 方法,執行清理過時租約邏輯,在 「4. 過時邏輯」 詳細解析。

4. 過時邏輯

調用 #evict(compensationTime) 方法,執行清理過時租約邏輯,實現代碼以下:

1: public void evict(long additionalLeaseMs) {
  2:     logger.debug("Running the evict task");
  3: 
  4:     if (!isLeaseExpirationEnabled()) {
  5:         logger.debug("DS: lease expiration is currently disabled.");
  6:         return;
  7:     }
  8: 
  9:     // 得到 全部過時的租約
 10:     // We collect first all expired items, to evict them in random order. For large eviction sets,
 11:     // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
 12:     // the impact should be evenly distributed across all applications.
 13:     List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
 14:     for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
 15:         Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
 16:         if (leaseMap != null) {
 17:             for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
 18:                 Lease<InstanceInfo> lease = leaseEntry.getValue();
 19:                 if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) { // 過時
 20:                     expiredLeases.add(lease);
 21:                 }
 22:             }
 23:         }
 24:     }
 25: 
 26:     // 計算 最大容許清理租約數量
 27:     // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
 28:     // triggering self-preservation. Without that we would wipe out full registry.
 29:     int registrySize = (int) getLocalRegistrySize();
 30:     int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
 31:     int evictionLimit = registrySize - registrySizeThreshold;
 32: 
 33:     // 計算 清理租約數量
 34:     int toEvict = Math.min(expiredLeases.size(), evictionLimit);
 35:     if (toEvict > 0) {
 36:         logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
 37: 
 38:         // 逐個過時
 39:         Random random = new Random(System.currentTimeMillis());
 40:         for (int i = 0; i < toEvict; i++) {
 41:             // Pick a random item (Knuth shuffle algorithm)
 42:             int next = i + random.nextInt(expiredLeases.size() - i);
 43:             Collections.swap(expiredLeases, i, next);
 44:             Lease<InstanceInfo> lease = expiredLeases.get(i);
 45: 
 46:             String appName = lease.getHolder().getAppName();
 47:             String id = lease.getHolder().getId();
 48:             EXPIRED.increment();
 49:             logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
 50:             internalCancel(appName, id, false);
 51:         }
 52:     }
 53: }
  • 第 3 至 7 行 :判斷容許執行清理過時租約邏輯,主要和自我保護機制有關,在 《Eureka 源碼解析 —— 應用實例註冊發現(四)之自我保護機制》 有詳細解析。

  • 第 9 至 24 行 :得到全部過時的租約集合。

    • 第 19 行 :調用 Lease#isisExpired(additionalLeaseMs) 方法,判斷租約是否過時,實現代碼以下:

      // Lease.java
      public boolean isExpired(long additionalLeaseMs) {
         return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
      }
      
      public void renew() {
         lastUpdateTimestamp = System.currentTimeMillis() + duration;
      }
      • 😈注意:在不考慮 additionalLeaseMs 參數的狀況下,租約過時時間比預期多了一個 duration,緣由在於 #renew() 方法錯誤的設置 lastUpdateTimestamp = System.currentTimeMillis() + duration,正確的設置應該是 lastUpdateTimestamp = System.currentTimeMillis()

        Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to +duration more than what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will not be fixed.

      • TODO[0023]:additionalLeaseMs

  • 第 26 至 34 行 :計算最大容許清理租約的數量,後計算容許清理租約的數量。

    • 😈注意:即便 Eureka-Server 關閉自我保護機制,若是使用renewalPercentThreshold = 0.85 默認配置,結果會是分批逐步過時。舉個例子:

      // 假設 20 個租約,其中有 10 個租約過時。
      
      // 第一輪執行開始
      int registrySize = 20;
      int registrySizeThreshold = (int) (20 * 0.85) = 17;
      int evictionLimit = 20 - 17 = 3;
      int toEvict = Math.min(10, 3) = 3;
      // 第一輪執行結束,剩餘 17 個租約,其中有 7 個租約過時。
      
      // 第二輪執行開始
      int registrySize = 17;
      int registrySizeThreshold = (int) (17 * 0.85) = 14;
      int evictionLimit = 17 - 14 = 3;
      int toEvict = Math.min(7, 3) = 3;
      // 第二輪執行結束,剩餘 14 個租約,其中有 4 個租約過時。
      
      // 第三輪執行開始
      int registrySize = 14;
      int registrySizeThreshold = (int) (14 * 0.85) = 11;
      int evictionLimit = 14 - 11 = 3;
      int toEvict = Math.min(4, 3) = 3;
      // 第三輪執行結束,剩餘 11 個租約,其中有 1 個租約過時。
      
      // 第四輪執行開始
      int registrySize = 11;
      int registrySizeThreshold = (int) (11 * 0.85) = 9;
      int evictionLimit = 11 - 9 = 2;
      int toEvict = Math.min(1, 2) = 1;
      // 第四輪執行結束,剩餘 10 個租約,其中有 0 個租約過時。結束。
      • 結論:是否開啓自我保護的差異,在因而否執行清理過時租約邏輯。若是想關閉分批逐步過時,設置 renewalPercentThreshold = 0
    • 因爲 JVM GC ,或是本地時間差別緣由,可能自我保護機制的閥值 expectedNumberOfRenewsPerMinnumberOfRenewsPerMinThreshold 不夠正確,在過時這個相對「危險」的操做,從新計算自我保護的閥值。

  • 第 35 至 51 行 :隨機清理過時的租約。因爲租約是按照應用順序添加到數組,經過隨機的方式,儘可能避免單個應用被所有過時

  • 第 50 行 :調用 #internalCancel() 方法,下線已過時的租約,在 《Eureka 源碼解析 —— 應用實例註冊發現(四)之自我保護機制》「3.2 下線應用實例信息」 有詳細解析。

666. 彩蛋

知識星球

😫 本來以爲比較容易的一篇文章,結果消耗了比想象中的時間,可能有四個小時。主要卡在補償時間,目前也沒弄懂。若是有知道的胖友,麻煩告知下。

胖友,分享個人公衆號( 芋道源碼 ) 給你的胖友可好?

相關文章
相關標籤/搜索