在微服務治理1 - Eureka服務治理架構及客戶端裝配和啓動流程分析中介紹了Eureka Client端主要提供以下功能:
- 服務註冊:服務在啓動時通過發送REST請求將自己註冊到Eureka註冊中心
- 服務續約(Renew):週期性的向Eureka註冊中心發送心跳信息,以防止Eureka註冊中心將服務剔除
- 服務下線:關閉服務時,向Eureka註冊中心發送REST請求,告訴Eureka註冊中心服務下線
- 獲取服務列表:向Eureka註冊中心發送REST請求以獲取Eureka註冊中心上的註冊的服務清單
接下來將逐條分析上述功能的源碼。
1. 服務註冊
在文章微服務治理1 - Eureka服務治理架構及客戶端裝配和啓動流程分析中的步驟(14)~(15)中說明了服務註冊是在DiscoverClient
的initScheduledTasks()
函數中完成的,接下來看看initScheduledTasks()
函數的源碼:
private void initScheduledTasks() {
......
(1)if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
// Heartbeat timer
(2)scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
(3)instanceInfoReplicator = new InstanceInfoReplicator(
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);
}
(4)instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
(1) 是否向Eureka Server註冊服務信息,默認爲true,可以通過eureka.client.register-with-eureka
配置屬性控制
(2) 服務續約,在後續的章節詳細介紹
(3) 實例化InstanceInfoReplicator
對象,該類的註釋如下:
/**
* A task for updating and replicating the local instanceinfo to the remote server. Properties of this task are:
* - configured with a single update thread to guarantee sequential update to the remote server
* - update tasks can be scheduled on-demand via onDemandUpdate()
* - task processing is rate limited by burstSize
* - a new update task is always scheduled automatically after an earlier update task. However if an on-demand task
* is started, the scheduled automatic update task is discarded (and a new one will be scheduled after the new
* on-demand update).
*/
從註釋中可以看出其是任務類,負責將服務實例信息週期性的上報到Eureka server,其特性如下:
- 兩個觸發場景:單線程週期性自動更新實例信息、通過調用
onDemandUpdate()
函數按需更新實例信息 - 任務處理通過burstSize參數來控制頻率
- 先創建的任務總是先執行,但是
onDemandUpdate()
方法中創建的任務會將週期性任務給丟棄掉
在實例化InstanceInfoReplicator
對象時,傳入了4個參數,其中:
instanceInfo
代表服務實例信息,該實例信息的初始化在請參考附錄代碼一clientConfig.getInstanceInfoReplicationIntervalSeconds()
,該方法返回的是服務實例信息向Eureka Server同步的週期(默認30s),可以通過eureka.client.instance-info-replication-interval-seconds
屬性進行配置- 2爲burstSize的值,控制任務處理的頻率
(4) 通過調用instanceInfoReplicator.start()
啓動週期性任務,傳入的參數爲clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()
,該參數代表產自注冊服務實例信息的延遲時間(默認40s),可以通過屬性eureka.client.initial-instance-info-replication-interval-seconds
配置,這也是爲什麼首次註冊服務實例信息比較慢的原因
接下來進入InstanceInfoReplicator
類中,分析其註冊服務信息的過程,其構造函數爲:
InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
this.discoveryClient = discoveryClient;
this.instanceInfo = instanceInfo;
(5)this.scheduler = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d")
.setDaemon(true)
.build());
this.scheduledPeriodicRef = new AtomicReference<Future>();
this.started = new AtomicBoolean(false);
(6)this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
this.replicationIntervalSeconds = replicationIntervalSeconds;
this.burstSize = burstSize;
(7)this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);
}
(5) core size爲1的線程池
(6) 工具類,用於限制單位時間內的任務次數
(7) 通過週期間隔,和burstSize參數,計算每分鐘允許的任務數
步驟(4)中通過調用InstanceInfoReplicator
類中的start
方法啓動任務,代碼如下:
public void start(int initialDelayMs) {
(8)if (started.compareAndSet(false, true)) {
instanceInfo.setIsDirty(); // for initial register
(9)Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
(10)scheduledPeriodicRef.set(next);
}
}
(8) CAS操作,保證start
方法的邏輯只執行一次
(9) 提交一個延時執行任務,由於第一個參數是this,因此延時結束時會調用InstanceInfoReplicator
中的run方法
(10) 將任務的Feature對象放在成員變量scheduledPeriodicRef
中,方便後續在調用onDemandUpdate
函數時取消任務
當延遲結束時,會調用InstanceInfoReplicator
中的run方法,代碼如下:
public void run() {
try {
(11)discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
(12)if (dirtyTimestamp != null) {
(13)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);
}
}
(11) 更新服務實例信息
(12) 只有服務實例信息改變後纔會真正執行服務註冊操作,從而能夠減少網絡通信量
(13) 向Eureka Server註冊服務
可以看到最終雙調到了DiscoveryClient
類的register()
方法,代碼如下:
/**
* Register with the eureka service by making the appropriate REST call.
*/
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
(14)httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
(14) 可以看到最終是通過發送REST請求來註冊服務實例信息
2. 服務註冊的REST請求分析
上節講到了最終調用DiscoveryClient
的register()
函數來註冊服務,本節看一下register()
是如何同Eureka Server進行通信的。
Eureka Client同Eureka Server通信的所有API都封裝在接口EurekaHttpClient
接口中,其中包含的API如下:
該接口的繼承關係如下(請讀者可自行分析):
register()
調用時序圖如下(請讀者自行分析):
結合上面兩幅圖可知:
EurekaHttpClientDecorator
採用的是裝飾器模式設計註冊的業務邏輯- 每個
EurekaHttpClientDecorator
的實現類在execute
函數中封裝其特有的業務邏輯 - 最終調
JerseyApplicationClient
實現真正的同Eureka Server通信
每個類的具體作用如下:
SessionEurekaHttpClient
:維持與Eureka Server的會話,同時爲了防止長時間佔用一個Eureka Server和實現Eureka Server的負載均衡,會定期重連Eureka ServerRetryableEurekaHttpClient
:當與某一個Eureka Server通信失敗時,嘗試與集羣中的其他Eureka Server通信,默認嘗試次數爲3,不可在屬性中配置。RetryableEurekaHttpClient
會將失敗的Eureka Server放入其quarantineSet
集合中(該集合的大小超過閾值後會被清空)RedirectingEurekaHttpClient
:處理Eureka Server重定向的業務邏輯MetricsCollectingEurekaClient
:分類統計與Eureka Server通信的異常信息JerseyApplicationClient
:完成與Eureka Server的通信
3. Eureka Client的Regin和Zone
在文章微服務治理1 - Eureka服務治理架構及客戶端裝配和啓動流程分析中,我們看到Eureka Server服務治理架構中有區域親和特性,即Eureka Client可以優先訪問與其處於同一個區域的Eureka Server。通過這種特性,配合實際部署的物理結構,就可以設計出對區域性故障的容錯集羣。在Eureka Client中通過Regin和Zone兩個概念來實現區域親和特性。
使用Region和Zone的一般配置方式如下:
eureka:
client:
service-url:
test1: http://peer3.9003/erureka
test2: http://peer3.9003/erureka
region: test
availability-zones:
test: test1, test2
eureka.client.service-url
是Map<String, String>類行,其中key爲Zone,value爲Eureka Server的地址,多個地址可以用逗號分割;eureka.client.region
是String類型,代表當前Eureka Client的Region,eureka.client.availability-zones
也是Map<String, String>類型,其中key爲Region,value爲Zone,多個Zone用逗號分割。可以看到Region和Zone的關係是一對多。
上一節介紹了當與某一個Eureka Server通信失敗時,RetryableEurekaHttpClient
會嘗試與集羣中的其他Eureka Server通信,其中的關鍵代碼爲:
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
List<EurekaEndpoint> candidateHosts = null;
int endpointIdx = 0;
for (int retry = 0; retry < numberOfRetries; retry++) {
EurekaHttpClient currentHttpClient = delegate.get();
EurekaEndpoint currentEndpoint = null;
if (currentHttpClient == null) {
if (candidateHosts == null) {
(15)candidateHosts = getHostCandidates();
(16)currentEndpoint = candidateHosts.get(endpointIdx++);
currentHttpClient = clientFactory.newClient(currentEndpoint);
}
try {
EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
} catch (Exception e) {
}
}
}
(15) 獲得當前所有可用的Eureka Server
(16) 獲得下一個將要通信的Eureka Server
這裏的關鍵是步驟(15)中通過getHostCandidates()
獲得所有Eureka Server,其通過調用AsyncResolver
類中的getClusterEndpoints()
獲得配置的所有Eureka Server信息,而AsyncResolver
類的初始化則在EurekaHttpClients
類中完成,代碼如下:
static ClosableResolver<AwsEndpoint> defaultBootstrapResolver(final EurekaClientConfig clientConfig, final InstanceInfo myInstanceInfo, final EndpointRandomizer randomizer) {
(17)String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
(18)String myZone = InstanceInfo.getZone(availZones, myInstanceInfo);
(19)ClusterResolver<AwsEndpoint> delegateResolver = new ZoneAffinityClusterResolver(
new ConfigClusterResolver(clientConfig, myInstanceInfo),
myZone,
true,
randomizer
);
(20)List<AwsEndpoint> initialValue = delegateResolver.getClusterEndpoints();
if (initialValue.isEmpty()) {
String msg = "Initial resolution of Eureka server endpoints failed. Check ConfigClusterResolver logs for more info";
logger.error(msg);
failFastOnInitCheck(clientConfig, msg);
}
(21)return new AsyncResolver<>(
EurekaClientNames.BOOTSTRAP,
delegateResolver,
initialValue,
1,
clientConfig.getEurekaServiceUrlPollIntervalSeconds() * 1000
);
}
(17) 根據Region獲得配置的可用Zone,代碼如下:
@Override
public String[] getAvailabilityZones(String region) {
String value = this.availabilityZones.get(region);
if (value == null) {
//默認Zone爲defaultZone,即通過eureka.client.service-url.defaultZone配置的值
value = DEFAULT_ZONE;
}
return value.split(",");
}
如果使用本節開始介紹的一般配置,該步驟將獲得test1, test2
(18) 獲取服務實例所在的Zone,默認返回步驟(17)中獲得的第一個Zone
(19) 初始化ZoneAffinityClusterResolver
類
(20) 獲得配置的Eureka Server集羣節點,這個方法是最關鍵的方法,其代碼如下:
@Override
public List<AwsEndpoint> getClusterEndpoints() {
//獲得配置的Eureka Server集羣節點
List<AwsEndpoint>[] parts = ResolverUtils.splitByZone(delegate.getClusterEndpoints(), myZone);
List<AwsEndpoint> myZoneEndpoints = parts[0];
List<AwsEndpoint> remainingEndpoints = parts[1];
List<AwsEndpoint> randomizedList = randomizeAndMerge(myZoneEndpoints, remainingEndpoints);
if (!zoneAffinity) {
Collections.reverse(randomizedList);
}
logger.debug("Local zone={}; resolved to: {}", myZone, randomizedList);
//返回隨機化後的Eureka Server集羣節點信息
return randomizedList;
}
其中delegate.getClusterEndpoints()
函數最調用的是ConfigClusterResolver
類中的getClusterEndpointsFromConfig()
函數,後者又調用EndpointUtils
類中的getServiceUrlsMapFromConfig
函數以獲得配置的Eureka Server集羣節點信息。
(21) 初始化AsyncResolver
類
以上就是Region和Zone配置信息初始化及生效的代碼分析過程。
附錄
代碼一
該附錄分析一下本文步驟(3)中的instanceInfo
實例的初始化過程。instanceInfo
是類InstanceInfo
類的實例,InstanceInfo
即代表向Eureka Server註冊的服務實例信息,同時也是服務消費者從Eureka Server獲取的服務實例信息,比如應用名、ip地址、端口、homePageUrl、healthCheckUrl、statusPageUrl、securePort等。在DiscoveryClient
實例中instanceInfo
通過ApplicationInfoManager
實例初始化,而後者的實例爲EurekaClientAutoConfiguration
類中向Spring容器注入的bean(可以看下CloudEurekaClient
的初始化過程),代碼如下:
@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class,
search = SearchStrategy.CURRENT)
public ApplicationInfoManager eurekaApplicationInfoManager(
EurekaInstanceConfig config) {
(1)InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
return new ApplicationInfoManager(config, instanceInfo);
}
(1) 通過工廠方法創建InstanceInfo
實例,需要用到注入的EurekaInstanceConfig
實例,其同樣是在EurekaClientAutoConfiguration
類中初始化的,代碼如下:
@Bean
@ConditionalOnMissingBean(value = EurekaInstanceConfig.class,
search = SearchStrategy.CURRENT)
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,
ManagementMetadataProvider managementMetadataProvider) {
String hostname = getProperty("eureka.instance.hostname");
boolean preferIpAddress = Boolean
.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));
String ipAddress = getProperty("eureka.instance.ip-address");
boolean isSecurePortEnabled = Boolean
.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));
String serverContextPath = env.getProperty("server.servlet.context-path", "/");
int serverPort = Integer.parseInt(
env.getProperty("server.port", env.getProperty("port", "8080")));
Integer managementPort = env.getProperty("management.server.port", Integer.class);
String managementContextPath = env
.getProperty("management.server.servlet.context-path");
Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port",
Integer.class);
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
instance.setNonSecurePort(serverPort);
instance.setInstanceId(getDefaultInstanceId(env));
instance.setPreferIpAddress(preferIpAddress);
instance.setSecurePortEnabled(isSecurePortEnabled);
if (StringUtils.hasText(ipAddress)) {
instance.setIpAddress(ipAddress);
}
if (isSecurePortEnabled) {
instance.setSecurePort(serverPort);
}
if (StringUtils.hasText(hostname)) {
instance.setHostname(hostname);
}
String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");
String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");
if (StringUtils.hasText(statusPageUrlPath)) {
instance.setStatusPageUrlPath(statusPageUrlPath);
}
if (StringUtils.hasText(healthCheckUrlPath)) {
instance.setHealthCheckUrlPath(healthCheckUrlPath);
}
ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort,
serverContextPath, managementContextPath, managementPort);
if (metadata != null) {
instance.setStatusPageUrl(metadata.getStatusPageUrl());
instance.setHealthCheckUrl(metadata.getHealthCheckUrl());
if (instance.isSecurePortEnabled()) {
instance.setSecureHealthCheckUrl(metadata.getSecureHealthCheckUrl());
}
Map<String, String> metadataMap = instance.getMetadataMap();
metadataMap.computeIfAbsent("management.port",
k -> String.valueOf(metadata.getManagementPort()));
}
else {
// without the metadata the status and health check URLs will not be set
// and the status page and health check url paths will not include the
// context path so set them here
if (StringUtils.hasText(managementContextPath)) {
instance.setHealthCheckUrlPath(
managementContextPath + instance.getHealthCheckUrlPath());
instance.setStatusPageUrlPath(
managementContextPath + instance.getStatusPageUrlPath());
}
}
setupJmxPort(instance, jmxPort);
return instance;
}
可以看到我們在配置文件中設置的很多屬性最終都會在這裏生效。