【Spring Cloud】源碼-Eureka客戶端的服務註冊、服務獲取與服務續約

在看源碼之前,先說一下標題中提到的三個概念:

1. 服務註冊:

    服務提供者(eureka客戶端)在啓動後,如果參數eureka.client.register-with-eureka爲true,那麼會將自己註冊到服務註冊中心中,註冊的動作會將自己的元數據發送給註冊中心,註冊中心將接受的元數據保存在一個註冊列表中,該列表是一個雙層Map結構,具體爲:Map<服務名, Map<實例名,服務實例>>

2. 服務續約:

    成功註冊的eureka服務(eureka客戶端)會在註冊之後維護一個心跳來告訴註冊中心“我還活着”,這樣註冊中心就不會從註冊列表中將這個服務實例剔除,關於這個心跳機制涉及到兩個配置參數:

    eureka.instance.lease-renewal-interval-in-seconds(默認30):心跳間隔時間

    eureka.instance.lease-expiration-duration-in-seconds(默認90):定義服務失效時間

3. 服務獲取:

    前兩個概念是針對服務提供者,而服務獲取是針對服務消費者(也屬於eureka客戶端),即調用服務方纔需要獲取服務列表以便選擇調用哪一個服務實例。在服務消費者啓動後,會向服務註冊中心請求一份服務清單,該清單記錄了已經註冊到服務中心的服務實例。該請求動作不會僅限於啓動的時候,因爲消費者需要訪問正確的、健康的服務實例,因此會定時發送請求。間隔時間通過配置參數:

    eureka.instance.registry-fetch-interval-seconds(默認30)

 

那麼接下來我們來簡單看一下源碼:

首先eureka客戶端最重要的功能實現類就是org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient

這個類是對Eureka發現法務的封裝,而SpringCloudEureka本身就是對NetflixEureka的功能封裝,因此,EurekaDiscoveryClient類會持有一個com.netflix.discovery.EurekaClient.EurekaClient對象引用(爲組合關係,具體實現是NetflixEureka的DiscoveryClient類),而SpringCloudEureka本身有一個對發現服務的常用方法的抽象,這就是org.springframework.cloud.netflix.eureka.DiscoveryClient接口,EurekaDiscoveryClient實現了該接口(爲繼承關係),他們的關係大概如下圖所示:

其中左邊的兩個是SpringCloudEureka的,右面的兩個是NetflixEureka的。

既然服務發現的方法主要在Netflix的DiscoveryClient類中,可以看一下這個類的註釋,主要告訴我們DiscoveryClient的主要功能:Eureka客戶端的註冊、續約、取消租約(服務關閉)、獲取服務。

因此首先研究的就是它,在構造器中,DiscoveryClient會調用一個initScheduledTasks()方法,從命名就可以看出這是一個初始化方法,那麼我們可以從這個方法入手,源碼如下:

/**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {    // #1
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {    //#2
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);

            // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);    // #3

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(    // #4
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

 

在這段代碼中,出現兩個重要的if判斷,首先看#1處的if判斷:

 

if(clientConfig.shouldFetchRegistry()),顧名思義,獲取配置中參數eureka.client.fetch-registery(是否獲取服務列表)的值,如果是服務消費者,那麼這個值就爲true,進入判斷代碼塊後,首先會從配置中獲取一個參數:

registeryFetchIntervalSeconds:對應配置參數eureka.client.registry-fetch-interval-seconds,即從服務註冊中心獲取服務列表的間隔時間,默認是30。之後會執行定時任務CacheRefreshThread(),爲什麼叫CacheRefreshThread呢?因爲客戶端所持有的服務列表會緩存起來,到了一定時間(即上面的eureka.client.registry-fetch-interval-seconds)後會更新緩存並重新從註冊中心獲取新的服務列表,因此有一個“Cache”開頭的方法。這個方法的代碼如下:

/**
     * The task that fetches the registry information at specified intervals.
     *
     */
    class CacheRefreshThread implements Runnable {
        public void run() {
            refreshRegistry();
        }
    }

run()方法中執行refreshRegistry(),方法代碼如下:

@VisibleForTesting
    void refreshRegistry() {
        try {
            boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();

            boolean remoteRegionsModified = false;
            // This makes sure that a dynamic change to remote regions to fetch is honored.
            String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();
            if (null != latestRemoteRegions) {
                String currentRemoteRegions = remoteRegionsToFetch.get();
                if (!latestRemoteRegions.equals(currentRemoteRegions)) {
                    // Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in sync
                    synchronized (instanceRegionChecker.getAzToRegionMapper()) {
                        if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {
                            String[] remoteRegions = latestRemoteRegions.split(",");
                            remoteRegionsRef.set(remoteRegions);
                            instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);
                            remoteRegionsModified = true;
                        } else {
                            logger.info("Remote regions to fetch modified concurrently," +
                                    " ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);
                        }
                    }
                } else {
                    // Just refresh mapping to reflect any DNS/Property change
                    instanceRegionChecker.getAzToRegionMapper().refreshMapping();
                }
            }

            boolean success = fetchRegistry(remoteRegionsModified);
            if (success) {
                registrySize = localRegionApps.get().size();
                lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
            }

            if (logger.isDebugEnabled()) {
                StringBuilder allAppsHashCodes = new StringBuilder();
                allAppsHashCodes.append("Local region apps hashcode: ");
                allAppsHashCodes.append(localRegionApps.get().getAppsHashCode());
                allAppsHashCodes.append(", is fetching remote regions? ");
                allAppsHashCodes.append(isFetchingRemoteRegionRegistries);
                for (Map.Entry<String, Applications> entry : remoteRegionVsApps.entrySet()) {
                    allAppsHashCodes.append(", Remote region: ");
                    allAppsHashCodes.append(entry.getKey());
                    allAppsHashCodes.append(" , apps hashcode: ");
                    allAppsHashCodes.append(entry.getValue().getAppsHashCode());
                }
                logger.debug("Completed cache refresh task for discovery. All Apps hash code is {} ",
                        allAppsHashCodes.toString());
            }
        } catch (Throwable e) {
            logger.error("Cannot fetch registry from server", e);
        }        
    }

該段代碼沒研究...

 

 

 

接下來我們看#2處的if判斷:

 

if(clientConfig.shouldRegisterWithEureka()),可以看出,此處判斷參數eureka.client.register-with-eureka(是否將自己註冊到服務註冊中心,默認true),如果爲true,那麼就意味着需要做兩件事:服務註冊與服務續約。那麼我們看代碼進入#2的if判斷之後,首先看#3處的代碼,很明顯這是一個定時任務,從renewalIntervalInSecs變量可以看出,這是心跳機制的定時任務,renewalIntervalInSecs變量則是從配置文件參數eureka.instance.lease-renewal-interval-in-seconds的值(默認爲30),含義是心跳間隔時間,而HeartbeatThread()方法就是續約方法,

HeartbeatThread()方法代碼如下:

/**
     * The heartbeat task that renews the lease in the given intervals.
     */
    private class HeartbeatThread implements Runnable {

        public void run() {
            if (renew()) {    // #3.1
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }

#3.1調用方法renew():

/**
     * Renew with the eureka service by making the appropriate REST call
     */
    boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
            logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == 404) {
                REREGISTER_COUNTER.increment();
                logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
                return register();
            }
            return httpResponse.getStatusCode() == 200;
        } catch (Throwable e) {
            logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
            return false;
        }
    }

該方法就是發送Rest請求給註冊中心,若返回404,則調用register()方法,這個方法就是將自己的元數據重新註冊到服務中心,具體代碼不貼上來了。

之後在#4處的代碼會創建一個InstanceInfoReplicator對象,這個對象也會做一個定時任務,具體run()方法代碼如下:

public void run() {
        try {
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                discoveryClient.register();    // #4.1
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

很明顯,#4.1處的代碼行,register()方法正是服務註冊方法,其實代碼如下:

/**
     * Register with the eureka service by making the appropriate REST call.
     */
    boolean register() throws Throwable {
        logger.info(PREFIX + appPathIdentifier + ": registering service...");
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);    // #4.1.1
        } catch (Exception e) {
            logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == 204;
    }

#4.1.1處的代碼就是具體的註冊動作,該方法會發送一個Rest請求給服務註冊中心,同時傳遞一個instanceInfo對象,之前我們說過,註冊動作會將客戶端自己的元數據傳遞給服務註冊中心,那麼這個instanceInfo對象就是客戶端的元數據。

至此,#3#4兩處代碼分別是 服務的註冊與續約功能實現。由於服務註冊與續約均需要參數eureka.client.register-with-eureka爲true,因此兩個功能寫入一個if判斷中,而服務發現需要參數eureka.client.fetch-registery爲true,因此單獨在一個if判斷中。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章