Spring Cloud Eureka 源碼分析

Eureka服務治理體系三個核心角色:服務註冊中心、服務提供者以及服務消費者。後兩者(也就是Eureka客戶端)在整個運行機制中是大部分通信行爲的主動發起者,而註冊中心主要是處理請求的接收者。所以,我們可以從Eureka的客戶端作爲入口看看它是如何完成這些主動通信行爲的。

我們在將一個普通的Spring Boot應用註冊到Eureka Server中,或是從Eureka Server中獲取服務列表時,主要就做了兩件事:

  • 在應用主類中配置了@EnableDiscoveryClient註解
  • application.properties中用eureka.client.serviceUrl.defaultZone參數指定了服務註冊中心的位置

順着上面的線索,我們先查看@EnableDiscoveryClient的源碼如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

}

從該註解的註釋中我們可以知道,它主要用來開啓DiscoveryClient的實例。通過搜索DiscoveryClient,我們可以發現有一個類和一個接口。通過梳理我們可以得到如圖所示的關係:

其中,左邊的org.springframework.cloud.client.discovery.DiscoveryClientSpringCloud的接口,它定義了用來發現服務的抽象方法,通過該接口可以有效地屏蔽服務治理的實現細節,所以用SpringCloud構建的微服務應用可以方便地切換不同的服務治理框架,而不改動程序代碼,只需要添加一些針對服務治理框架的配置即可。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是針對該接口實現,從命名來判斷,它實現的是對Eureka發現服務的封裝。所以EurekaDiscoveryClient依賴NetflixEurekacom.netflix.discovery.EurekaClient接口,EurekaClient繼承了LookupService接口,它們都是Netflix開源包中的內容,主要定義了針對Eureka的服務發現的抽象方法,而真正的發現服務的則是Netflix包中com.netflix.discovery.DiscoveryClient類。

接下來,我們就來詳細的看看DiscoveryClient類吧。先解讀一下該類頭部的註釋,註釋的大致內容如下所示:

這個類用於幫助與Eureka Server互相協作。


Eureka Client 負責下面的任務:
-向Eureka Server 註冊服務實例
-向Eureka Server 服務租約
-當服務關閉期間,向Eureka Server 取消租約
-查詢Eureka Server 中的服務實例列表


Eureka Client 還需要配置一個Eureka Server 的URL列表。

在具體研究Eureka Client負責完成的任務之前,我們先看看在哪裏對Eureka  Server的URL列表進行配置。根據我們配置的屬性名eureka.client.serviceUrl.defaultZone,通過serviceURL可以找到該屬性相關的加載屬性,但是在SR5版本中它們都被@Deprecated標註爲不再建議使用,並@link到了替代類com.netflix.discovery.endpoint.EndPointUtils,所以我們可以在該類中找到下面這個函數:

public static Map<String, List<String>> getServiceUrlsMapFromConfig(
			EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
    Map<String, List<String>> orderedUrls = new LinkedHashMap<>();
    String region = getRegion(clientConfig);
    String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
    if (availZones == null || availZones.length == 0) {
        availZones = new String[1];
        availZones[0] = DEFAULT_ZONE;
    }
	……
    int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);

    String zone = availZones[myZoneOffset];
    List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
    if (serviceUrls != null) {
        orderedUrls.put(zone, serviceUrls);
    }
	……
    return orderedUrls;
}

Region、Zone

在上面的函數中,可以發現,客戶端依次加載了兩個內容,第一個是Region,第二個是Zone,從其加載邏輯我們可以判斷他們之間的關係:

  • 通過getRegion函數,我們可以看到它從配置中都去了一個Region返回,所以一個微服務應用只可以屬於一個Region,如果不特別配置,默認爲default。若我們要自己設置,可以通過eureka.client.region屬性來定義。
    public static String getRegion(EurekaClientConfig clientConfig) {
        String region = clientConfig.getRegion();
        if (region == null) {
            region = DEFAULT_REGION;
        }
        region = region.trim().toLowerCase();
        return region;
    }

     

  • 通過getAvailabilityZones函數,可以知道當我們沒有特別爲Region配置Zone的時候,將默認採用defaultZone,這也是我們之前配置參數eureka.client.serviceUrl.defaultZone的由來。若要爲應用指定Zone,可以通過eureka.client.availability-zones屬性來進行設置。從該函數的return內容,我們可以知道Zone能夠設置多個,並且通過逗號分隔來配置。由此,我們可以判斷Region與Zone是一對多的關係。
    public String[] getAvailabilityZones(String region) {
    	String value = this.availabilityZones.get(region);
    	if (value == null) {
    		value = DEFAULT_ZONE;
    	}
    	return value.split(",");
    }

serviceUrls

在獲取了Region和Zone的信息之後,纔開始真正的加載Eureka Server 的具體地址。它根據傳入的參數按一定算法確定加載位於哪一個Zone配置的serviceUrls。

int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
String zone = availZones[myZoneOffset];
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);

具體獲取serviceUrls的實現,我們可以詳細查看getEurekaServerUrls函數的具體實現類EurekaClientConfigBean,該類是EurekaClientConfigEurekaConstants接口的實現。用來加載配置文件的內容,這裏有非常多有用的信息,我們先說一下我們此處關心的,關於defaultZone的信息。通過搜索defaultZone,我們可以很容易的找到下面這個函數,它具體實現瞭如何解析參數的過程,通過此內容,我們就可以知道,eureka.client.serviceUrl.defaultZone屬性可以配置多個,並且需要通過逗號分隔。

public List<String> getEurekaServerServiceUrls(String myZone) {
	String serviceUrls = this.serviceUrl.get(myZone);
	if (serviceUrls == null || serviceUrls.isEmpty()) {
		serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);
	}
	if (!StringUtils.isEmpty(serviceUrls)) {
		final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
		List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length);
		for (String eurekaServiceUrl : serviceUrlsSplit) {
			if (!endsWithSlash(eurekaServiceUrl)) {
				eurekaServiceUrl += "/";
			}
			eurekaServiceUrls.add(eurekaServiceUrl);
		}
		return eurekaServiceUrls;
	}
	return new ArrayList<>();
}

當我們在微服務應用中使用Ribbon來實現服務調用時,對於Zone的設置可以在負載均衡時實現區域親和特性:Ribbon的默認策略會優先訪問同客戶端處於一個Zone中的服務端實例,只有當同一個Zone中沒有可用服務端實例的時候纔會訪問其他Zone中的實例,所以通過Zone屬性的定義,配合實際部署的物理結構,我們就可以有效的設計出對區域性故障的容錯集羣。

服務註冊

在理解了多個服務註冊中心信息的加載後,我們再回頭看看DiscoveryClient類是如何實現“服務註冊”行爲的,通過查看它的構造類,可以找到它調用了下面這個函數:

private void initScheduledTasks() {
    ...
    if (clientConfig.shouldRegisterWithEureka()) {
        ...
        // InstanceInfo replicator
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
               instanceInfo,
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize
        ...
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

從上面的函數中,可以看到一個與服務註冊相關的判斷語句if(clientConfig.shouldRegisterWithEureka())。在該分支,創建一個InstanceInfoReplicator類的實例,它會執行一個定時任務,而這個定時任務的具體工作可以查看該類的run()函數,具體如下所示:

public void run() {
    try {
        discoveryClient.refreshInstanceInfo();
        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
            discoveryClient.register();
            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);
    }
}

相信大家都發現了discoveryClient.register();這一行,真正觸發調用註冊的地方就在這裏。繼續查看register()的實現內容,如下所示:

boolean register() throws Throwable {
    logger.info(PREFIX + appPathIdentifier + ": registering service...");
    EurekaHttpResponse<Void> httpResponse;
    try {
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } 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;
}

通過屬性命名,大家基本也能猜出來,註冊操作也是通過REST請求的方式進行的。同時,我們能看到發起註冊請求的時候,傳入了一個com.netflix.appinfo.InstanceInfo對象,該對象就是註冊時客戶端給服務端的服務的元數據。

服務獲取與續約

順着上面的思路,我們繼續來看DiscoveryClientinitScheduledTasks函數,不難發現其中還有兩個定時任務,分別是“服務獲取”和“服務續約”:

private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {
        // 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()) {
		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);
		// InstanceInfo replicator
		……
	}
}

從源碼中我們可以發現,“服務獲取”任務相對於“服務續約”和“服務註冊”任務更加獨立。“服務續約”與“服務註冊”在同一個if邏輯中,這個不難理解,服務註冊到Eureka Server後,自然需要一個心跳去續約,防止被剔除,所以它們肯定是成對出現的。從源碼中,我們更清楚地看到了之前所提到的,對於服務續約相關的時間控制參數:

eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90

而“服務獲取”的邏輯在獨立的一個if判斷中,其判斷依據就是我們之前所提到的eureka.client.fetch-registry=true參數,它默認爲true,大部分情況下我們不需要關心。爲了定期更新客戶端的服務清單,以保證客戶端能夠訪問確實健康的服務實例,“服務獲取”的請求不會只限於服務啓動,而是一個定時執行的任務,從源碼中我們可以看到任務運行中的registryFetchIntervalSeconds參數對應的就是之前所提到的eureka.client.registry-fetch-interval-seconds=30配置參數,它默認爲30秒。

繼續向下深入,我們能分別發現實現“服務獲取”和“服務續約”的具體方法,其中“服務續約”的實現較爲簡單,直接以REST請求的方式進行續約:

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請求和相應處理。具體的實現邏輯跟之前類似,有興趣的讀者可以繼續查看服務客戶端的具體其他內容,以瞭解更多細節。

服務註冊中心處理

通過上面的源碼分析,可以看到所有的交互都是通過REST請求來發起的。下面我們來看看服務註冊中心對這些請求的處理。Eureka Server對於各類REST請求的定義都位於com.netflix.eureka.resources包下。

以“服務註冊”請求爲列:

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
                  @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
    // validate that the instanceinfo contains all the necessary required fields
    ...
    // handle cases where clients may be registering with bad DataCenterInfo with missing data
    DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
    if (dataCenterInfo instanceof UniqueIdentifier) {
        String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
        if (isBlank(dataCenterInfoId)) {
            boolean experimental = "true".equalsIgnoreCase(
					serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
            if (experimental) {
                String entity = "DataCenterInfo of type " + dataCenterInfo.getClass()
										+ " must contain a valid id";
                return Response.status(400).entity(entity).build();
            } else if (dataCenterInfo instanceof AmazonInfo) {
                AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                if (effectiveId == null) {
                    amazonInfo.getMetadata().put(
							AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                }
            } else {
                logger.warn("Registering DataCenterInfo of type {} without an appropriate id",
						dataCenterInfo.getClass());
            }
        }
    }

    registry.register(info, "true".equals(isReplication));
    return Response.status(204).build();  // 204 to be backwards compatible
}

在對註冊信息進行了一堆校驗之後,會調用org.springframework.cloud.netflix.eureka.server.InstanceRegistry對象中的register(InstanceInfo info, int leaseDuration, boolean isReplication)函數來進行服務註冊:

public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
	if (log.isDebugEnabled()) {
		log.debug("register " + info.getAppName() + ", vip " + info.getVIPAddress()
				+ ", leaseDuration " + leaseDuration + ", isReplication "
				+ isReplication);
	}
	this.ctxt.publishEvent(new EurekaInstanceRegisteredEvent(this, info,
			leaseDuration, isReplication));

	super.register(info, leaseDuration, isReplication);
}

在註冊函數中,先調用publishEvent函數,將該新服務註冊的事件傳播出去,然後調用com.netflix.eureka.registry.AbstractInstanceRegistry父類中的註冊實現,將InstanceInfo中的元數據信息存儲在一個ConcurrentHashMap對象中。正如我們之前所說的,註冊中心存儲了兩層Map結構,第一層key存儲服務名:InstanceInfo中的appName屬性,第二層的key存儲實例名:InstanceInfo中的instanceId屬性。

服務端的請求和接收非常類似,對於其他的服務端處理,這裏不再展開敘述,讀者可以根據上面的脈絡來自己查看其內容(這裏包含很多細節內容)來幫助和加深理解。

 

 

 

 

 

 

 

 

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