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

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

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

  • 1. 概述
  • 2. 爲什麼需要過期
  • 3. EvictionTask
  • 4. 過期邏輯

  • 1. 概述

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

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

    推薦 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 下線應用實例信息」 有詳細解析。
發佈了22 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章