Eureka 源碼解析 —— EndPoint 與 解析器

摘要: 原創出處 http://www.iocoder.cn/Eureka/end-point-and-resolver/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

本文主要基於 Eureka 1.8.X 版本

  • 1. 概述
  • 2. EndPoint

  • 3. 解析器
  • 4. 初始化解析器


  • 1. 概述

    本文主要分享 EndPoint 與 解析器

    • EndPoint ,服務端點。例如,Eureka-Server 的訪問地址。
    • EndPoint 解析器,將配置的 Eureka-Server 的訪問地址解析成 EndPoint 。

    目前有多種 Eureka-Server 訪問地址的配置方式,本文只分享 Eureka 1.x 的配置,不包含 Eureka 1.x 對 Eureka 2.x 的兼容配置:

    • 第一種,直接配置實際訪問地址。例如,eureka.serviceUrl.defaultZone=http://127.0.0.1:8080/v2
    • 第二種,基於 DNS 解析出訪問地址。例如,eureka.shouldUseDns=true 並且 eureka.eurekaServer.domainName=eureka.iocoder.cn

    本文涉及類在 com.netflix.discovery.shared.resolver 包下,涉及到主體類的類圖如下( 打開大圖 ):

    • 紅色部分 —— EndPoint
    • 黃色部分 —— EndPoint 解析器

    推薦 Spring Cloud 書籍

    2. EndPoint

    2.1 EurekaEndpoint

    com.netflix.discovery.shared.resolver.EurekaEndpoint ,Eureka 服務端點接口,實現代碼如下:


    public interface EurekaEndpoint extends Comparable<Object> {
    /
    * @return 完整的服務 URL
    /
    String getServiceUrl();
    /
    @deprecated use {@link #getNetworkAddress()}
    /
    @Deprecated
    String getHostName();
    /
    @return 網絡地址
    /
    String getNetworkAddress();
    /
    @return 端口
    /
    int getPort();
    /
    @return 是否安全( https )
    /
    boolean isSecure();
    /
    @return 相對路徑
    */
    String getRelativeUri();
    }

    2.2 DefaultEndpoint

    com.netflix.discovery.shared.resolver.DefaultEndpoint ,默認 Eureka 服務端點實現類。實現代碼如下:


    public class DefaultEndpoint implements EurekaEndpoint {
    /
    * 網絡地址
    /
    protected final String networkAddress;
    /
    端口
    /
    protected final int port;
    /
    是否安全( https )
    /
    protected final boolean isSecure;
    /
    相對地址
    /
    protected final String relativeUri;
    /*
    * 完整的服務 URL
    */
    protected final String serviceUrl;
    public DefaultEndpoint(String serviceUrl) {
    this.serviceUrl = serviceUrl;
    // 將 serviceUrl 分解成 幾個屬性
    try {
    URL url = new URL(serviceUrl);
    this.networkAddress = url.getHost();
    this.port = url.getPort();
    this.isSecure = "https".equals(url.getProtocol());
    this.relativeUri = url.getPath();
    } catch (Exception e) {
    throw new IllegalArgumentException("Malformed serviceUrl: " + serviceUrl);
    }
    }
    public DefaultEndpoint(String networkAddress, int port, boolean isSecure, String relativeUri) {
    this.networkAddress = networkAddress;
    this.port = port;
    this.isSecure = isSecure;
    this.relativeUri = relativeUri;
    // 幾個屬性 拼接成 serviceUrl
    StringBuilder sb = new StringBuilder().append(isSecure ? "https" : "http").append("://").append(networkAddress);
    if (port >= 0) {
    sb.append(':').append(port);
    }
    if (relativeUri != null) {
    if (!relativeUri.startsWith("/")) {
    sb.append('/');
    }
    sb.append(relativeUri);
    }
    this.serviceUrl = sb.toString();
    }
    }
    • 重寫了 #equals(...)#hashCode(...) 方法,標準實現方式,這裏就不貼代碼了。
    • 重寫了 #compareTo(...) 方法,基於 serviceUrl 屬性做比較。

    2.3 AwsEndpoint

    com.netflix.discovery.shared.resolver.aws.AwsEndpoint ,基於 regionzone 的 Eureka 服務端點實現類 ( 請不要在意 AWS 開頭 )。實現代碼如下:


    public class AwsEndpoint extends DefaultEndpoint {
    /
    * 區域
    /
    protected final String region;
    /
    可用區
    */
    protected final String zone;
    }
    • 重寫了 #equals(...)#hashCode(...) 方法,標準實現方式,這裏就不貼代碼了。

    3. 解析器

    EndPoint 解析器使用委託設計模式實現。所以,上文圖片中我們看到好多個解析器,實際代碼非常非常非常清晰

    FROM 《委託模式》
    委託模式是軟件設計模式中的一項基本技巧。在委託模式中,有兩個對象參與處理同一個請求,接受請求的對象將請求委託給另一個對象來處理。委託模式是一項基本技巧,許多其他的模式,如狀態模式、策略模式、訪問者模式本質上是在更特殊的場合採用了委託模式。委託模式使得我們可以用聚合來替代繼承,它還使我們可以模擬mixin。

    我們在上圖的基礎上,增加委託的關係,如下圖:

    3.1 ClusterResolver

    com.netflix.discovery.shared.resolver.ClusterResolver ,集羣解析器接口。接口代碼如下:


    public interface ClusterResolver<T extends EurekaEndpoint> {
    /
    * @return 地區
    /
    String getRegion();
    /
    @return EndPoint 集羣( 數組 )
    */
    List<T> getClusterEndpoints();
    }

    3.2 ClosableResolver

    com.netflix.discovery.shared.resolver.ClosableResolver可關閉的解析器接口,繼承自 ClusterResolver 接口。接口代碼如下:


    public interface ClosableResolver<T extends EurekaEndpoint> extends ClusterResolver<T> {
    /*
    關閉
    */
    void shutdown();
    }

    3.3 DnsTxtRecordClusterResolver

    com.netflix.discovery.shared.resolver.aws.DnsTxtRecordClusterResolver ,基於 DNS TXT 記錄類型的集羣解析器。類屬性代碼如下:


    public class DnsTxtRecordClusterResolver implements ClusterResolver<AwsEndpoint> {
    /
    * 地區
    /
    private final String region;
    /
    集羣根地址,例如 txt.default.eureka.iocoder.cn
    /
    private final String rootClusterDNS;
    /
    是否解析可用區( zone )
    /
    private final boolean extractZoneFromDNS;
    /
    端口
    /
    private final int port;
    /
    是否安全
    /
    private final boolean isSecure;
    /
    相對地址
    */
    private final String relativeUri;
    }
    • 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} 可用區。

    #getClusterEndpoints(...) 方法,實現代碼如下:

    1: @Override
    2: public List<AwsEndpoint> getClusterEndpoints() {
    3: List<AwsEndpoint> eurekaEndpoints = resolve(region, rootClusterDNS, extractZoneFromDNS, port, isSecure, relativeUri);
    4: if (logger.isDebugEnabled()) {
    5: logger.debug("Resolved {} to {}", rootClusterDNS, eurekaEndpoints);
    6: }
    7: return eurekaEndpoints;
    8: }
    9:
    10: private static List<AwsEndpoint> resolve(String region, String rootClusterDNS, boolean extractZone, int port, boolean isSecure, String relativeUri) {
    11: try {
    12: // 解析 第一層 DNS 記錄
    13: Set<String> zoneDomainNames = resolve(rootClusterDNS);
    14: if (zoneDomainNames.isEmpty()) {
    15: throw new ClusterResolverException("Cannot resolve Eureka cluster addresses; there are no data in TXT record for DN " + rootClusterDNS);
    16: }
    17: // 記錄 第二層 DNS 記錄
    18: List<AwsEndpoint> endpoints = new ArrayList<>();
    19: for (String zoneDomain : zoneDomainNames) {
    20: String zone = extractZone ? ResolverUtils.extractZoneFromHostName(zoneDomain) : null; //
    21: Set<String> zoneAddresses = resolve(zoneDomain);
    22: for (String address : zoneAddresses) {
    23: endpoints.add(new AwsEndpoint(address, port, isSecure, relativeUri, region, zone));
    24: }
    25: }
    26: return endpoints;
    27: } catch (NamingException e) {
    28: throw new ClusterResolverException("Cannot resolve Eureka cluster addresses for root: " + rootClusterDNS, e);
    29: }
    30: }

    • 第 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. 開頭。

    • 第 17 至 25 行 :循環第一層 DNS 記錄的解析結果,進一步解析第二層 DNS 記錄。

      • 第 20 行 :解析可用區( zone )。
      • 第 21 行 :調用 #resolve(rootClusterDNS) 解析第二層 DNS 記錄。


    3.4 ConfigClusterResolver

    com.netflix.discovery.shared.resolver.aws.ConfigClusterResolver ,基於配置文件的集羣解析器。類屬性代碼如下:


    public class ConfigClusterResolver implements ClusterResolver<AwsEndpoint> {
    private final EurekaClientConfig clientConfig;
    private final InstanceInfo myInstanceInfo;
    public ConfigClusterResolver(EurekaClientConfig clientConfig, InstanceInfo myInstanceInfo) {
    this.clientConfig = clientConfig;
    this.myInstanceInfo = myInstanceInfo;
    }
    }

    #getClusterEndpoints(...) 方法,實現代碼如下:

    1: @Override
    2: public List<AwsEndpoint> getClusterEndpoints() {
    3: // 使用 DNS 獲取 EndPoint
    4: if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
    5: if (logger.isInfoEnabled()) {
    6: logger.info("Resolving eureka endpoints via DNS: {}", getDNSName());
    7: }
    8: return getClusterEndpointsFromDns();
    9: } else {
    10: // 直接配置實際訪問地址
    11: logger.info("Resolving eureka endpoints via configuration");
    12: return getClusterEndpointsFromConfig();
    13: }
    14: }

    • 第 3 至 8 行 :基於 DNS 獲取 EndPoint 集羣,調用 #getClusterEndpointsFromDns() 方法,實現代碼如下:

      private List<AwsEndpoint> getClusterEndpointsFromDns() {
      String discoveryDnsName = getDNSName(); // 獲取 集羣根地址
      int port = Integer.parseInt(clientConfig.getEurekaServerPort()); // 端口
      // cheap enough so just re-use
      DnsTxtRecordClusterResolver dnsResolver = new DnsTxtRecordClusterResolver(
      getRegion(),
      discoveryDnsName,
      true, // 解析 zone
      port,
      false,
      clientConfig.getEurekaServerURLContext()
      );
      // 調用 DnsTxtRecordClusterResolver 解析 EndPoint
      List<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.porteureka.eurekaServer.context
      • 從代碼中我們可以看出,使用 DnsTxtRecordClusterResolver 解析出 EndPoint 集羣。

    • 第 9 至 13 行 :直接配置文件填寫實際 EndPoint 集羣,調用 #getClusterEndpointsFromConfig() 方法,實現代碼如下:



    1: private List<AwsEndpoint> getClusterEndpointsFromConfig() {
    2: // 獲得 可用區
    3: String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
    4: // 獲取 應用實例自己 的 可用區
    5: String myZone = InstanceInfo.getZone(availZones, myInstanceInfo);
    6: // 獲得 可用區與 serviceUrls 的映射
    7: Map<String, List<String>> serviceUrls = EndpointUtils.getServiceUrlsMapFromConfig(clientConfig, myZone, clientConfig.shouldPreferSameZoneEureka());
    8: // 拼裝 EndPoint 集羣結果
    9: List<AwsEndpoint> endpoints = new ArrayList<>();
    10: for (String zone : serviceUrls.keySet()) {
    11: for (String url : serviceUrls.get(zone)) {
    12: try {
    13: endpoints.add(new AwsEndpoint(url, getRegion(), zone));
    14: } catch (Exception ignore) {
    15: logger.warn("Invalid eureka server URI: {}; removing from the server pool", url);
    16: }
    17: }
    18: }
    19:
    20: // 打印日誌,EndPoint 集羣
    21: if (logger.isDebugEnabled()) {
    22: logger.debug("Config resolved to {}", endpoints);
    23: }
    24: // 打印日誌,解析結果爲空
    25: if (endpoints.isEmpty()) {
    26: logger.error("Cannot resolve to any endpoints from provided configuration: {}", serviceUrls);
    27: }
    28:
    29: return endpoints;
    30: }
    • 第 3 行 :獲得可用區數組。通過 eureka.${REGION}.availabilityZones 配置。
    • 第 5 行 :調用 InstanceInfo#getZone(...) 方法,獲得應用實例自己所在的可用區( zone )。非亞馬遜 AWS 環境下,可用區數組的第一個元素就是應用實例自己所在的可用區
    • 第 7 行 :調用 EndpointUtils#getServiceUrlsMapFromConfig(...) 方法,獲得可用區與 serviceUrls 的映射。實現代碼如下:

      // EndpointUtils.java
      1: public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
      2: Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); // key:zone;value:serviceUrls
      3: // 獲得 應用實例的 地區( 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 集羣結果。



    3.5 ZoneAffinityClusterResolver

    com.netflix.discovery.shared.resolver.aws.ZoneAffinityClusterResolver ,使用可用區親和的集羣解析器。類屬性代碼如下:


    public class ZoneAffinityClusterResolver implements ClusterResolver<AwsEndpoint> {
    private static final Logger logger = LoggerFactory.getLogger(ZoneAffinityClusterResolver.class);
    /
    * 委託的解析器
    * 目前代碼裏爲 {@link ConfigClusterResolver}
    /
    private final ClusterResolver<AwsEndpoint> delegate;
    /
    應用實例的可用區
    /
    private final String myZone;
    /*
    * 是否可用區親和
    */
    private final boolean zoneAffinity;
    public ZoneAffinityClusterResolver(ClusterResolver<AwsEndpoint> delegate, String myZone, boolean zoneAffinity) {
    this.delegate = delegate;
    this.myZone = myZone;
    this.zoneAffinity = zoneAffinity;
    }
    }
    • 屬性 delegate ,委託的解析器。目前代碼裏使用的是 ConfigClusterResolver 。
    • 屬性 zoneAffinity ,是否可用區親和。
      • true :EndPoint 可用區爲本地的優先被放在前面。
      • false :EndPoint 可用區非本地的優先被放在前面。

    #getClusterEndpoints(...) 方法,實現代碼如下:

    1: @Override
    2: public List<AwsEndpoint> getClusterEndpoints() {
    3: // 拆分成 本地的可用區和非本地的可用區的 EndPoint 集羣
    4: List<AwsEndpoint>[] parts = ResolverUtils.splitByZone(delegate.getClusterEndpoints(), myZone);
    5: List<AwsEndpoint> myZoneEndpoints = parts[0];
    6: List<AwsEndpoint> remainingEndpoints = parts[1];
    7: // 隨機打亂 EndPoint 集羣並進行合併
    8: List<AwsEndpoint> randomizedList = randomizeAndMerge(myZoneEndpoints, remainingEndpoints);
    9: // 非可用區親和,將非本地的可用區的 EndPoint 集羣放在前面
    10: if (!zoneAffinity) {
    11: Collections.reverse(randomizedList);
    12: }
    13:
    14: if (logger.isDebugEnabled()) {
    15: logger.debug("Local zone={}; resolved to: {}", myZone, randomizedList);
    16: }
    17:
    18: return randomizedList;
    19: }

    • 第 2 行 :調用 ClusterResolver#getClusterEndpoints() 方法,獲得 EndPoint 集羣。再調用 ResolverUtils#splitByZone(...) 方法,拆分成本地非本地的可用區的 EndPoint 集羣,點擊鏈接查看實現。
    • 第 8 行 :調用 #randomizeAndMerge(...) 方法,分別隨機打亂每個 EndPoint 集羣,並進行合併數組,實現代碼如下:

      // ZoneAffinityClusterResolver.java
      private 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.java
      public 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 集羣放在前面。



    3.6 AsyncResolver

    com.netflix.discovery.shared.resolver.AsyncResolver異步執行解析的集羣解析器。AsyncResolver 屬性較多,而且複雜的多,我們拆分到具體方法裏分享。


    3.6.1 定時任務

    AsyncResolver 內置定時任務,定時刷新 EndPoint 集羣解析結果。

    爲什麼要刷新?例如,Eureka-Server 的 serviceUrls 基於 DNS 配置。

    定時任務代碼如下

    /
    * 是否已經調度定時任務 {@link #updateTask}
    /
    private final AtomicBoolean scheduled = new AtomicBoolean(false);
    /
    委託的解析器
    * 目前代碼爲 {@link com.netflix.discovery.shared.resolver.aws.ZoneAffinityClusterResolver}
    /
    private final ClusterResolver<T> delegate;
    /
    定時服務
    /
    private final ScheduledExecutorService executorService;
    /
    線程池執行器
    /
    private final ThreadPoolExecutor threadPoolExecutor;
    /
    後臺任務
    * 定時解析 EndPoint 集羣
    /
    private final TimedSupervisorTask backgroundTask;
    /
    解析 EndPoint 集羣結果
    /
    private final AtomicReference<List<T>> resultsRef;
    /
    定時解析 EndPoint 集羣的頻率
    /
    private final int refreshIntervalMs;
    /
    預熱超時時間,單位:毫秒
    */
    private final int warmUpTimeoutMs;
    // Metric timestamp, tracking last time when data were effectively changed.
    private volatile long lastLoadTimestamp = -1;
    AsyncResolver(String name,
    ClusterResolver<T> delegate,
    List<T> initialValue,
    int executorThreadPoolSize,
    int refreshIntervalMs,
    int warmUpTimeoutMs) {
    this.name = name;
    this.delegate = delegate;
    this.refreshIntervalMs = refreshIntervalMs;
    this.warmUpTimeoutMs = warmUpTimeoutMs;
    // 初始化 定時服務
    this.executorService = Executors.newScheduledThreadPool(1, // 線程大小=1
    new ThreadFactoryBuilder()
    .setNameFormat("AsyncResolver-" + name + "-%d")
    .setDaemon(true)
    .build());
    // 初始化 線程池執行器
    this.threadPoolExecutor = new ThreadPoolExecutor(
    1, // 線程大小=1
    executorThreadPoolSize, 0, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(), // use direct handoff
    new ThreadFactoryBuilder()
    .setNameFormat("AsyncResolver-" + name + "-executor-%d")
    .setDaemon(true)
    .build()
    );
    // 初始化 後臺任務
    this.backgroundTask = new TimedSupervisorTask(
    this.getClass().getSimpleName(),
    executorService,
    threadPoolExecutor,
    refreshIntervalMs,
    TimeUnit.MILLISECONDS,
    5,
    updateTask
    );
    this.resultsRef = new AtomicReference<>(initialValue);
    Monitors.registerObject(name, this);
    }

    • backgroundTask ,後臺任務,定時解析 EndPoint 集羣。

      • TimedSupervisorTask ,在 《Eureka 源碼解析 —— 應用實例註冊發現(二)之續租》「2.3 TimedSupervisorTask」 有詳細解析。
      • updateTask 實現代碼如下:

        private final Runnable updateTask = new Runnable() {
        @Override
        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 集羣」 詳細解析。




    3.6.2 解析 EndPoint 集羣

    調用 #getClusterEndpoints() 方法,解析 EndPoint 集羣,實現代碼如下:


    1: @Override
    2: public List<T> getClusterEndpoints() {
    3: long delay = refreshIntervalMs;
    4: // 若未預熱解析 EndPoint 集羣結果,進行預熱
    5: if (warmedUp.compareAndSet(false, true)) {
    6: if (!doWarmUp()) {
    7: delay = 0; // 預熱失敗,取消定時任務的第一次延遲
    8: }
    9: }
    10: // 若未調度定時任務,進行調度
    11: if (scheduled.compareAndSet(false, true)) {
    12: scheduleTask(delay);
    13: }
    14: // 返回 EndPoint 集羣
    15: return resultsRef.get();
    16: }
    • 第 5 至 9 行 :若未預熱解析 EndPoint 集羣結果,調用 #doWarmUp() 方法,進行預熱。若預熱失敗,取消定時任務的第一次延遲。#doWarmUp() 方法實現代碼如下:

      boolean doWarmUp() {
      Future future = null;
      try {
      future = threadPoolExecutor.submit(updateTask);
      future.get(warmUpTimeoutMs, TimeUnit.MILLISECONDS); // block until done or timeout
      return 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 集羣。當第一次預熱失敗,會返回空,直到定時任務獲得到結果



    4. 初始化解析器

    Eureka-Client 在初始化時,調用 DiscoveryClient#scheduleServerEndpointTask() 方法,初始化 AsyncResolver 解析器。實現代碼如下:


    private void scheduleServerEndpointTask(EurekaTransport eurekaTransport,
    AbstractDiscoveryClientOptionalArgs args) {
    // ... 省略無關代碼
    // 創建 EndPoint 解析器
    eurekaTransport.bootstrapResolver = EurekaHttpClients.newBootstrapResolver(
    clientConfig,
    transportConfig,
    eurekaTransport.transportClientFactory,
    applicationInfoManager.getInfo(),
    applicationsSource
    );
    // ... 省略無關代碼
    }
    • 調用 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: applicationsSource
      18: );
      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 default
      26: 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: // 創建 ZoneAffinityClusterResolver
      41: ClusterResolver<AwsEndpoint> delegateResolver = new ZoneAffinityClusterResolver(
      42: new ConfigClusterResolver(clientConfig, myInstanceInfo),
      43: myZone,
      44: true
      45: );
      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: // 創建 AsyncResolver
      58: return new AsyncResolver<>(
      59: EurekaClientNames.BOOTSTRAP,
      60: delegateResolver,
      61: initialValue,
      62: 1,
      63: clientConfig.getEurekaServiceUrlPollIntervalSeconds() * 1000
      64: );
      65: }
* 第 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>&#123;</div><div class="line">   <span class="keyword">if</span> (<span class="string">"true"</span>.equals(clientConfig.getExperimental(<span class="string">"clientTransportFailFastOnInit"</span>))) &#123;</div><div class="line">       <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(msg);</div><div class="line">   &#125;</div><div class="line">&#125;</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

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