我也是醉了,Eureka 延遲註冊還有這個坑!

2022-07-25 18:00:18

Eureka 有個延遲註冊的功能,也就是在服務啟動成功之後不立刻註冊到 Eureka Server,而是延遲一段時間再去註冊,這樣做的主要目的是因為雖然服務啟動成功了,可能還有一些框架或者業務的程式碼沒有初始化完成,可能會導致呼叫的報錯,所以需要延遲註冊。

但是發現,然並卵啊,好像這個延遲註冊並沒有生效,也是開始了排查之路。

延遲註冊

首先,延遲註冊的功能主要依賴這兩個引數,eureka.client.initial-instance-info-replication-interval-seconds代表第一次初始化延遲註冊的時間間隔,eureka.client.instance-info-replication-interval-seconds則代表後續同步註冊的時間間隔。

eureka.client.initial-instance-info-replication-interval-seconds=40 //預設40秒
eureka.client.instance-info-replication-interval-seconds=30 //預設30秒

我們從原始碼先來看是怎麼做到延遲註冊的,先看 DiscoveryClientinitScheduledTasks ,這裡建立了同步註冊到 Eureka Server 的定時任務。

之後呼叫 start 方法建立定時任務,並且延遲 40 秒執行,也就是我們達到的延遲註冊的效果。

預設的第一次註冊,也就是延遲註冊的時間是 40 秒,之後每 30 秒會同步註冊資訊。

但是,即便我們設定了這倆屬性,發現好像沒什麼卵用,接下來我們要排查下到底是為啥捏?

第一個問題

我發現在 InstanceInfoReplica 中存在這樣一段終止當前執行緒池任務,並且直接呼叫 run 方法的存在,猜測失效就是他直接呼叫導致延遲任務沒有生效,因為這個方法的直接呼叫導致延遲註冊壓根就沒效果嘛。

看起來他存在兩個呼叫,第一個是registerHealthCheck,當存在這個健康檢查什麼玩意兒的時候就會去呼叫onDemandUpdate

經過排查我們發現,只要設定了eureka.client.healthcheck.enabled=true,就會建立 HealthCheckHandler的範例出來,預設情況下他是false的,所以應該是對我們沒有影響的。

這裡需要特別說明一下 eureka.client.healthcheck.enabled 的作用,預設 Eureka 根據心跳來決定應用的狀態,如果是這個屬性設定成 true的話,則是會根據 Spring Boot Actuator 來決定,而不是心跳了。

比如我們可以實現 HealthIndicator介面,自己寫一個Controller來動態改變服務的狀態

@RestController
public class ControllerTest {
    @Autowired
    private HealthChecker healthChecker;

    @RequestMapping("/change")
    public String test(Boolean flag) {
        healthChecker.setUp(new AtomicBoolean(flag));
        return "success";
    }

}

實現HealthChecker,這樣會發現啟動、下線服務 Eureka Server 的狀態不會變成 Down,只有通過呼叫介面手動改變應用狀態 Server 的狀態才會發生改變,大家可以自行測試。

@Component
public class HealthChecker extends EurekaHealthIndicator implements HealthIndicator {
    private AtomicBoolean up = new AtomicBoolean(true);

    public HealthChecker(EurekaClient eurekaClient, EurekaInstanceConfig instanceConfig, EurekaClientConfig clientConfig) {
        super(eurekaClient, instanceConfig, clientConfig);
    }

    @Override
    public Health health() {
        if(up.get()){
            return Health.up().build();
        }else{
            return Health.down().build();
        }
    }

第二個問題

第一個問題我們找到了,發現他不是導致我們問題的根因,於是繼續排查。

發現第二個呼叫,在DiscoveryClient註冊了狀態事件變更的監聽,如果狀態發生變更,也會去呼叫 onDemandUpdate ,影響延遲註冊的效果。

這裡存在一個設定項onDemandUpdateStatusChange,預設是true,所以應該是他沒錯了。

進入StatusChangeListener,找到了一個呼叫。

就是通過setInstanceStatus方法觸發的事件通知。

這裡存在 6 個呼叫,一一排查,通過原始碼找啊找,最終定位到服務啟動自動裝配的地方,在這裡去修改服務狀態為 UP,然後觸發事件通知,啟動 start 方法呼叫register方法。

繼續呼叫,修改應用為上線UP狀態。

由此我們知道,只要服務啟動成功,就會觸發事件通知,所以這個基本上是啟動成功立刻就會去註冊到 Eureka Server,這就會導致延遲註冊的失效,從啟動紀錄檔也能直觀的看到這個效果。

驗證

為了驗證我的猜想,我把這兩個設定同時設定成false,並且把延遲註冊的時間調整到非常大。

eureka.client.healthcheck.enabled=false
eureka.client.onDemandUpdateStatusChange=false
eureka.client.initial-instance-info-replication-interval-seconds=9999999 //預設40秒
eureka.client.instance-info-replication-interval-seconds=999999 //預設30秒

但是,但是!!!

發現過了幾十秒之後,還是註冊到 Server 了,真的是醉了。。。

那就繼續看吧。

再看下注冊方法,可能不止一個地方存在呼叫,我們發現果然如此,有 3 個地方都呼叫了註冊方法。

第一個呼叫在DiscoveryClient注入的時候,這個看了下,clientConfig.shouldEnforceRegistrationAtInit()預設是false,方法不會進來,不管他了。

那麼繼續看第二個呼叫,第二個呼叫你看renew方法,這一看我們就知道了,這不就是心跳嗎?!

傳送心跳如果返回NOT_FOUND,就會去註冊了啊。

感覺已經接近真相了,去找下 Server 心跳的原始碼,根據呼叫的路徑找到原始碼位於InstanceResource中。

可以看到第一次註冊的時候從登入檔拿到的範例資訊是空的,所以直接返回了 false,就會返回 NOT FOUND 了。

registry.renew方法,最終會呼叫到AbstractInstanceRegistry中,初始化的時候登入檔registry肯定沒有當前範例的資訊,所以拿到是空的,返回了false,最終就返回了NOT_FOUND

因此,雖然我們把這兩個引數都設定成了false,但是由於心跳預設 30 秒一次,所以最終我們發現設定的超級大的延遲註冊的時間並沒有完全生效。

總結

OK,到此,延遲註冊不生效的原因找到了,我們做一個總結。

預設情況下,設定了延遲註冊的時間並不會生效,因為事件監聽預設是true,服務啟動之後就會立刻註冊到 Eureka Server。

如果需要延遲註冊生效,必須 eureka.client.healthcheck.enabled eureka.client.onDemandUpdateStatusChange 都為false

即便我們把所有途徑都封死了,但是傳送心跳的執行緒仍然會去註冊,所以這個延遲註冊的時間最多也不會超過 30 秒,即便設定的延遲時間超過 30 秒。

OK,到此為止,結束,我是艾小仙,歡迎拍磚。