深入理解Eureka自我保護機制(五)
為什麼要有自我保護機制
眾所周知,Eureka在CAP理論當中是屬於AP , 也就說當產生網路分割槽時,Eureka保證系統的可用性,但不保證系統裡面資料的一致性, 舉個例子。當發生網路分割槽的時候,Eureka-Server和client端的通訊被終止,server端收不到大部分的client的續約,這個時候,如果直接將沒有收到心跳的client端自動剔除,那麼會將可用的client端剔除,這不符合AP理論,所以Eureka寧可保留也許已經宕機了的client端 , 也不願意將可以用的client端一起剔除。 從這一點上,也就保證了Eureka程式的健壯性,符合AP理論
重要變數
this
.expectedNumberOfRenewsPerMin = count * 2; this.numberOfRenewsPerMinThreshold =(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold.expectedNumberOfRenewsPerMin = count * 2; this.numberOfRenewsPerMinThreshold =(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());());
expectedNumberOfRenewsPerMin :每分鐘最大的續約數量,由於客戶端是每30秒續約一次,一分鐘就是續約2次, count代表的是客戶端數量所以這個變數的計算公式 : 客戶端數量*2 numberOfRenewsPerMinThreshold : 每分鐘最小續約數量, 使用expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold()。serverConfig.getRenewalPercentThreshold()的預設值為0.85 , 也就是說每分鐘的續約數量要大於85% 。
Eureka的自我保護機制,都是圍繞這兩個變數來實現的, 如果每分鐘的續約數量小於numberOfRenewsPerMinThreshold , 就會開啟自動保護機制。
在此期間,不會再主動剔除任何一個客戶端。
變數更新
Eureka-Server初始化,cancle主動下線, 客戶端註冊 ,定時器, 這四個場景會更新這兩個變數
Eureka-Server初始化
protected void initEurekaServerContext() throws Exception {
// ....省略N多程式碼
// 服務剛剛啟動的時候,去其他服務節點同步客戶端的數量。
int registryCount = this.registry.syncUp();
// 這個方法裡面計算expectedNumberOfRenewsPerMin的值
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// 此處初始化值,客戶端數量*2
this.expectedNumberOfRenewsPerMin = count * 2;
// serverConfig.getRenewalPercentThreshold() 預設為0.85
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
// ...省略N多程式碼
// 開啟定時清理過期客戶端的定時器
super.postInit();
}
void initEurekaServerContext() throws Exception {
// ....省略N多程式碼
// 服務剛剛啟動的時候,去其他服務節點同步客戶端的數量。
int registryCount = this.registry.syncUp();
// 這個方法裡面計算expectedNumberOfRenewsPerMin的值
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// 此處初始化值,客戶端數量*2
this.expectedNumberOfRenewsPerMin = count * 2;
// serverConfig.getRenewalPercentThreshold() 預設為0.85
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
// ...省略N多程式碼
// 開啟定時清理過期客戶端的定時器
super.postInit();
}
cancle主動下線
@Override
public boolean cancel(final String appName, final String id,
final boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// 重點在這裡,,,,,主動下線的時候,需要去更新每分鐘最大續約數,
// 一個客戶端的每30秒續約一次,一分鐘就是續約兩次,所以需要減2.
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
return true;
}
return false;
}
@Override
public boolean cancel(final String appName, final String id,
final boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// 重點在這裡,,,,,主動下線的時候,需要去更新每分鐘最大續約數,
// 一個客戶端的每30秒續約一次,一分鐘就是續約兩次,所以需要減2.
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
return true;
}
return false;
}
客戶端註冊
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
// ....省略 N多程式碼
if (existingLease != null && (existingLease.getHolder() != null)) {
// ....省略 N多程式碼
} else {
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// 重點在這裡, 註冊一個客戶端,一個客戶端每分鐘需要兩次續約,所以這裡加2
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
// ....省略 N多程式碼
} finally {
read.unlock();
}
}
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
// ....省略 N多程式碼
if (existingLease != null && (existingLease.getHolder() != null)) {
// ....省略 N多程式碼
} else {
synchronized (lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
// 重點在這裡, 註冊一個客戶端,一個客戶端每分鐘需要兩次續約,所以這裡加2
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
}
logger.debug("No previous lease information found; it is new registration");
}
// ....省略 N多程式碼
} finally {
read.unlock();
}
}
定時器
在Eureka-Server啟動的時候,會進行初始化,執行路徑如下:
DefaultEurekaServerContext 》@PostConstruct修飾的initialize()方法》init()
@Override
public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
// .... 省略N多程式碼
// 啟動定時器
scheduleRenewalThresholdUpdateTask();
// .... 省略N多程式碼
}
private void scheduleRenewalThresholdUpdateTask() {
timer.schedule(new TimerTask() {
@Override
public void run() {
updateRenewalThreshold();
}
}, serverConfig.getRenewalThresholdUpdateIntervalMs(),
serverConfig.getRenewalThresholdUpdateIntervalMs());
}
private void updateRenewalThreshold() {
try {
Applications apps = eurekaClient.getApplications();
// 計算有效的應用例項數量
int count = 0;
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
if (this.isRegisterable(instance)) {
++count;
}
}
}
synchronized (lock) {
// 重新計算值
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);
}
}
@Override
public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
// .... 省略N多程式碼
// 啟動定時器
scheduleRenewalThresholdUpdateTask();
// .... 省略N多程式碼
}
private void scheduleRenewalThresholdUpdateTask() {
timer.schedule(new TimerTask() {
@Override
public void run() {
updateRenewalThreshold();
}
}, serverConfig.getRenewalThresholdUpdateIntervalMs(),
serverConfig.getRenewalThresholdUpdateIntervalMs());
}
private void updateRenewalThreshold() {
try {
Applications apps = eurekaClient.getApplications();
// 計算有效的應用例項數量
int count = 0;
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
if (this.isRegisterable(instance)) {
++count;
}
}
}
synchronized (lock) {
// 重新計算值
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);
}
}
renewalThresholdUpdateIntervalMs : 預設為15分鐘serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold 這個地方有個這個比較,當前最小續約數0.85 , 然後呢,count2 要大於他,這個意思,主要是為了防止開啟自我保護機制之後,被定時器重新計算了expectedNumberOfRenewsPerMin 和numberOfRenewsPerMinThreshold 的值
自我保護機制
開啟
定期清理任務的執行緒最終執行的是這個方法,這裡就直接開始講
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 是否需要開啟自我保護機制,如果需要,那麼直接RETURE, 不需要繼續往下執行了
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// ..... 省略N多程式碼,。這下面主要是做服務自動下線的操作的
}
@Override
public boolean isLeaseExpirationEnabled() {
// 是否開啟自我保護機制,這是個配置,預設為true
if (!isSelfPreservationModeEnabled()) {
return true;
}
// 計算是否需要自我保護
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 是否需要開啟自我保護機制,如果需要,那麼直接RETURE, 不需要繼續往下執行了
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// ..... 省略N多程式碼,。這下面主要是做服務自動下線的操作的
}
@Override
public boolean isLeaseExpirationEnabled() {
// 是否開啟自我保護機制,這是個配置,預設為true
if (!isSelfPreservationModeEnabled()) {
return true;
}
// 計算是否需要自我保護
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
從上面可以導,判斷是否開啟自我保護機制,主要在於計算每分鐘最小續約數的值, getNumOfRenewInLastMin()這個獲取的
是每分鐘的續約數量(每個客戶端來續約的時候,都是會更新這個值得,每分鐘重置一次,有執行緒去跑的), 如果每分鐘的
續約數量>最小續約數,則不需要開啟自我保護機制, 如果是小於,那麼就是需要開啟, 所以當返回false的時候,就需要開啟
自我保護機制了。
PS: 其實說白了,自我保護機制,就是在定時任務執行之前,判斷每分鐘的續約數量,然後決定是否繼續執行下去。
因此Eureka Server的過期時間(預設60s) ,客戶端的續約時間(預設30s) , 這個配置最好不要更改,如果更改的話
就會打破自我保護機制的規則。
解除
1.當服務的網路分割槽解除之後,客戶端能夠和服務進行互動時,在續約的時候,更新每分鐘的續約數,當每分鐘的續約數大於
85%時,則自動解除。
2.重啟服務