註冊中心 Eureka 原始碼解析 —— 應用例項註冊發現(五)之過期

芋道原始碼_以德服人_不服就幹發表於2018-05-25

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

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


註冊中心 Eureka 原始碼解析 —— 應用例項註冊發現(五)之過期

???關注**微信公眾號:【芋道原始碼】**有福利:

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

1. 概述

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

FROM 《深度剖析服務發現元件Netflix Eureka》

註冊中心 Eureka 原始碼解析 —— 應用例項註冊發現(五)之過期

推薦 Spring Cloud 書籍

推薦 Spring Cloud 視訊

2. 為什麼需要過期

正常情況下,應用例項下線時候會主動向 Eureka-Server 發起下線請求。但實際情況下,應用例項可能異常崩潰,又或者是網路異常等原因,導致下線請求無法被成功提交。

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

3. EvictionTask

com.netflix.eureka.registry.AbstractInstanceRegistry.EvictionTask,清理租約過期任務。在 Eureka-Server 啟動時,初始化 EvictionTask 定時執行,實現程式碼如下:

// 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. 彩蛋

知識星球

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

胖友,分享我的公眾號( 芋道原始碼 ) 給你的胖友可好?

相關文章