摘要: 原創出處 http://www.iocoder.cn/Eureka/instance-registry-fetch-all/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
- 1. 概述
- 2. Eureka-Client 發起全量獲取
- 3. Eureka-Server 接收全量獲取
- 請支持正版。下載盜版,等於主動編寫低級 BUG 。
- 程序猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
- 兩書齊買,京東包郵。
com.netflix.discovery.shared.Applications
,註冊的應用集合。較爲容易理解,點擊 鏈接 鏈接查看帶中文註釋的類,這裏就不囉嗦了。Applications 與 InstanceInfo 類關係如下:配置
eureka.shouldFetchRegistry = true
,開啓從 Eureka-Server 獲取註冊信息。默認值:true
。- 調用
#fetchRegistry(false)
方法,從 Eureka-Server 全量獲取註冊信息,在 「2.4 發起獲取註冊信息」 詳細解析。 - 初始化定時任務代碼,和續租的定時任務代碼類似,在 《Eureka 源碼解析 —— 應用實例註冊發現(二)之續租
》 有詳細解析,這裏不重複分享。 com.netflix.discovery.DiscoveryClient.CacheRefreshThread
,註冊信息緩存刷新任務,實現代碼如下:class CacheRefreshThread implements Runnable {public void run() {refreshRegistry();}}- 調用
#refreshRegistry(false)
方法,刷新註冊信息緩存,在 「2.3 刷新註冊信息緩存」 詳細解析。
- 調用
- 第 3 至 28 行 :TODO[0009]:RemoteRegionRegistry
- 第 30 行 :調用
#fetchRegistry(false)
方法,從 Eureka-Server 獲取註冊信息,在 「2.4 發起獲取註冊信息」 詳細解析。 第 31 至 36 行 :獲取註冊信息成功,設置註冊信息的應用實例數,最後獲取註冊信息時間。變量代碼如下:
/*** 註冊信息的應用實例數*/private volatile int registrySize = 0;/*** 最後成功從 Eureka-Server 拉取註冊信息時間戳*/private volatile long lastSuccessfulRegistryFetchTimestamp = -1;第 38 至 53 行 :打印調試日誌。
- 第 54 至 56 行 :打印異常日誌。
第 5 至 8 行 :獲取本地緩存的註冊的應用實例集合,實現代碼如下:
public Applications getApplications() {return localRegionApps.get();}第 10 至 26 行 :全量獲取註冊信息。
- 第 11 行 :配置
eureka.disableDelta = true
,禁用增量獲取註冊信息。默認值:false
。 - 第 12 行 :只獲得一個
vipAddress
對應的應用實例們的註冊信息。 - 第 13 行 :方法參數
forceFullRegistryFetch
強制全量獲取註冊信息。 - 第 14 至 15 行 :本地緩存爲空。
- 第 25 至 26 行 :調用
#getAndStoreFullRegistry()
方法,全量獲取註冊信息,並設置到本地緩存。下文詳細解析。
- 第 11 行 :配置
- 第 27 至 30 行 :增量獲取註冊信息,並刷新本地緩存,在 《Eureka 源碼解析 —— 應用實例註冊發現 (七)之增量獲取》 詳細解析。
- 第 31 至 32 行 :計算應用集合
hashcode
。該變量用於校驗增量獲取的註冊信息和 Eureka-Server 全量的註冊信息是否一致( 完整 ),在 《Eureka 源碼解析 —— 應用實例註冊發現 (七)之增量獲取》 詳細解析。 第 33 至 34 行 :打印調試日誌,輸出本地緩存的註冊的應用實例數量。實現代碼如下:
private void logTotalInstances() {if (logger.isDebugEnabled()) {int totInstances = 0;for (Application application : getApplications().getRegisteredApplications()) {totInstances += application.getInstancesAsIsFromEureka().size();}logger.debug("The total number of all instances in the client now is {}", totInstances);}}
第 44 至 45 行 :觸發 CacheRefreshedEvent 事件,事件監聽器執行。目前 Eureka 未提供默認的該事件監聽器。
#onCacheRefreshed()
方法,實現代碼如下:/*** Eureka 事件監聽器*/private final CopyOnWriteArraySet<EurekaEventListener> eventListeners = new CopyOnWriteArraySet<>();protected void onCacheRefreshed() {fireEvent(new CacheRefreshedEvent());}protected void fireEvent(final EurekaEvent event) {for (EurekaEventListener listener : eventListeners) {listener.onEvent(event);}}- x
筆者的YY :你可以實現自定義的事件監聽器監聽 CacheRefreshedEvent 事件,以達到持久化最新的註冊信息到存儲器( 例如,本地文件 ),通過這樣的方式,配合實現 BackupRegistry 接口讀取存儲器。BackupRegistry 接口調用如下:
// 【3.2.12】從 Eureka-Server 拉取註冊信息if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {fetchRegistryFromBackup();}
第47 至 48 行 :更新本地緩存的當前應用實例在 Eureka-Server 的狀態。
1: private volatile InstanceInfo.InstanceStatus lastRemoteInstanceStatus = InstanceInfo.InstanceStatus.UNKNOWN;2:3: private synchronized void updateInstanceRemoteStatus() {4: // Determine this instance's status for this app and set to UNKNOWN if not found5: InstanceInfo.InstanceStatus currentRemoteInstanceStatus = null;6: if (instanceInfo.getAppName() != null) {7: Application app = getApplication(instanceInfo.getAppName());8: if (app != null) {9: InstanceInfo remoteInstanceInfo = app.getByInstanceId(instanceInfo.getId());10: if (remoteInstanceInfo != null) {11: currentRemoteInstanceStatus = remoteInstanceInfo.getStatus();12: }13: }14: }15: if (currentRemoteInstanceStatus == null) {16: currentRemoteInstanceStatus = InstanceInfo.InstanceStatus.UNKNOWN;17: }18:19: // Notify if status changed20: if (lastRemoteInstanceStatus != currentRemoteInstanceStatus) {21: onRemoteStatusChanged(lastRemoteInstanceStatus, currentRemoteInstanceStatus);22: lastRemoteInstanceStatus = currentRemoteInstanceStatus;23: }24: }- 第 4 至 14 行 :從註冊信息中獲取當前應用在 Eureka-Server 的狀態。
第 19 至 23 行 :對比本地緩存和最新的的當前應用實例在 Eureka-Server 的狀態,若不同,更新本地緩存( 注意,只更新該緩存變量,不更新本地當前應用實例的狀態(
instanceInfo.status
) ),觸發 StatusChangeEvent 事件,事件監聽器執行。目前 Eureka 未提供默認的該事件監聽器。#onRemoteStatusChanged(...)
實現代碼如下:protected void onRemoteStatusChanged(InstanceInfo.InstanceStatus oldStatus, InstanceInfo.InstanceStatus newStatus) {fireEvent(new StatusChangeEvent(oldStatus, newStatus));}- Eureka-Client 本地應用實例與 Eureka-Server 的該應用實例狀態不同的原因,因爲應用實例的覆蓋狀態,在 《Eureka 源碼解析 —— 應用實例註冊發現 (八)之覆蓋狀態》 有詳細解析。
第 6 至 14 行 :全量獲取註冊信息,實現代碼如下:
// AbstractJerseyEurekaHttpClient.javapublic EurekaHttpResponse<Applications> getApplications(String... regions) {return getApplicationsInternal("apps/", regions);}private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {ClientResponse response = null;String regionsParamValue = null;try {WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath);if (regions != null && regions.length > 0) {regionsParamValue = StringUtil.join(regions);webResource = webResource.queryParam("regions", regionsParamValue);}Builder requestBuilder = webResource.getRequestBuilder();addExtraHeaders(requestBuilder);response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class); // JSONApplications applications = null;if (response.getStatus() == Status.OK.getStatusCode() && response.hasEntity()) {applications = response.getEntity(Applications.class);}return anEurekaHttpResponse(response.getStatus(), Applications.class).headers(headersOf(response)).entity(applications).build();} finally {if (logger.isDebugEnabled()) {logger.debug("Jersey HTTP GET {}/{}?{}; statusCode={}",serviceUrl, urlPath,regionsParamValue == null ? "" : "regions=" + regionsParamValue,response == null ? "N/A" : response.getStatus());}if (response != null) {response.close();}}}- 調用
AbstractJerseyEurekaHttpClient#getApplications(...)
方法,GET 請求 Eureka-Server 的apps/
接口,參數爲regions
,返回格式爲 JSON ,實現全量獲取註冊信息。
- 調用
第 16 至 24 行 :設置到本地註冊信息緩存。
- 第 19 行 :TODO[0025] :併發更新的情況???
- 第 20 行 :調用
#filterAndShuffle(...)
方法,根據配置eureka.shouldFilterOnlyUpInstances = true
( 默認值 :true
) 過濾只保留狀態爲開啓( UP )的應用實例,並隨機打亂應用實例順序。打亂後,實現調用應用服務的隨機性。代碼比較易懂,點擊鏈接查看方法實現。
- 第 8 至 17 行 :TODO[0009]:RemoteRegionRegistry
- 第 19 至 25 行 :Eureka-Server 啓動完成,但是未處於就緒( Ready )狀態,不接受請求全量應用註冊信息的請求,例如,Eureka-Server 啓動時,未能從其他 Eureka-Server 集羣的節點獲取到應用註冊信息。
第 27 至 28 行 :設置 API 版本號。默認最新 API 版本爲 V2。實現代碼如下:
public enum Version {V1, V2;public static Version toEnum(String v) {for (Version version : Version.values()) {if (version.name().equalsIgnoreCase(v)) {return version;}}//Defaults to v2return V2;}}第 30 至 36 行 :設置返回數據格式,默認 JSON 。
- 第 38 至 42 行 :創建響應緩存( ResponseCache ) 的鍵( KEY ),在 「3.2.1 緩存鍵」詳細解析。
- 第 44 至 55 行 :從響應緩存讀取全量註冊信息,在 「3.3 緩存讀取」詳細解析。
其中,
#getVersionDelta()
和#getVersionDeltaWithRegions()
已經廢棄。這裏保留的原因主要是考慮兼容性。判斷依據來自如下代碼:// Applications.javapublic void setVersion(Long version) {this.versionDelta = version;}// AbstractInstanceRegistry.javapublic Applications getApplicationDeltas() {// ... 省略其它無關代碼apps.setVersion(responseCache.getVersionDelta().get()); // 唯一調用到 ResponseCache#getVersionDelta() 方法的地方// ... 省略其它無關代碼}#get()
:獲得緩存。#getGZIP()
:獲得緩存,並 GZIP 。#invalidate()
:過期緩存。- 只讀緩存(
readOnlyCacheMap
) - 固定過期 + 固定大小的讀寫緩存(
readWriteCacheMap
) - 應用實例註冊、下線、過期時,只只只過期
readWriteCacheMap
。 readWriteCacheMap
寫入一段時間( 可配置 )後自動過期。- 定時任務對比
readWriteCacheMap
和readOnlyCacheMap
的緩存值,若不一致,以前者爲主。通過這樣的方式,實現了readOnlyCacheMap
的定時過期。 - 第 5 至 7 行 :調用
#get(key, useReadOnlyCache)
方法,讀取緩存。其中shouldUseReadOnlyResponseCache
通過配置eureka.shouldUseReadOnlyResponseCache = true
(默認值 :true
) 開啓只讀緩存。如果你對數據的一致性有相對高的要求,可以關閉這個開關,當然因爲少了readOnlyCacheMap
,性能會有一定的下降。 第 9 至 16 行 :調用
getValue(key, useReadOnlyCache)
方法,讀取緩存。從readOnlyCacheMap
和readWriteCacheMap
變量可以看到緩存值的類爲com.netflix.eureka.registry.ResponseCacheImpl.Value
,實現代碼如下:public class Value {/*** 原始值*/private final String payload;/*** GZIP 壓縮後的值*/private byte[] gzipped;public Value(String payload) {this.payload = payload;if (!EMPTY_PAYLOAD.equals(payload)) {// ... 省略 GZIP 壓縮代碼gzipped = bos.toByteArray();} else {gzipped = null;}}public String getPayload() {return payload;}public byte[] getGzipped() {return gzipped;}}第 21 至 31 行 :讀取緩存。
- 第 21 至 28 行 :先讀取
readOnlyCacheMap
。讀取不到,讀取readWriteCacheMap
,並設置到readOnlyCacheMap
。 - 第 29 至 31 行 :讀取
readWriteCacheMap
。 readWriteCacheMap
實現代碼如下:this.readWriteCacheMap =CacheBuilder.newBuilder().initialCapacity(1000).expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS).removalListener(new RemovalListener<Key, Value>() {public void onRemoval(RemovalNotification<Key, Value> notification) {// TODO[0009]:RemoteRegionRegistryKey removedKey = notification.getKey();if (removedKey.hasRegions()) {Key cloneWithNoRegions = removedKey.cloneWithoutRegions();regionSpecificKeys.remove(cloneWithNoRegions, removedKey);}}}).build(new CacheLoader<Key, Value>() {public Value load(Key key) throws Exception {// // TODO[0009]:RemoteRegionRegistryif (key.hasRegions()) {Key cloneWithNoRegions = key.cloneWithoutRegions();regionSpecificKeys.put(cloneWithNoRegions, key);}Value value = generatePayload(key);return value;}});readWriteCacheMap
最大緩存數量爲 1000 。- 調用
#generatePayload(key)
方法,生成緩存值。
- 第 21 至 28 行 :先讀取
#generatePayload(key)
方法,實現代碼如下:
1: private Value generatePayload(Key key) {2: Stopwatch tracer = null;3: try {4: String payload;5: switch (key.getEntityType()) {6: case Application:7: boolean isRemoteRegionRequested = key.hasRegions();8:9: if (ALL_APPS.equals(key.getName())) {10: if (isRemoteRegionRequested) { // TODO[0009]:RemoteRegionRegistry11: tracer = serializeAllAppsWithRemoteRegionTimer.start();12: payload = getPayLoad(key, registry.getApplicationsFromMultipleRegions(key.getRegions()));13: } else {14: tracer = serializeAllAppsTimer.start();15: payload = getPayLoad(key, registry.getApplications());16: }17: } else if (ALL_APPS_DELTA.equals(key.getName())) {18: // ... 省略增量獲取相關的代碼19: } else {20: tracer = serializeOneApptimer.start();21: payload = getPayLoad(key, registry.getApplication(key.getName()));22: }23: break;24: // ... 省略部分代碼25: }26: return new Value(payload);27: } finally {28: if (tracer != null) {29: tracer.stop();30: }31: }32: }- 第 10 至 12 行 :TODO[0009]:RemoteRegionRegistry
- 第 13 至 16 行 :調用
AbstractInstanceRegistry#getApplications()
方法,獲得註冊的應用集合。後調用#getPayLoad()
方法,將註冊的應用集合轉換成緩存值。�� 這兩個方法代碼較多,下面詳細解析。 - 第 17 至 18 行 :獲取增量註冊信息的緩存值,在 《Eureka 源碼解析 —— 應用實例註冊發現 (七)之增量獲取》 詳細解析。
- 第 6 至 8 行 :TODO[0009]:RemoteRegionRegistry
第 9 至 16 行 :調用
#getApplicationsFromMultipleRegions(...)
方法,獲得註冊的應用集合,實現代碼如下:1: public Applications getApplicationsFromMultipleRegions(String[] remoteRegions) {2: // TODO[0009]:RemoteRegionRegistry3: boolean includeRemoteRegion = null != remoteRegions && remoteRegions.length != 0;4: logger.debug("Fetching applications registry with remote regions: {}, Regions argument {}",5: includeRemoteRegion, Arrays.toString(remoteRegions));6: if (includeRemoteRegion) {7: GET_ALL_WITH_REMOTE_REGIONS_CACHE_MISS.increment();8: } else {9: GET_ALL_CACHE_MISS.increment();10: }11: // 獲得獲得註冊的應用集合12: Applications apps = new Applications();13: apps.setVersion(1L);14: for (Entry<String, Map<String, Lease<InstanceInfo>>> entry : registry.entrySet()) {15: Application app = null;16:17: if (entry.getValue() != null) {18: for (Entry<String, Lease<InstanceInfo>> stringLeaseEntry : entry.getValue().entrySet()) {19: Lease<InstanceInfo> lease = stringLeaseEntry.getValue();20: if (app == null) {21: app = new Application(lease.getHolder().getAppName());22: }23: app.addInstance(decorateInstanceInfo(lease));24: }25: }26: if (app != null) {27: apps.addApplication(app);28: }29: }30: // TODO[0009]:RemoteRegionRegistry31: if (includeRemoteRegion) {32: for (String remoteRegion : remoteRegions) {33: RemoteRegionRegistry remoteRegistry = regionNameVSRemoteRegistry.get(remoteRegion);34: if (null != remoteRegistry) {35: Applications remoteApps = remoteRegistry.getApplications();36: for (Application application : remoteApps.getRegisteredApplications()) {37: if (shouldFetchFromRemoteRegistry(application.getName(), remoteRegion)) {38: logger.info("Application {} fetched from the remote region {}",39: application.getName(), remoteRegion);40:41: Application appInstanceTillNow = apps.getRegisteredApplications(application.getName());42: if (appInstanceTillNow == null) {43: appInstanceTillNow = new Application(application.getName());44: apps.addApplication(appInstanceTillNow);45: }46: for (InstanceInfo instanceInfo : application.getInstances()) {47: appInstanceTillNow.addInstance(instanceInfo);48: }49: } else {50: logger.debug("Application {} not fetched from the remote region {} as there exists a "51: + "whitelist and this app is not in the whitelist.",52: application.getName(), remoteRegion);53: }54: }55: } else {56: logger.warn("No remote registry available for the remote region {}", remoteRegion);57: }58: }59: }60: // 設置 應用集合 hashcode61: apps.setAppsHashCode(apps.getReconcileHashCode());62: return apps;63: }- 第 2 至 第 10 行 :TODO[0009]:RemoteRegionRegistry
- 第 11 至 29 行 :獲得獲得註冊的應用集合。
- 第 30 至 59 行 :TODO[0009]:RemoteRegionRegistry
- 第 61 行 :計算應用集合
hashcode
。該變量用於校驗增量獲取的註冊信息和 Eureka-Server 全量的註冊信息是否一致( 完整 ),在 《Eureka 源碼解析 —— 應用實例註冊發現 (七)之增量獲取》 詳細解析。
調用
#invalidate(keys)
方法,逐個過期每個緩存鍵值,實現代碼如下:public void invalidate(Key... keys) {for (Key key : keys) {logger.debug("Invalidating the response cache key : {} {} {} {}, {}", key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());// 過期讀寫緩存readWriteCacheMap.invalidate(key);// TODO[0009]:RemoteRegionRegistryCollection<Key> keysWithRegions = regionSpecificKeys.get(key);if (null != keysWithRegions && !keysWithRegions.isEmpty()) {for (Key keysWithRegion : keysWithRegions) {logger.debug("Invalidating the response cache key : {} {} {} {} {}",key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());readWriteCacheMap.invalidate(keysWithRegion);}}}}- 配置
eureka.responseCacheAutoExpirationInSeconds
,設置寫入過期時長。默認值 :180 秒。 - 第 7 至 12 行 :初始化定時任務。配置
eureka.responseCacheUpdateIntervalMs
,設置任務執行頻率,默認值 :30 * 1000 毫秒。 - 第 17 至 39 行 :創建定時任務。
- 第 22 行 :循環
readOnlyCacheMap
的緩存鍵。爲什麼不循環readWriteCacheMap
呢?readOnlyCacheMap
的緩存過期依賴readWriteCacheMap
,因此緩存鍵會更多。 - 第 28 行 至 33 行 :對比
readWriteCacheMap
和readOnlyCacheMap
的緩存值,若不一致,以前者爲主。通過這樣的方式,實現了readOnlyCacheMap
的定時過期。
- 第 22 行 :循環
1. 概述
本文主要分享 Eureka-Client 向 Eureka-Server 獲取全量註冊信息的過程。
Eureka-Client 獲取註冊信息,分成全量獲取和增量獲取。默認配置下,Eureka-Client 啓動時,首先執行一次全量獲取進行本地緩存註冊信息,而後每 30 秒增量獲取刷新本地緩存( 非“正常”情況下會是全量獲取 )。
本文重點在於全量獲取。
推薦 Spring Cloud 書籍:
2. Eureka-Client 發起全量獲取
本小節調用關係如下:
2.1 初始化全量獲取
Eureka-Client 啓動時,首先執行一次全量獲取進行本地緩存註冊信息,首先代碼如下:
|
2.2 定時獲取
Eureka-Client 在初始化過程中,創建獲取註冊信息線程,固定間隔向 Eureka-Server 發起獲取註冊信息( fetch ),刷新本地註冊信息緩存。實現代碼如下:
|
2.3 刷新註冊信息緩存
調用 #refreshRegistry(false)
方法,刷新註冊信息緩存,實現代碼如下:
|
2.4 發起獲取註冊信息
調用 #fetchRegistry(false)
方法,從 Eureka-Server 獲取註冊信息( 根據條件判斷,可能是全量,也可能是增量 ),實現代碼如下:
|
2.4.1 全量獲取註冊信息,並設置到本地緩存
調用 #getAndStoreFullRegistry()
方法,全量獲取註冊信息,並設置到本地緩存。下實現代碼如下:
|
3. Eureka-Server 接收全量獲取
3.1 接收全量獲取請求
com.netflix.eureka.resources.ApplicationsResource
,處理所有應用的請求操作的 Resource ( Controller )。
接收全量獲取請求,映射 ApplicationsResource#getContainers()
方法,實現代碼如下:
|
3.2 響應緩存 ResponseCache
com.netflix.eureka.registry.ResponseCache
,響應緩存接口,接口代碼如下:
|
3.2.1 緩存鍵
com.netflix.eureka.registry.Key
,緩存鍵。實現代碼如下:
|
3.2.2 響應緩存實現類
com.netflix.eureka.registry.ResponseCacheImpl
,響應緩存實現類。
在 ResponseCacheImpl 裏,將緩存拆分成兩層 :
默認配置下,緩存讀取策略如下:
緩存過期策略如下:
注意:應用實例註冊、下線、過期時,不會很快刷新到 readWriteCacheMap
緩存裏。默認配置下,最大延遲在 30 秒。
爲什麼可以使用緩存?
在 CAP 的選擇上,Eureka 選擇了 AP ,不同於 Zookeeper 選擇了 CP 。
推薦閱讀:
3.3 緩存讀取
調用 ResponseCacheImpl#get(...)
方法( #getGzip(...)
類似 ),讀取緩存,實現代碼如下:
|
3.3.1 獲得註冊的應用集合
調用 AbstractInstanceRegistry#getApplications()
方法,獲得註冊的應用集合,實現代碼如下:
|
3.3.2 轉換成緩存值
調用 #getPayLoad()
方法,將註冊的應用集合轉換成緩存值,實現代碼如下:
|
3.4 主動過期讀寫緩存
應用實例註冊、下線、過期時,調用 ResponseCacheImpl#invalidate()
方法,主動過期讀寫緩存( readWriteCacheMap
),實現代碼如下:
|
3.5 被動過期讀寫緩存
讀寫緩存( readWriteCacheMap
) 寫入後,一段時間自動過期,實現代碼如下:
|
3.6 定時刷新只讀緩存
定時任務對比 readWriteCacheMap
和 readOnlyCacheMap
的緩存值,若不一致,以前者爲主。通過這樣的方式,實現了 readOnlyCacheMap
的定時過期。實現代碼如下:
|