續租、下線等操作比較直觀,實際上也不復雜。讓我們自己想想它們大概會在服務端有什麼操作。
- renew: 更新Lease的
lastUpdateTimestamp
, 更新一下InstanceInfo的最新狀態。然後呼叫其他同伴節點的renew介面。 - cancel:把lease從registry中移除,設定lease的
evictionTimestamp
,然後設定InstanceInfo為已刪除。然後把lease加入到recentlyChangedQueue
中。最後呼叫同伴節點的cancel介面。
—— 服務端確實做了差不多這些事情。因此為了不影響其他重要事件,這裡不再繼續深入。
自我保護機制
Eureka選擇做一個支援AP的系統(CAP定理)。其實很好理解,當發生大量應用例項不再renew時,服務端認為發生了網路分割槽。Eureka為了保證高可用,不再踢掉已經到期的lease,從而讓依賴於該服務端例項的client端仍然能正常進行服務發現(儘管存在服務例項確實掛掉的可能,即犧牲了一致性)。
在Spring Cloud Eureka的Home頁面上面經常會見到這個警告,就是啟用了自我保護機制。
關於這個問題網上的解釋也很多了,下面這篇文章提供了幾幅有用的圖:
The Mystery of Eureka Self-Preservation
實現細節
這篇文章有所有你想要的。 Eureka 原始碼解析 —— 應用例項註冊發現(四)之自我保護機制
搬過來一些重點:
1. 觸發條件
看程式碼
// PeerAwareInstanceRegistryImpl.java
@Override
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
複製程式碼
當每分鐘心跳次數(
renewsLastMin
) 小於numberOfRenewsPerMinThreshold
時,並且開啟自動保護模式開關(eureka.server.enableSelfPreservation = true
) 時,觸發自動保護機制,不再自動過期租約。
其中:
2. 計算公式
numberOfRenewsPerMinThreshold
=expectedNumberOfRenewsPerMin
* 續租百分比(eureka.server.renewalPercentThreshold
, 預設0.85 )expectedNumberOfRenewsPerMin
= 當前註冊的應用例項數 x 2
為什麼乘以 2:
預設情況下,註冊的應用例項每半分鐘續租一次,那麼一分鐘心跳兩次,因此 x 2 。
這裡有硬編碼的情況,因此修改應用例項的續租頻率會讓計算不太準。不過,自我保護機制我比較懷疑它的重要性,該調還得調。
3. 計算時機
有四個地方會重新計算numberOfRenewsPerMinThreshold
、 expectedNumberOfRenewsPerMin
。
- Eureka-Server 啟動時。先從相鄰節點同步registry中的資訊,得到已註冊的例項數量,然後套公式計算。
- 定時重新計算。計算方法同上類似。頻率由
eureka.server.renewalThresholdUpdateIntervalMs
控制。 - 應用例項註冊時,增加了註冊例項數,所以要重算。
- 例項下線時,同理。
自動清理機制
清理過期lease的也是一個定時任務EvictionTask
,頻率由eureka.server.evictionIntervalTimerInMs
,預設為60秒。
1. 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);
}
}
}
複製程式碼
那個getCompensationTimeMs()
方法的意思是,定時器可能比設定的時間更晚觸發,原因可能是GC等。假設本來要在上一次執行後60s再次觸發,但因為GC在第70秒才被觸發,這時去檢查有沒有lease有沒有超期無renew不能用70s來算,而應該還用60s來算,因為在這10s的GC時間中,很可能此服務端無法處理註冊請求。補償的10s就是這個意思,就是允許例項多超期這10s。
2. 清理邏輯
evict(compensationTime)
又比較長,下面分段分析。
- 判斷是不是啟用了自我保護機制,如是則不再清理。
- 遍歷registry中的所有例項,比較當前時間和
lastUpdatedTime
+duration
+compensationTime
,如果前者大,說明已過期。 - 計算最大可清理的例項數量。不得讓清理後的例項數量低於當前數量 *
eureka.server.renewalPercentThreshold
,預設又是0.85。這個閾值還是很高的,如果有大量例項過期,就需要分多批執行才能清理完。 - 隨機清理過期的租約。由於租約是按照應用順序新增到陣列,通過隨機的方式,儘量避免單個應用被全部過期。
- 最後,呼叫
internalCancel
處理每個要被清理的lease,這個方法就是前面的cancel階段會調的。
這個自動清理似乎沒有告知同伴節點,大家各做各的。可見Eureka的一致性是蠻低的。