1. 程式人生 > >【一起學原始碼-微服務】Nexflix Eureka 原始碼十一:EurekaServer自我保護機制竟然有這麼多Bug?

【一起學原始碼-微服務】Nexflix Eureka 原始碼十一:EurekaServer自我保護機制竟然有這麼多Bug?

前言

前情回顧

上一講主要講了服務下線,已經註冊中心自動感知宕機的服務。
其實上一講已經包含了很多EurekaServer自我保護的程式碼,其中還發現了1.7.x(1.9.x)包含的一些bug,但這些問題在master分支都已修復了。

服務下線會將服務例項從登錄檔中刪除,然後放入到recentQueue中,下次其他EurekaClient來進行登錄檔抓取的時候就能感知到對應的哪些服務下線了。

自動感知服務例項宕機不會呼叫下線的邏輯,所以我們還丟擲了一個問題,一個client宕機,其他的client需要多久才能感知到?通過原始碼我們知道 至少要180s 才能被註冊中心給摘除,也就是最快180s才能被其他服務感知,因為這裡還涉及讀寫快取和只讀快取不一致的情況。

本講目錄

本講主要講解註冊中心一個獨有的功能,如果使用Eureka作為註冊中心的小夥伴可能都看過註冊中心Dashboard上會有這麼一段文字:

那註冊中心為何要做這種自我保護呢?這裡要跟註冊中心的設計思想相關聯了,我們知道Eureka是一個高可用的元件,符合CAP架構中的A、P,如果註冊中心檢測到很多服務例項宕機的時候,它不會將這些宕機的資料全都剔除,會做一個判斷,如果宕機的服務例項大於所有例項數的15%,那麼就會開啟保護模式,不會摘除任何例項(程式碼中是通過每分鐘所有例項心跳總數和期望例項心跳總數對比)。

試想,如果沒有自我保護機制,註冊中心因為網路故障,收不到其他服務例項的續約 而誤將這些服務例項都剔除了,是不是就出大問題了。

目錄如下:

  1. evict()方法解讀
  2. expectedNumberOfRenewsPerMin計算方式
  3. expectedNumberOfRenewsPerMin自動更新機制
  4. 註冊中心Dashboard顯示自我保護頁面實現
  5. 自我保護機制bug彙總

技術亮點:

  1. 如何計算每一分鐘內的記憶體中的計數呢?
    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;
}

這裡我們先解讀一下,上面註釋已經說得很清晰了。

  1. 我們在程式碼中可以找到this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());

  2. 這段的意思expectedNumberOfRenewsPerMin 代表每分鐘期待的心跳時間,例如現在有100次心跳,然後乘以預設的心跳配比85%,這裡就是nuberOfRenewsPerMinThreshold的含義了

  3. 如果上一分鐘實際心跳次數小於這個值,那麼就會進入自我保護模式

然後是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的設計是一個閃光點,看下重要的兩個屬性:

  1. lastBucket: 記錄上一分鐘總心跳次數
  2. currentBucket: 記錄當前最近一分鐘總心跳次數

首先我們看下increment()方法,這裡看一下呼叫會發現在服務端處理續約renew()中的最後會呼叫此方法,使得currentBucket進行原子性的+1操作。

然後這裡明有一個start()方法,這裡面也是個時間排程任務,我們可以看下sampleInterval這個時間戳,在建構函式中被賦值,在AbstractInstanceRegistry的構造方法中被呼叫,預設時間為一分鐘。

這裡最重要的是lastBucket.set(currentBucket.getAndSet(0)); 每分鐘排程一次,把當前一分鐘總心跳時間賦值給上一分鐘總心跳時間,然後將當前一分鐘總心跳時間置為0.

expectedNumberOfRenewsPerMin計算方式

我們上一講中已經介紹過expectedNumberOfRenewsPerMin的計算方式,因為這個屬性很重要,所以這裡再深入研究一下。

首先我們要理解這個屬性的含義:期待的一分鐘註冊中心接收到的總心跳時間,接著看看哪幾個步驟會更新:

  1. EurekaServer初始的時候會計算
    openForTraffic() 方法的入口會有計算
  2. 服務註冊呼叫register()方法是會更新
  3. 服務下線呼叫cancel()方法時會更新
  4. 服務剔除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彙總

  1. 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 分支此問題已經修復。如下圖:

  1. 同理 服務註冊、服務下線 都是將

註冊:expectedNumberOfRenewsPerMin+2
下線:expectedNumberOfRenewsPerMin-2

master分支也給予修復,圖片如下:
服務註冊:

服務下線:

  1. 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 和公眾號:壹枝花算不算浪漫,如若轉載請標明來源!

感興趣的小夥伴可關注個人公眾號:壹枝花算不算浪漫

相關推薦

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer自我保護機制竟然這麼Bug

前言 前情回顧 上一講主要講了服務下線,已經註冊中心自動感知宕機的服務。 其實上一講已經包含了很多EurekaServer自我保護的程式碼,其中還發現了1.7.x(1.9.x)包含的一些bug,但這些問題在master分支都已修復了。 服務下線會將服務例項從登錄檔中刪除,然後放入到recentQueue中,下

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer啟動之配置檔案載入以及面向介面的配置項讀取

前言 上篇文章已經介紹了 為何要讀netflix eureka原始碼了,這裡就不再概述,下面開始正式原始碼解讀的內容。 如若轉載 請標明來源:一枝花算不算浪漫 程式碼總覽 還記得上文中,我們通過web.xml找到了eureka server入口的類EurekaBootStrap,這裡我們就先來簡單地看下: /

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer啟動之EurekaServer上下文EurekaClient建立

前言 上篇文章已經介紹了 Eureka Server 環境和上下文初始化的一些程式碼,其中重點講解了environment初始化使用的單例模式,以及EurekaServerConfigure基於介面對外暴露配置方法的設計方式。這一講就是講解Eureka Server上下文初始化剩下的內容:Eureka Cli

一起原始碼-服務Nexflix Eureka 原始碼在眼花繚亂的程式碼中,EurekaClient是如何註冊的?

前言 上一講已經講解了EurekaClient的啟動流程,到了這裡已經有6篇Eureka原始碼分析的文章了,看了下之前的文章,感覺程式碼成分太多,會影響閱讀,後面會只擷取主要的程式碼,加上註釋講解。 這一講看的是EurekaClient註冊的流程,當然也是一塊核心,標題為什麼會寫上眼花繚亂呢?關於Eureka

一起原始碼-服務Nexflix Eureka 原始碼通過單元測試來Debug Eureka註冊過程

前言 上一講eureka client是如何註冊的,一直跟到原始碼傳送http請求為止,當時看eureka client註冊時如此費盡,光是找一個regiter的地方就找了半天,那麼client端傳送了http請求給server端,server端是如何處理的呢? 帶著這麼一個疑問 就開始今天原始碼的解讀了。

一起原始碼-服務Nexflix Eureka 原始碼EurekaClient登錄檔抓取 精妙設計分析!

前言 前情回顧 上一講 我們通過單元測試 來梳理了EurekaClient是如何註冊到server端,以及server端接收到請求是如何處理的,這裡最重要的關注點是登錄檔的一個數據結構:ConcurrentHashMap<String, Map<String, Lease<InstanceI

一起原始碼-服務Nexflix Eureka 原始碼服務續約原始碼分析

前言 前情回顧 上一講 我們講解了服務發現的相關邏輯,所謂服務發現 其實就是登錄檔抓取,服務例項預設每隔30s去註冊中心抓取一下注冊表增量資料,然後合併本地登錄檔資料,最後有個hash對比的操作。 本講目錄 今天主要是看下服務續約的邏輯,服務續約就是client端給server端傳送心跳檢測,告訴對方我還活著

一起原始碼-服務Nexflix Eureka 原始碼服務下線及例項摘除,一個client下線到底多久才會被其他例項感知?

前言 前情回顧 上一講我們講了 client端向server端傳送心跳檢查,也是預設每30鍾傳送一次,server端接收後會更新登錄檔的一個時間戳屬性,然後一次心跳(續約)也就完成了。 本講目錄 這一篇有兩個知識點及一個疑問,這個疑問是在工作中真真實實遇到過的。 例如我有服務A、服務B,A、B都註冊在同一個註

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer叢集模式原始碼分析

前言 前情回顧 上一講看了Eureka 註冊中心的自我保護機制,以及裡面提到的bug問題。 哈哈 轉眼間都2020年了,這個系列的文章從12.17 一直寫到現在,也是不容易哈,每天持續不斷學習,輸出部落格,這一段時間確實收穫很多。 今天在公司給組內成員分享了Eureka原始碼剖析,反響效果還可以,也算是感覺收

一起原始碼-服務Nexflix Eureka 原始碼十三Eureka原始碼解讀完結撒花篇~!

前言 想說的話 【一起學原始碼-微服務-Netflix Eureka】專欄到這裡就已經全部結束了。 實話實說,從最開始Eureka Server和Eureka Client初始化的流程還是一臉悶逼,到現在Eureka各種操作都瞭然於心了。 本專欄從12.17開始寫,一直到今天12.30(文章在平臺是延後釋出的

一起原始碼-服務Ribbon 原始碼Ribbon概念理解及Demo除錯

前言 前情回顧 前面文章已經梳理清楚了Eureka相關的概念及原始碼,接下來開始研究下Ribbon的實現原理。 我們都知道Ribbon在spring cloud中擔當負載均衡的角色, 當兩個Eureka Client互相呼叫的時候,Ribbon能夠做到呼叫時的負載,保證多節點的客戶端均勻接收請求。(這個有點類

一起原始碼-服務Ribbon 原始碼通過Debug找出Ribbon初始化流程及ILoadBalancer原理分析

前言 前情回顧 上一講講了Ribbon的基礎知識,通過一個簡單的demo看了下Ribbon的負載均衡,我們在RestTemplate上加了@LoadBalanced註解後,就能夠自動的負載均衡了。 本講目錄 這一講主要是繼續深入RibbonLoadBalancerClient和Ribbon+Eureka整合的

一起原始碼-服務Ribbon 原始碼Ribbon與Eureka整合原理分析

前言 前情回顧 上一篇講了Ribbon的初始化過程,從LoadBalancerAutoConfiguration 到RibbonAutoConfiguration 再到RibbonClientConfiguration,我們找到了ILoadBalancer預設初始化的物件等。 本講目錄 這一講我們會進一步往下

一起原始碼-服務Ribbon 原始碼進一步探究Ribbon的IRule和IPing

前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進

一起原始碼-服務Ribbon原始碼Ribbon原始碼解讀彙總篇~

前言 想說的話 【一起學原始碼-微服務-Ribbon】專欄到這裡就已經全部結束了,共更新四篇文章。 Ribbon比較小巧,這裡是直接 讀的spring cloud 內嵌封裝的版本,裡面的各種configuration確實有點繞,不過看看第三講Ribbon初始化的過程總結圖就會清晰很多。 緊接著會繼續整理學習F

一起原始碼-服務Feign 原始碼原始碼初探,通過Demo Debug Feign原始碼

前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進

一起原始碼-服務Feign 原始碼Feign動態代理構造過程

前言 前情回顧 上一講主要看了@EnableFeignClients中的registerBeanDefinitions()方法,這裡面主要是 將EnableFeignClients註解對應的配置屬性注入,將FeignClient註解對應的屬性注入。 最後是生成FeignClient對應的bean,注入到Spr

一起原始碼-服務Feign 原始碼Feign結合Ribbon實現負載均衡的原理分析

前言 前情回顧 上一講我們已經知道了Feign的工作原理其實是在專案啟動的時候,通過JDK動態代理為每個FeignClinent生成一個動態代理。 動態代理的資料結構是:ReflectiveFeign.FeignInvocationHandler。其中包含target(裡面是serviceName等資訊)和d

一起原始碼-服務Hystrix 原始碼Hystrix基礎原理與Demo搭建

說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一個系列文章講解了Feign的原始碼,主要是Feign動態代理實現的原理,及配合Ribbon實現負載均衡的機制。 這裡我們講解一個新的元件Hystrix,也是和Fe

一起原始碼-服務Hystrix 原始碼Hystrix核心流程Hystix非降級邏輯流程梳理

說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一講我們講了配置了feign.hystrix.enabled=true之後,預設的Targeter就會構建成HystrixTargter, 然後通過對應的Hystr