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

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秒

我們從源碼先來看是怎麼做到延遲註冊的,先看 DiscoveryClient 的 initScheduledTasks ,這裡創建了同步註冊到 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,到此為止,結束,我是艾小仙,歡迎拍磚。