在看源碼之前,先說一下標題中提到的三個概念:
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判斷中。