摘要: 原創出處 http://www.iocoder.cn/Eureka/end-point-and-resolver/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
- 1. 概述
- 2. EndPoint
- 3. 解析器
- 3.1 ClusterResolver
- 3.2 ClosableResolver
- 3.3 DnsTxtRecordClusterResolver
- 3.4 ConfigClusterResolver
- 3.5 ZoneAffinityClusterResolver
- 3.6 AsyncResolver
- 4. 初始化解析器
- EndPoint ,服務端點。例如,Eureka-Server 的訪問地址。
- EndPoint 解析器,將配置的 Eureka-Server 的訪問地址解析成 EndPoint 。
- 第一種,直接配置實際訪問地址。例如,
eureka.serviceUrl.defaultZone=http://127.0.0.1:8080/v2
。 - 第二種,基於 DNS 解析出訪問地址。例如,
eureka.shouldUseDns=true
並且eureka.eurekaServer.domainName=eureka.iocoder.cn
。 - 紅色部分 —— EndPoint
- 黃色部分 —— EndPoint 解析器
- 請支持正版。下載盜版,等於主動編寫低級 BUG 。
- 程序猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
- 兩書齊買,京東包郵。
- 重寫了
#equals(...)
和#hashCode(...)
方法,標準實現方式,這裏就不貼代碼了。 - 重寫了
#compareTo(...)
方法,基於serviceUrl
屬性做比較。 - 重寫了
#equals(...)
和#hashCode(...)
方法,標準實現方式,這裏就不貼代碼了。 DnsTxtRecordClusterResolver 通過集羣根地址(
rootClusterDNS
) 解析出 EndPoint 集羣。需要在 DNS 配置兩層解析記錄:- 第一層 :
- 主機記錄 :格式爲
TXT.${REGION}.${自定義二級域名}
。 - 記錄類型 :TXT 記錄類型。
- 記錄值 :第二層的主機記錄。如有多個第二層級,使用空格分隔。
- 主機記錄 :格式爲
- 第二層:
- 主機記錄 :格式爲
TXT.${ZONE}.${自定義二級域名}
或者${ZONE}.${自定義二級域名}
。 - 記錄類型 :TXT 記錄類型。
- 記錄值 :EndPoint 的網絡地址。如有多個 EndPoint,使用空格分隔。
- 舉個例子:
- 舉個例子:
- 主機記錄 :格式爲
- 第一層 :
rootClusterDNS
,集羣根地址。例如:txt.default.eureka.iocoder.cn
,其·txt.default.eureka
爲 DNS 解析記錄的第一層的主機記錄。
region
:地區。需要和rootClusterDNS
的${REGION}
一致。extractZoneFromDNS
:是否解析 DNS 解析記錄的第二層級的主機記錄的${ZONE}
可用區。第 12 至 16 行 :調用
#resolve(rootClusterDNS)
解析第一層 DNS 記錄。實現代碼如下:1: private static Set<String> resolve(String rootClusterDNS) throws NamingException {2: Set<String> result;3: try {4: result = DnsResolver.getCNamesFromTxtRecord(rootClusterDNS);5: // TODO 芋艿:這塊是bug,不需要這一段6: if (!rootClusterDNS.startsWith("txt.")) {7: result = DnsResolver.getCNamesFromTxtRecord("txt." + rootClusterDNS);8: }9: } catch (NamingException e) {10: if (!rootClusterDNS.startsWith("txt.")) {11: result = DnsResolver.getCNamesFromTxtRecord("txt." + rootClusterDNS);12: } else {13: throw e;14: }15: }16: return result;17: }- 第 4 行 : 調用
DnsResolver#getCNamesFromTxtRecord(...)
方法,解析 TXT 主機記錄。點擊鏈接查看帶中文註釋的 DnsResolver 的代碼,比較解析,筆者就不囉嗦了。 - 第 5 至 8 行 :當傳遞參數
rootClusterDNS
不以txt.
開頭時,即使第 4 行解析成功,也會報錯,此時是個 Eureka 的 BUG 。因此,配置 DNS 解析記錄時,主機記錄暫時必須以txt.
開頭。
- 第 4 行 : 調用
第 17 至 25 行 :循環第一層 DNS 記錄的解析結果,進一步解析第二層 DNS 記錄。
- 第 20 行 :解析可用區(
zone
)。 - 第 21 行 :調用
#resolve(rootClusterDNS)
解析第二層 DNS 記錄。
- 第 20 行 :解析可用區(
第 3 至 8 行 :基於 DNS 獲取 EndPoint 集羣,調用
#getClusterEndpointsFromDns()
方法,實現代碼如下:private List<AwsEndpoint> getClusterEndpointsFromDns() {String discoveryDnsName = getDNSName(); // 獲取 集羣根地址int port = Integer.parseInt(clientConfig.getEurekaServerPort()); // 端口// cheap enough so just re-useDnsTxtRecordClusterResolver dnsResolver = new DnsTxtRecordClusterResolver(getRegion(),discoveryDnsName,true, // 解析 zoneport,false,clientConfig.getEurekaServerURLContext());// 調用 DnsTxtRecordClusterResolver 解析 EndPointList<AwsEndpoint> endpoints = dnsResolver.getClusterEndpoints();if (endpoints.isEmpty()) {logger.error("Cannot resolve to any endpoints for the given dnsName: {}", discoveryDnsName);}return endpoints;}private String getDNSName() {return "txt." + getRegion() + '.' + clientConfig.getEurekaServerDNSName();}- 必須配置
eureka.shouldUseDns=true
,開啓基於 DNS 獲取 EndPoint 集羣。 - 必須配置
eureka.eurekaServer.domainName=${xxxxx}
,配置集羣根地址。 - 選填配
eureka.eurekaServer.port
,eureka.eurekaServer.context
。 - 從代碼中我們可以看出,使用 DnsTxtRecordClusterResolver 解析出 EndPoint 集羣。
- 必須配置
第 9 至 13 行 :直接配置文件填寫實際 EndPoint 集羣,調用
#getClusterEndpointsFromConfig()
方法,實現代碼如下:
- 第 3 行 :獲得可用區數組。通過
eureka.${REGION}.availabilityZones
配置。 - 第 5 行 :調用
InstanceInfo#getZone(...)
方法,獲得應用實例自己所在的可用區(zone
)。非亞馬遜 AWS 環境下,可用區數組的第一個元素就是應用實例自己所在的可用區。 第 7 行 :調用
EndpointUtils#getServiceUrlsMapFromConfig(...)
方法,獲得可用區與serviceUrls
的映射。實現代碼如下:// EndpointUtils.java1: public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {2: Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); // key:zone;value:serviceUrls3: // 獲得 應用實例的 地區( region )4: String region = getRegion(clientConfig);5: // 獲得 應用實例的 可用區6: String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());7: if (availZones == null || availZones.length == 0) {8: availZones = new String[1];9: availZones[0] = DEFAULT_ZONE;10: }11: logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones));12: // 獲得 開始位置13: int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);14: // 將 開始位置 的 serviceUrls 添加到結果15: String zone = availZones[myZoneOffset];16: List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);17: if (serviceUrls != null) {18: orderedUrls.put(zone, serviceUrls);19: }20: // 從開始位置順序遍歷剩餘的 serviceUrls 添加到結果21: int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);22: while (currentOffset != myZoneOffset) {23: zone = availZones[currentOffset];24: serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);25: if (serviceUrls != null) {26: orderedUrls.put(zone, serviceUrls);27: }28: if (currentOffset == (availZones.length - 1)) {29: currentOffset = 0;30: } else {31: currentOffset++;32: }33: }34:35: // 爲空,報錯36: if (orderedUrls.size() < 1) {37: throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");38: }39: return orderedUrls;40: }第 13 行 :獲得開始位置。實現代碼如下:
private static int getZoneOffset(String myZone, boolean preferSameZone, String[] availZones) {for (int i = 0; i < availZones.length; i++) {if (myZone != null && (availZones[i].equalsIgnoreCase(myZone.trim()) == preferSameZone)) {return i;}}logger.warn("DISCOVERY: Could not pick a zone based on preferred zone settings. My zone - {}," +" preferSameZone- {}. Defaulting to " + availZones[0], myZone, preferSameZone);return 0;}- 當方法參數
preferSameZone=true
,即eureka.preferSameZone=true
( 默認值 :true
) 時,開始位置爲可用區數組(availZones
)的第一個和應用實例所在的可用區(myZone
)【相等】元素的位置。 - 當方法參數
preferSameZone=false
,即eureka.preferSameZone=false
( 默認值 :true
) 時,開始位置爲可用區數組(availZones
)的第一個和應用實例所在的可用區(myZone
)【不相等】元素的位置。
- 當方法參數
第 20 至 33 行 :從開始位置順序將剩餘的可用區的
serviceUrls
添加到結果。順序理解如下圖:
第 9 至 18 行 :拼裝 EndPoint 集羣結果。
- 屬性
delegate
,委託的解析器。目前代碼裏使用的是 ConfigClusterResolver 。 - 屬性
zoneAffinity
,是否可用區親和。true
:EndPoint 可用區爲本地的優先被放在前面。false
:EndPoint 可用區非本地的優先被放在前面。
- 第 2 行 :調用
ClusterResolver#getClusterEndpoints()
方法,獲得 EndPoint 集羣。再調用ResolverUtils#splitByZone(...)
方法,拆分成本地和非本地的可用區的 EndPoint 集羣,點擊鏈接查看實現。 第 8 行 :調用
#randomizeAndMerge(...)
方法,分別隨機打亂每個 EndPoint 集羣,並進行合併數組,實現代碼如下:// ZoneAffinityClusterResolver.javaprivate static List<AwsEndpoint> randomizeAndMerge(List<AwsEndpoint> myZoneEndpoints, List<AwsEndpoint> remainingEndpoints) {if (myZoneEndpoints.isEmpty()) {return ResolverUtils.randomize(remainingEndpoints); // 打亂}if (remainingEndpoints.isEmpty()) {return ResolverUtils.randomize(myZoneEndpoints); // 打亂}List<AwsEndpoint> mergedList = ResolverUtils.randomize(myZoneEndpoints); // 打亂mergedList.addAll(ResolverUtils.randomize(remainingEndpoints)); // 打亂return mergedList;}// ResolverUtils.javapublic static <T extends EurekaEndpoint> List<T> randomize(List<T> list) {// 數組大小爲 0 或者 1 ,不進行打亂List<T> randomList = new ArrayList<>(list);if (randomList.size() < 2) {return randomList;}// 以本地IP爲隨機種子,有如下好處:// 多個主機,實現對同一個 EndPoint 集羣負載均衡的效果。// 單個主機,同一個 EndPoint 集羣按照固定順序訪問。Eureka-Server 不是強一致性的註冊中心,Eureka-Client 對同一個 Eureka-Server 拉取註冊信息,保證兩者之間增量同步的一致性。Random random = new Random(LOCAL_IPV4_ADDRESS.hashCode());int last = randomList.size() - 1;for (int i = 0; i < last; i++) {int pos = random.nextInt(randomList.size() - i);if (pos != i) {Collections.swap(randomList, i, pos);}}return randomList;}- 注意,
ResolverUtils#randomize(...)
使用以本機IP爲隨機種子,有如下好處:- 多個主機,實現對同一個 EndPoint 集羣負載均衡的效果。
- 單個主機,同一個 EndPoint 集羣按照固定順序訪問。Eureka-Server 不是強一致性的註冊中心,Eureka-Client 對同一個 Eureka-Server 拉取註冊信息,保證兩者之間增量同步的一致性。
- 注意,
第 10 至 12 行 :非可用區親和,將非本地的可用區的 EndPoint 集羣放在前面。
backgroundTask
,後臺任務,定時解析 EndPoint 集羣。- TimedSupervisorTask ,在 《Eureka 源碼解析 —— 應用實例註冊發現(二)之續租》「2.3 TimedSupervisorTask」 有詳細解析。
updateTask
實現代碼如下:private final Runnable updateTask = new Runnable() {public void run() {try {List<T> newList = delegate.getClusterEndpoints(); // 調用 委託的解析器 解析 EndPoint 集羣if (newList != null) {resultsRef.getAndSet(newList);lastLoadTimestamp = System.currentTimeMillis();} else {logger.warn("Delegate returned null list of cluster endpoints");}logger.debug("Resolved to {}", newList);} catch (Exception e) {logger.warn("Failed to retrieve cluster endpoints from the delegate", e);}}};delegate
,委託的解析器,目前代碼爲 ZoneAffinityClusterResolver。
後臺任務的發起在
#getClusterEndpoints()
方法,在 「3.6.2 解析 EndPoint 集羣」 詳細解析。
第 5 至 9 行 :若未預熱解析 EndPoint 集羣結果,調用
#doWarmUp()
方法,進行預熱。若預熱失敗,取消定時任務的第一次延遲。#doWarmUp()
方法實現代碼如下:boolean doWarmUp() {Future future = null;try {future = threadPoolExecutor.submit(updateTask);future.get(warmUpTimeoutMs, TimeUnit.MILLISECONDS); // block until done or timeoutreturn true;} catch (Exception e) {logger.warn("Best effort warm up failed", e);} finally {if (future != null) {future.cancel(true);}}return false;}- 調用
updateTask
,解析 EndPoint 集羣。
- 調用
第 10 至 13 行 : 若未調度定時任務,進行調度,調用
#scheduleTask()
方法,實現代碼如下:
void scheduleTask(long delay) {executorService.schedule(backgroundTask, delay, TimeUnit.MILLISECONDS);}- x
第 15 行 :返回 EndPoint 集羣。當第一次預熱失敗,會返回空,直到定時任務獲得到結果。
調用
EurekaHttpClients#newBootstrapResolver(...)
方法,創建 EndPoint 解析器,實現代碼如下:1: public static final String COMPOSITE_BOOTSTRAP_STRATEGY = "composite";2:3: public static ClosableResolver<AwsEndpoint> newBootstrapResolver(4: final EurekaClientConfig clientConfig,5: final EurekaTransportConfig transportConfig,6: final TransportClientFactory transportClientFactory,7: final InstanceInfo myInstanceInfo,8: final ApplicationsResolver.ApplicationsSource applicationsSource)9: {10: if (COMPOSITE_BOOTSTRAP_STRATEGY.equals(transportConfig.getBootstrapResolverStrategy())) {11: if (clientConfig.shouldFetchRegistry()) {12: return compositeBootstrapResolver(13: clientConfig,14: transportConfig,15: transportClientFactory,16: myInstanceInfo,17: applicationsSource18: );19: } else {20: logger.warn("Cannot create a composite bootstrap resolver if registry fetch is disabled." +21: " Falling back to using a default bootstrap resolver.");22: }23: }24:25: // if all else fails, return the default26: return defaultBootstrapResolver(clientConfig, myInstanceInfo);27: }28:29: /**30: * @return a bootstrap resolver that resolves eureka server endpoints based on either DNS or static config,31: * depending on configuration for one or the other. This resolver will warm up at the start.32: */33: static ClosableResolver<AwsEndpoint> defaultBootstrapResolver(final EurekaClientConfig clientConfig,34: final InstanceInfo myInstanceInfo) {35: // 獲得 可用區集合36: String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());37: // 獲得 應用實例的 可用區38: String myZone = InstanceInfo.getZone(availZones, myInstanceInfo);39:40: // 創建 ZoneAffinityClusterResolver41: ClusterResolver<AwsEndpoint> delegateResolver = new ZoneAffinityClusterResolver(42: new ConfigClusterResolver(clientConfig, myInstanceInfo),43: myZone,44: true45: );46:47: // 第一次 EndPoint 解析48: List<AwsEndpoint> initialValue = delegateResolver.getClusterEndpoints();49:50: // 解析不到 Eureka-Server EndPoint ,快速失敗51: if (initialValue.isEmpty()) {52: String msg = "Initial resolution of Eureka server endpoints failed. Check ConfigClusterResolver logs for more info";53: logger.error(msg);54: failFastOnInitCheck(clientConfig, msg);55: }56:57: // 創建 AsyncResolver58: return new AsyncResolver<>(59: EurekaClientNames.BOOTSTRAP,60: delegateResolver,61: initialValue,62: 1,63: clientConfig.getEurekaServiceUrlPollIntervalSeconds() * 100064: );65: }
1. 概述
本文主要分享 EndPoint 與 解析器。
目前有多種 Eureka-Server 訪問地址的配置方式,本文只分享 Eureka 1.x 的配置,不包含 Eureka 1.x 對 Eureka 2.x 的兼容配置:
本文涉及類在 com.netflix.discovery.shared.resolver
包下,涉及到主體類的類圖如下( 打開大圖 ):
推薦 Spring Cloud 書籍:
2. EndPoint
2.1 EurekaEndpoint
com.netflix.discovery.shared.resolver.EurekaEndpoint
,Eureka 服務端點接口,實現代碼如下:
|
2.2 DefaultEndpoint
com.netflix.discovery.shared.resolver.DefaultEndpoint
,默認 Eureka 服務端點實現類。實現代碼如下:
|
2.3 AwsEndpoint
com.netflix.discovery.shared.resolver.aws.AwsEndpoint
,基於 region
、zone
的 Eureka 服務端點實現類 ( 請不要在意 AWS 開頭 )。實現代碼如下:
|
3. 解析器
EndPoint 解析器使用委託設計模式實現。所以,上文圖片中我們看到好多個解析器,實際代碼非常非常非常清晰。
FROM 《委託模式》
委託模式是軟件設計模式中的一項基本技巧。在委託模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委託給另一個對象來處理。委託模式是一項基本技巧,許多其他的模式,如狀態模式、策略模式、訪問者模式本質上是在更特殊的場合採用了委託模式。委託模式使得我們可以用聚合來替代繼承,它還使我們可以模擬mixin。
我們在上圖的基礎上,增加委託的關係,如下圖:
3.1 ClusterResolver
com.netflix.discovery.shared.resolver.ClusterResolver
,集羣解析器接口。接口代碼如下:
|
3.2 ClosableResolver
com.netflix.discovery.shared.resolver.ClosableResolver
,可關閉的解析器接口,繼承自 ClusterResolver 接口。接口代碼如下:
|
3.3 DnsTxtRecordClusterResolver
com.netflix.discovery.shared.resolver.aws.DnsTxtRecordClusterResolver
,基於 DNS TXT 記錄類型的集羣解析器。類屬性代碼如下:
|
#getClusterEndpoints(...)
方法,實現代碼如下:
|
3.4 ConfigClusterResolver
com.netflix.discovery.shared.resolver.aws.ConfigClusterResolver
,基於配置文件的集羣解析器。類屬性代碼如下:
|
#getClusterEndpoints(...)
方法,實現代碼如下:
|
|
3.5 ZoneAffinityClusterResolver
com.netflix.discovery.shared.resolver.aws.ZoneAffinityClusterResolver
,使用可用區親和的集羣解析器。類屬性代碼如下:
|
#getClusterEndpoints(...)
方法,實現代碼如下:
|
3.6 AsyncResolver
com.netflix.discovery.shared.resolver.AsyncResolver
,異步執行解析的集羣解析器。AsyncResolver 屬性較多,而且複雜的多,我們拆分到具體方法裏分享。
3.6.1 定時任務
AsyncResolver 內置定時任務,定時刷新 EndPoint 集羣解析結果。
爲什麼要刷新?例如,Eureka-Server 的 serviceUrls
基於 DNS 配置。
定時任務代碼如下:
|
3.6.2 解析 EndPoint 集羣
調用 #getClusterEndpoints()
方法,解析 EndPoint 集羣,實現代碼如下:
|
4. 初始化解析器
Eureka-Client 在初始化時,調用 DiscoveryClient#scheduleServerEndpointTask()
方法,初始化 AsyncResolver 解析器。實現代碼如下:
|
* 第 10 至 23 行 :組合解析器,用於 Eureka 1.x 對 Eureka 2.x 的兼容配置,暫時不需要了解。TODO[0028]寫入集羣和讀取集羣 * 第 26 行 :調用 `#defaultBootstrapResolver()` 方法,創建默認的解析器 AsyncResolver 。 * 第 40 至 45 行 :創建 ZoneAffinityClusterResolver 。在 ZoneAffinityClusterResolver 構造方法的參數,我們看到創建 ConfigClusterResolver 作爲 `delegate` 參數。 * 第 48 行 :調用 `ZoneAffinityClusterResolver#getClusterEndpoints()` 方法,**第一次 Eureka-Server EndPoint 集羣解析**。 * 第 51 至 55 行 :解析不到 Eureka-Server EndPoint 集羣時,可以通過配置( `eureka.experimental.clientTransportFailFastOnInit=true` ),使 Eureka-Client 初始化失敗。`#failFastOnInitCheck(...)` 方法,實現代碼如下:
<figure class="highlight java"><table><tr><td class="code"><pre><div class="line"><span class="comment">// potential future feature, guarding with experimental flag for now</span></div><div class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">failFastOnInitCheck</span><span class="params">(EurekaClientConfig clientConfig, String msg)</span> </span>{</div><div class="line"> <span class="keyword">if</span> (<span class="string">"true"</span>.equals(clientConfig.getExperimental(<span class="string">"clientTransportFailFastOnInit"</span>))) {</div><div class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(msg);</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure> * x
第 58 至 64 行 :創建 AsyncResolver 。從代碼上,我們可以看到,
AsyncResolver.resultsRef
屬性一開始已經用initialValue
傳遞給 AsyncResolver 構造方法。實現代碼如下:
public AsyncResolver(String name,ClusterResolver<T> delegate,List<T> initialValues,int executorThreadPoolSize,int refreshIntervalMs) {this(name,delegate,initialValues,executorThreadPoolSize,refreshIntervalMs,0);// 設置已經預熱warmedUp.set(true);}- x