【SpringBoot】服務對註冊中心的註冊時機

酷酷-發表於2024-05-25

1 前言

我們看過很多的時機,比如服務資料來源的關閉時機服務正式開始處理請求的時機或者Tomcat和SpringBoot的協同、還有 mybatis等一些外掛的入場時機等,這節我們要再看一個時機,就是關於跟註冊中心(Eureka、Nacos)的時機,比如你有沒有思考過:

我服務還沒起來,你就到註冊中心上線了,那請求過來豈不是就亂了,或者我服務要停止了我資料來源都釋放了,你還沒從註冊中心下線,那請求還過來是不是也會亂,所以我們就要看看微服務裡上線或者叫註冊和下線的時機都是什麼時候,做到心中有數。本節我們先看註冊時機。

環境的話,我本地有 Eureka,本節我們就拿 Eureka 註冊中心除錯看看。

服務的話,我有幾個簡單的微服務,都是平時用來除錯的哈

2 註冊時機

先來看個效果,當我的 demo 服務起來後:

問題來了,我怎麼知道它的註冊時機,從哪看呢?我從官網的文件裡看了看:中文官網英文官網,發現它只是從使用方式上介紹了怎麼使用以及使用上的細節,並沒說原理。

那怎麼看呢?那就從服務的日誌以及程式碼的依賴看起,當你看的原始碼多了,大多融合 SpringBoot 的方式都差不多。

找到註冊的 Bean了沒?就是他:EurekaAutoServiceRegistration

@Bean
@ConditionalOnBean({AutoServiceRegistrationProperties.class})
@ConditionalOnProperty(
    value = {"spring.cloud.service-registry.auto-registration.enabled"},
    matchIfMissing = true
)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry, EurekaRegistration registration) {
    return new EurekaAutoServiceRegistration(context, registry, registration);
}

那我們就從 EurekaAutoServiceRegistration 看起,先看看它的類關係:

正如我圖上所聯絡的,這個類有兩個動作來驅動他執行,(1)事件監聽(2)生命週期介面

(1)針對事件監聽,它主要監聽了 WebServerInitializedEvent(Web容器初始化完畢的事件) 和 ContextClosedEvent(上下文關閉事件也就是服務停止的事件) 兩個事件

public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof WebServerInitializedEvent) {
        this.onApplicationEvent((WebServerInitializedEvent)event);
    } else if (event instanceof ContextClosedEvent) {
        this.onApplicationEvent((ContextClosedEvent)event);
    }
}

(2)SmartLifecycle 生命週期介面,有兩個重要的動作就是啟動和停止

我們這裡關注的是服務註冊,也就是關注事件的監聽裡的 WebServerInitializedEvent 事件和生命週期的 start 方法。

那哪個先執行的呢?是生命週期的 start 先執行。我這裡畫個圖簡單回憶下:

看圖哈,都是在重新整理上下文的最後有個 finishRefresh 即結束上下文中的動作。具體可以看我之前的文章哈,就不闡述了。那我們就先看看 EurekaAutoServiceRegistration 的 start 方法:

// EurekaAutoServiceRegistration 的 start 方法
// private AtomicInteger port = new AtomicInteger(0);
public void start() {
    // 這個時候進來 port 還是0
    // 這裡提前告訴你 他是透過監聽事件 當監聽到我們的 web容器啟動完畢後,接收到監聽拉更改埠的 此時還沒啟動web容器 所以這裡還是0
    if (this.port.get() != 0) {
        if (this.registration.getNonSecurePort() == 0) {
            this.registration.setNonSecurePort(this.port.get());
        }
        if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
            this.registration.setSecurePort(this.port.get());
        }
    }
    // 這裡的 nonSecurePort 預設就是我們服務的埠號
    // 那麼當重新整理上下文的時候,這裡會執行
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
        this.serviceRegistry.register(this.registration);
        this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));
        this.running.set(true);
    }
}

this.registration.getNonSecurePort()的來源:

this.registration.getNonSecurePort(),這個埠號是來自 registration 我們這裡是 Eureka 即 EurekaRegistration,而它又來自於:CloudEurekaInstanceConfig instanceConfig,

那我們看到第一次到 start 會執行這段程式碼:

// 這裡的 nonSecurePort 預設就是我們服務的埠號
// 那麼當重新整理上下文的時候,這裡會執行
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
    this.serviceRegistry.register(this.registration);
    this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));
    this.running.set(true);
}

那麼看到這裡,我們得先了解下 serviceRegistry(EurekaServiceRegistry)和 registration(EurekaRegistration)的來源,都是在自動裝配類裡 EurekaClientAutoConfiguration:

EurekaServiceRegistry 的來源比較簡單:

EurekaRegistration的來源:

可以看到 EurekaRegistration 有很多的依賴:EurekaClient、CloudEurekaInstanceConfig、ApplicationInfoManager、HealthCheckHandler。

那我們看看這四個怎麼來的,先看 EurekaClient:

// 見 EurekaClientAutoConfiguration
@Bean(
    destroyMethod = "shutdown"
)
@ConditionalOnMissingBean(
    value = {EurekaClient.class},
    search = SearchStrategy.CURRENT
)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config, EurekaInstanceConfig instance, @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
    ApplicationInfoManager appManager;
    // 拿到原始物件
    if (AopUtils.isAopProxy(manager)) {
        appManager = (ApplicationInfoManager)ProxyUtils.getTargetObject(manager);
    } else {
        appManager = manager;
    }
    // 建立一個 client 物件
    CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);
    // 註冊健康檢查
    cloudEurekaClient.registerHealthCheck(healthCheckHandler);
    return cloudEurekaClient;
}

可以看到它也依賴很多: EurekaInstanceConfig、ApplicationInfoManager、HealthCheckHandler、EurekaClientConfig ,它跟 EurekaRegistration 的依賴差不多。

EurekaClientConfig 是 eureka.client 開頭的配置 Bean:

EurekaInstanceConfig 是 eureka.instance 開頭的配置 Bean:

HealthCheckHandler 健康檢查是來源於 EurekaDiscoveryClientConfiguration 自動裝配,當你開啟了 eureka.client.healthcheck.enabled 的配置(預設=false 不開啟)就會註冊一個健康檢查的 Bean:

ApplicationInfoManager 是對當前服務資訊的一個管理 Bean,來源於 EurekaClientAutoConfiguration 自動裝配類:

@Bean
@ConditionalOnMissingBean(
    value = {ApplicationInfoManager.class},
    search = SearchStrategy.CURRENT
)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
    // EurekaInstanceConfig eureka.instance 開頭的配置Bean
    // InstanceInfoFactory 工廠來建立 InstanceInfo 也就是針對當前服務的資訊封裝到 InstanceInfo裡
    InstanceInfo instanceInfo = (new InstanceInfoFactory()).create(config);
    // 例項化
    return new ApplicationInfoManager(config, instanceInfo);
}

瞭解完這四個依賴,我們繼續看 EurekaClient 的建立:

// 建立一個 client 物件
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager, config, this.optionalArgs, this.context);

CloudEurekaClient 最後會走到 DiscoveryClient 的構造器:

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    // 省略...
    logger.info("Initializing Eureka in region {}", this.clientConfig.getRegion());
    // 不開啟註冊或者服務發現的話走這裡  這個不看 我們重點看 else 的邏輯
    if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
        logger.info("Client configured to neither register nor query for data.");
        this.scheduler = null;
        this.heartbeatExecutor = null;
        this.cacheRefreshExecutor = null;
        this.eurekaTransport = null;
        this.instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), this.clientConfig.getRegion());
        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        this.initTimestampMs = System.currentTimeMillis();
        this.initRegistrySize = this.getApplications().size();
        this.registrySize = this.initRegistrySize;
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", this.initTimestampMs, this.initRegistrySize);
    } else {
        try {
            // 排程執行緒池 
            this.scheduler = Executors.newScheduledThreadPool(2, (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-%d").setDaemon(true).build());
            // 健康檢查的執行緒池 
            this.heartbeatExecutor = new ThreadPoolExecutor(1, this.clientConfig.getHeartbeatExecutorThreadPoolSize(), 0L, TimeUnit.SECONDS, new SynchronousQueue(), (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-HeartbeatExecutor-%d").setDaemon(true).build());
            this.cacheRefreshExecutor = new ThreadPoolExecutor(1, this.clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0L, TimeUnit.SECONDS, new SynchronousQueue(), (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d").setDaemon(true).build());
            this.eurekaTransport = new DiscoveryClient.EurekaTransport();
            this.scheduleServerEndpointTask(this.eurekaTransport, args);
            Object azToRegionMapper;
            if (this.clientConfig.shouldUseDnsForFetchingServiceUrls()) {
                azToRegionMapper = new DNSBasedAzToRegionMapper(this.clientConfig);
            } else {
                azToRegionMapper = new PropertyBasedAzToRegionMapper(this.clientConfig);
            }

            if (null != this.remoteRegionsToFetch.get()) {
                ((AzToRegionMapper)azToRegionMapper).setRegionsToFetch(((String)this.remoteRegionsToFetch.get()).split(","));
            }

            this.instanceRegionChecker = new InstanceRegionChecker((AzToRegionMapper)azToRegionMapper, this.clientConfig.getRegion());
        } catch (Throwable var12) {
            throw new RuntimeException("Failed to initialize DiscoveryClient!", var12);
        }
        
        // 服務註冊並且強制在初始化的時候就註冊 預設是false 也就是不會在初始化的時候註冊
        if (this.clientConfig.shouldRegisterWithEureka() && this.clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
                // 呼叫註冊
                if (!this.register()) {
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable var10) {
                logger.error("Registration error at startup: {}", var10.getMessage());
                throw new IllegalStateException(var10);
            }
        }
        // 初始化執行緒任務 
        this.initScheduledTasks();

        try {
            Monitors.registerObject(this);
        } catch (Throwable var9) {
            logger.warn("Cannot register timers", var9);
        }

        DiscoveryManager.getInstance().setDiscoveryClient(this);
        DiscoveryManager.getInstance().setEurekaClientConfig(config);
        this.initTimestampMs = System.currentTimeMillis();
        this.initRegistrySize = this.getApplications().size();
        this.registrySize = this.initRegistrySize;
        logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}", this.initTimestampMs, this.initRegistrySize);
    }
}

在這個構造器裡,我除錯發現它預設是不在初始化進行強制註冊到 Eureka的,並且建立了幾個執行緒池,那我們繼續看看初始化執行緒任務裡都要做什麼事情:

// TimedSupervisorTask 它這個看似是一個體系用於做執行緒任務的  我們本節暫時不看它原理
private void initScheduledTasks() {
    int renewalIntervalInSecs;
    int expBackOffBound;
    // 預設是開啟的
    if (this.clientConfig.shouldFetchRegistry()) {
        // 間隔時間 預設30秒
        renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
        expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        // cacheRefresh 續租任務 類似告訴伺服器我還活著 別把我下掉
        this.cacheRefreshTask = new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread());
        this.scheduler.schedule(this.cacheRefreshTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);
    }
    // 預設開啟的 註冊到 eureka 這個是我們本節關注的
    if (this.clientConfig.shouldRegisterWithEureka()) {
        renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
        // heartbeat 健康檢查的
        this.heartbeatTask = new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread());
        this.scheduler.schedule(this.heartbeatTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);
        // InstanceInfoReplicator 實現了 Runnable
        this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
        this.statusChangeListener = new StatusChangeListener() {
            public String getId() {
                return "statusChangeListener";
            }
            
            public void notify(StatusChangeEvent statusChangeEvent) {
                DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
            }
        };
        // 預設開啟 往 applicationInfoManager 註冊了一個監聽
        if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
            this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
        }
        // 啟動 我們的服務註冊就在這裡
        this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

關於 EurekaClient 的構造我們就暫時看到這裡,我們繼續看一個 InstanceInfoReplicator,看看這個任務幹了些什麼:

class InstanceInfoReplicator implements Runnable {
    ...
    // 構造器
    InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
        this.discoveryClient = discoveryClient;
        this.instanceInfo = instanceInfo;
        // 初始化了一個執行緒池
        this.scheduler = Executors.newScheduledThreadPool(1, (new ThreadFactoryBuilder()).setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d").setDaemon(true).build());
        this.scheduledPeriodicRef = new AtomicReference();
        this.started = new AtomicBoolean(false);
        this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
        this.replicationIntervalSeconds = replicationIntervalSeconds;
        this.burstSize = burstSize;
        this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
        logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", this.allowedRatePerMinute);
    }

    // start 方法就是往執行緒池裡提交了一個任務 任務的內容就是自己的 run 方法
    public void start(int initialDelayMs) {
        if (this.started.compareAndSet(false, true)) {
            this.instanceInfo.setIsDirty();
            Future next = this.scheduler.schedule(this, (long)initialDelayMs, TimeUnit.SECONDS);
            this.scheduledPeriodicRef.set(next);
        }

    }
    // this.instanceInfo.setIsDirty() 方法內容
    // public synchronized void setIsDirty() {
    //     this.isInstanceInfoDirty = true;
    //     this.lastDirtyTimestamp = System.currentTimeMillis();
    // }
    
    // ...
    public void run() {
        boolean var6 = false;

        ScheduledFuture next;
        label53: {
            try {
                var6 = true;
                this.discoveryClient.refreshInstanceInfo();
                Long dirtyTimestamp = this.instanceInfo.isDirtyWithTime();
                // 不為空 執行註冊
                if (dirtyTimestamp != null) {
                    this.discoveryClient.register();
                    this.instanceInfo.unsetIsDirty(dirtyTimestamp);
                    var6 = false;
                } else {
                    var6 = false;
                }
                break label53;
            } catch (Throwable var7) {
                logger.warn("There was a problem with the instance info replicator", var7);
                var6 = false;
            } finally {
                if (var6) {
                    ScheduledFuture next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
                    this.scheduledPeriodicRef.set(next);
                }
            }

            next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
            this.scheduledPeriodicRef.set(next);
            return;
        }

        next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
        this.scheduledPeriodicRef.set(next);
    }
}

哎喲,這個註冊,第一次的落點在 EurekaClient 的構造器裡,透過啟動 InstanceInfoReplicator 來進行註冊的。

但是有個重要的資訊,就是它都是帶 @Lazy 標記的,也就是不會在重新整理上下文的最後階段主動初始化這些類,而是在第一次使用的時候才進行例項化的。

那麼回到我們本節的主題,它的註冊時機在哪裡?或者第一次呼叫這就要回到我們最初的EurekaAutoServiceRegistration 生命週期 start 方法裡,就是我下邊紅色加粗的這裡:

// EurekaAutoServiceRegistration 的 start 方法
// private AtomicInteger port = new AtomicInteger(0);
public void start() {
    // 這個時候進來 port 還是0
    // 這裡提前告訴你 他是透過監聽事件 當監聽到我們的 web容器啟動完畢後,接收到監聽拉更改埠的 此時還沒啟動web容器 所以這裡還是0
    if (this.port.get() != 0) {
        if (this.registration.getNonSecurePort() == 0) {
            this.registration.setNonSecurePort(this.port.get());
        }
        if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
            this.registration.setSecurePort(this.port.get());
        }
    }
    // 這裡的 nonSecurePort 預設就是我們服務的埠號
    // 那麼當重新整理上下文的時候,這裡會執行    也就是第一次在這裡的執行
    if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
        this.serviceRegistry.register(this.registration);
        this.context.publishEvent(new InstanceRegisteredEvent(this, this.registration.getInstanceConfig()));
        this.running.set(true);
    }
}

走到這裡:this.serviceRegistry.register(this.registration);

// 服務註冊
public void register(EurekaRegistration reg) {
    // 看人家這個取名  maybe 或許 初始化 client 
    this.maybeInitializeClient(reg);
    if (log.isInfoEnabled()) {
        log.info("Registering application " + reg.getApplicationInfoManager().getInfo().getAppName() + " with eureka with status " + reg.getInstanceConfig().getInitialStatus());
    }
    reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
    reg.getHealthCheckHandler().ifAvailable((healthCheckHandler) -> {
        reg.getEurekaClient().registerHealthCheck(healthCheckHandler);
    });
}
// getApplicationInfoManager 是不是就會開始建立 我們的 ApplicationInfoManager 它由依賴於 EurekaClient
// 是不是就都建立起來了
private void maybeInitializeClient(EurekaRegistration reg) {
    reg.getApplicationInfoManager().getInfo();
    reg.getEurekaClient().getApplications();
}

好啦,至此到這裡,建立 EurekaClient 的時候,進行服務註冊的。

最後再看看註冊方法,哎喲,這裡就是第一次的註冊:

boolean register() throws Throwable {
    logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);
    EurekaHttpResponse httpResponse;
    try {
        // 服務註冊
        httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
    } catch (Exception var3) {
        logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
        throw var3;
    }
    if (logger.isInfoEnabled()) {
        logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

跟我們的日誌也能對上:

所以服務第一次註冊的時機,預設的情況下是在 重新整理上下文的 finishRefresh 裡呼叫 Bean 生命週期的 start,透過 serviceRegistry.registry 方法來第一次載入 EurekaClient 相關的 Bean,在 EurekaClient 的構造器裡透過 InstanceInfoReplicator 來進行服務的註冊。

3 小結

好啦,本節我們就暫時看到這裡,下節我們再看下線時機,有理解不對的地方歡迎指正哈。

相關文章