前言
前情回顧
上一講主要講了服務下線,已經註冊中心自動感知宕機的服務。
其實上一講已經包含了很多EurekaServer自我保護的代碼,其中還發現了1.7.x(1.9.x)包含的一些bug,但這些問題在master分支都已修復了。
服務下線會將服務實例從註冊表中刪除,然後放入到recentQueue中,下次其他EurekaClient來進行註冊表抓取的時候就能感知到對應的哪些服務下線了。
自動感知服務實例宕機不會調用下線的邏輯,所以我們還拋出了一個問題,一個client宕機,其他的client需要多久才能感知到?通過源碼我們知道 至少要180s 才能被註冊中心給摘除,也就是最快180s才能被其他服務感知,因爲這裏還涉及讀寫緩存和只讀緩存不一致的情況。
本講目錄
本講主要講解註冊中心一個獨有的功能,如果使用Eureka作爲註冊中心的小夥伴可能都看過註冊中心Dashboard上會有這麼一段文字:
那註冊中心爲何要做這種自我保護呢?這裏要跟註冊中心的設計思想相關聯了,我們知道Eureka是一個高可用的組件,符合CAP架構中的A、P,如果註冊中心檢測到很多服務實例宕機的時候,它不會將這些宕機的數據全都剔除,會做一個判斷,如果宕機的服務實例大於所有實例數的15%,那麼就會開啓保護模式,不會摘除任何實例(代碼中是通過每分鐘所有實例心跳總數和期望實例心跳總數對比)。
試想,如果沒有自我保護機制,註冊中心因爲網絡故障,收不到其他服務實例的續約 而誤將這些服務實例都剔除了,是不是就出大問題了。
目錄如下:
evict()
方法解讀expectedNumberOfRenewsPerMin
計算方式expectedNumberOfRenewsPerMin
自動更新機制- 註冊中心
Dashboard
顯示自我保護頁面實現 - 自我保護機制bug彙總
技術亮點:
- 如何計算每一分鐘內的內存中的計數呢?
MeassuredRate
計算每一分鐘內的心跳的次數,保存上一分鐘心跳次數和當前分鐘的心跳次數 後面我們會看一下這個類似怎麼實現的
說明
原創不易,如若轉載 請標明來源:一枝花算不算浪漫
源碼分析
evict()
方法解讀
接着上一講的內容,上一講其實已經講到了evict()
的使用,我們再來說下如何一步步調入進來的:
EurekaBootStrap.initEurekaServerContext()
中調用registry.openForTraffic()
, 然後進入PeerAwareInstanceRegistryImpl.openForTraffic()
方法,其中有調用super.postInit()
這裏面直接進入到 AbstractInstanceRegistry.postInit()
方法,這裏其實就是一個定時調度任務,默認一分鐘執行一次,這裏會執行EvictionTask
,在這個task裏面會有一個run()
方法,最後就是執行到了evict()
方法了。
這裏再來看下evict()
方法代碼:
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 是否允許主動刪除宕機節點數據,這裏判斷是否進入自我保護機制,如果是自我保護了則不允許摘除服務
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 省略服務摘除等等操作...
}
接着進入PeerAwareInstanceRegistryImpl.isLeaseExpirationEnabled()
:
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
// 這行代碼觸發自我保護機制,期望的一分鐘要有多少次心跳發送過來,所有服務實例一分鐘得發送多少次心跳
// getNumOfRenewsInLastMin 上一分鐘所有服務實例一共發送過來多少心跳,10次
// 如果上一分鐘 的心跳次數太少了(20次)< 我期望的100次,此時會返回false
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
這裏我們先解讀一下,上面註釋已經說得很清晰了。
我們在代碼中可以找到
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
這段的意思
expectedNumberOfRenewsPerMin
代表每分鐘期待的心跳時間,例如現在有100次心跳,然後乘以默認的心跳配比85%,這裏就是nuberOfRenewsPerMinThreshold的含義了如果上一分鐘實際心跳次數小於這個值,那麼就會進入自我保護模式
然後是getNumOfRenewsInLastMin()
:
private final MeasuredRate renewsLastMin;
public long getNumOfRenewsInLastMin() {
return renewsLastMin.getCount();
}
public class MeasuredRate {
private static final Logger logger = LoggerFactory.getLogger(MeasuredRate.class);
private final AtomicLong lastBucket = new AtomicLong(0);
private final AtomicLong currentBucket = new AtomicLong(0);
private final long sampleInterval;
private final Timer timer;
private volatile boolean isActive;
/**
* @param sampleInterval in milliseconds
*/
public MeasuredRate(long sampleInterval) {
this.sampleInterval = sampleInterval;
this.timer = new Timer("Eureka-MeasureRateTimer", true);
this.isActive = false;
}
public synchronized void start() {
if (!isActive) {
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
// Zero out the current bucket.
// renewsLastMin 爲1分鐘
// 每分鐘調度一次,將當前的88次總心跳設置到lastBucket中去,然後將當前的currentBucket 設置爲0 秒啊!
lastBucket.set(currentBucket.getAndSet(0));
} catch (Throwable e) {
logger.error("Cannot reset the Measured Rate", e);
}
}
}, sampleInterval, sampleInterval);
isActive = true;
}
}
public synchronized void stop() {
if (isActive) {
timer.cancel();
isActive = false;
}
}
/**
* Returns the count in the last sample interval.
*/
public long getCount() {
return lastBucket.get();
}
/**
* Increments the count in the current sample interval.
*/
public void increment() {
// 心跳次數+1 例如說1分鐘所有服務實例共發起了88次心跳
currentBucket.incrementAndGet();
}
}
最上面我們說過,MeasuredRate
的設計是一個閃光點,看下重要的兩個屬性:
lastBucket
: 記錄上一分鐘總心跳次數currentBucket
: 記錄當前最近一分鐘總心跳次數
首先我們看下increment()
方法,這裏看一下調用會發現在服務端處理續約renew()
中的最後會調用此方法,使得currentBucket
進行原子性的+1操作。
然後這裏明有一個start()
方法,這裏面也是個時間調度任務,我們可以看下sampleInterval
這個時間戳,在構造函數中被賦值,在AbstractInstanceRegistry
的構造方法中被調用,默認時間爲一分鐘。
這裏最重要的是lastBucket.set(currentBucket.getAndSet(0));
每分鐘調度一次,把當前一分鐘總心跳時間賦值給上一分鐘總心跳時間,然後將當前一分鐘總心跳時間置爲0.
expectedNumberOfRenewsPerMin
計算方式
我們上一講中已經介紹過expectedNumberOfRenewsPerMin
的計算方式,因爲這個屬性很重要,所以這裏再深入研究一下。
首先我們要理解這個屬性的含義:期待的一分鐘註冊中心接收到的總心跳時間,接着看看哪幾個步驟會更新:
- EurekaServer初始的時候會計算
在openForTraffic()
方法的入口會有計算 - 服務註冊調用
register()
方法是會更新 - 服務下線調用
cancel()
方法時會更新 - 服務剔除
evict()
也應該調用,可惜是代碼中並未找到調用的地方?這裏其實是個bug,我們可以看後面自我保護機制Bug彙總
中提到更多詳細內容。此問題至今未修復,我們先繼續往後看。
expectedNumberOfRenewsPerMin
自動更新機制
Server端初始化上下文的時候,15分鐘跑的一次定時任務:
scheduleRenewalThresholdUpdateTask
入口是:EurekaBootStrap.initEurekaServerContext()
方法,然後執行serverContext.initialize()
方法,裏面的registry.init()
執行PeerAwareInstanceRegistryImpl.init()
中會執行scheduleRenewalThresholdUpdateTask()
,這個調度任務默認是每15分鐘執行一次的,來看下源代碼:
private void updateRenewalThreshold() {
try {
// count爲註冊表中服務實例的個數
// 將自己作爲eureka client,從其他eureka server拉取註冊表
// 合併到自己本地去 將從別的eureka server拉取到的服務實例的數量作爲count
Applications apps = eurekaClient.getApplications();
int count = 0;
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
if (this.isRegisterable(instance)) {
++count;
}
}
}
synchronized (lock) {
// Update threshold only if the threshold is greater than the
// current expected threshold of if the self preservation is disabled.
// 這裏也是存在bug的,master分支已經修復
// 一分鐘服務實例心跳個數(其他eureka server拉取的服務實例個數 * 2) > 自己本身一分鐘所有服務實例實際心跳次數 * 0.85(閾值)
// 這裏主要是跟其他的eureka server去做一下同步
if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold)
|| (!this.isSelfPreservationModeEnabled())) {
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold = (int) ((count * 2) * serverConfig.getRenewalPercentThreshold());
}
}
logger.info("Current renewal threshold is : {}", numberOfRenewsPerMinThreshold);
} catch (Throwable e) {
logger.error("Cannot update renewal threshold", e);
}
}
這裏需要注意一點,爲何上面說eurekaClient.getApplications()
是從別的註冊中心獲取註冊表實例信息,因爲一個eurekaServer對於其他註冊中心來說也是一個eurekaClient。
這裏註釋已經寫得很清晰了,就不再多囉嗦了。
註冊中心Dashboard
顯示自我保護頁面實現
還是自己先找到對應jsp看看具體代碼實現:
這裏主要是看:registry.isBelowRenewThresold()
邏輯。
PeerAwareInstanceRegistryImpl.isBelowRenewThresold()
:
public int isBelowRenewThresold() {
if ((getNumOfRenewsInLastMin() <= numberOfRenewsPerMinThreshold)
&&
((this.startupTime > 0) && (System.currentTimeMillis() > this.startupTime + (serverConfig.getWaitTimeInMsWhenSyncEmpty())))) {
return 1;
} else {
return 0;
}
}
這裏的意思就是 上一分鐘服務實例實際總心跳個數 <= 一分鐘期望的總心跳實例 * 85%,而且判斷 Eureka-Server 是否允許被 Eureka-Client 獲取註冊信息。如果都滿足的話就會返回1,當前警告信息就會在dashbord上顯示自我保護的提示了。
這裏面注意一下配置:
#getWaitTimeInMsWhenSyncEmpty()
:Eureka-Server 啓動時,從遠程 Eureka-Server 讀取不到註冊信息時,多長時間不允許 Eureka-Client 訪問,默認是5分鐘
自我保護機制bug彙總
- expectedNumberOfRenewsPerMin計算方式
this.expectedNumberOfRenewsPerMin = count * 2;
// numberOfRenewsPerMinThreshold = count * 2 * 0.85 = 34 期望一分鐘 20個服務實例,得有34個心跳
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
這裏爲何要使用count * 2?count是註冊表中所有的註冊實例的數量,因爲作者以爲用戶不會修改默認續約時間(30s), 所以理想的認爲這裏應該乘以2就是一分鐘得心跳總數了。
好在看了master 分支此問題已經修復。如下圖:
- 同理 服務註冊、服務下線 都是將
註冊:expectedNumberOfRenewsPerMin+2
下線:expectedNumberOfRenewsPerMin-2
master分支也給予修復,圖片如下:
服務註冊:
服務下線:
evict()
方法爲何不更新expectedNumberOfRenewsPerMin
按常理來說這裏也應該進行 -2操作的,實際上並沒有更新,於是看了下master分支源碼仍然沒有更新,於是早上我便在netflix eureka
git
上提了一個isssue:(我蹩腳的英語大家就不要吐槽了,哈哈哈)
地址爲:Where to update the "expectedNumberOfClientsSendingRenews" when we evict a instance?
疑問:
搜索了github 發現也有人在2017年就遇到了這個問題,從最後一個回答來看這個問題依然沒有解決:
Eureka seems to do not recalculate numberOfRenewsPerMinThreshold during evicting expired leases
翻譯如下:
總結
一張圖代爲總結一下:
申明
本文章首發自本人博客:https://www.cnblogs.com/wang-meng 和公衆號:壹枝花算不算浪漫,如若轉載請標明來源!
感興趣的小夥伴可關注個人公衆號:壹枝花算不算浪漫