1. 程式人生 > >深入理解Eureka自我保護機制(五)

深入理解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.重啟服務

sharedCode原始碼交流群,歡迎喜歡閱讀原始碼的朋友加群,新增下面的微信, 備註”加群“ 。