Eureka 源碼解析 —— 網絡通信

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

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

  • 1. 概述
  • 2. EurekaHttpClient

  • 3. EurekaHttpClient
  • 4. AbstractJerseyEurekaHttpClient
  • 5. EurekaHttpClientDecorator
  • 6. 創建網絡通訊客戶端


  • 1. 概述

    本文主要分享 Eureka 的網絡通信部分。在不考慮 Eureka 2.x 的兼容的情況下,Eureka 1.x 主要兩部分的網絡通信:

    • Eureka-Client 請求 Eureka-Server 的網絡通信
    • Eureka-Server 集羣內,Eureka-Server 請求 其它的Eureka-Server 的網絡通信

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

    • 粉色部分 —— EurekaJerseyClient ,對基於 Jersey Server 的 Eureka-Server 的 Jersey 客戶端封裝。
    • 綠色部分 —— EurekaHttpClient ,Eureka-Server HTTP 訪問客戶端,定義了具體的 Eureka-Server API 調用方法。如果把 DiscoveryClient 類比成 Service ,那麼 EurekaHttpClient 可以類比城 Dao 。
    • 綜色部分 —— EurekaHttpClient 實現類,真正實現了具體的 Eureka-Server API 調用方法。
    • 紅色部分 —— EurekaHttpClient 委託類,提供了會話、重試、重定向、監控指標收集等特性。
    • 黃色部分 —— EurekaHttpClientFactory,用於創建 EurekaHttpClient 。

    類圖看起來很複雜,整體調用關係如下( 打開大圖 ):

    OK ,我們逐層解析,嗨起來。

    推薦 Spring Cloud 書籍

    2. EurekaHttpClient

    com.netflix.discovery.shared.transport.jersey.EurekaJerseyClient ,EurekaHttpClient 接口。接口代碼如下:


    public interface EurekaJerseyClient {
    ApacheHttpClient4 getClient();
    void destroyResources();
    }
    • com.sun.jersey.client.apache4.ApacheHttpClient4 ,基於 Apache HttpClient4 實現的 Jersey Client 。

    2.1 EurekaJerseyClientImpl

    com.netflix.discovery.shared.transport.jersey.EurekaJerseyClientImpl ,EurekaHttpClient 實現類。實現代碼如下:


    public class EurekaJerseyClientImpl implements EurekaJerseyClient {
    /
    * 基於 Apache HttpClient4 實現的 Jersey Client
    /
    private final ApacheHttpClient4 apacheHttpClient;
    /
    Apache HttpClient 空閒連接清理器
    /
    private final ApacheHttpClientConnectionCleaner apacheHttpClientConnectionCleaner;
    /*
    * Jersey Client 配置
    */
    ClientConfig jerseyClientConfig;
    public EurekaJerseyClientImpl(int connectionTimeout, int readTimeout, final int connectionIdleTimeout,
    ClientConfig clientConfig) {
    try {
    jerseyClientConfig = clientConfig;
    // 創建 ApacheHttpClient
    apacheHttpClient = ApacheHttpClient4.create(jerseyClientConfig);
    // 設置 連接參數
    HttpParams params = apacheHttpClient.getClientHandler().getHttpClient().getParams();
    HttpConnectionParams.setConnectionTimeout(params, connectionTimeout);
    HttpConnectionParams.setSoTimeout(params, readTimeout);
    // 創建 ApacheHttpClientConnectionCleaner
    this.apacheHttpClientConnectionCleaner = new ApacheHttpClientConnectionCleaner(apacheHttpClient, connectionIdleTimeout);
    } catch (Throwable e) {
    throw new RuntimeException("Cannot create Jersey client", e);
    }
    }
    @Override
    public ApacheHttpClient4 getClient() {
    return apacheHttpClient;
    }
    @Override
    public void destroyResources() {
    apacheHttpClientConnectionCleaner.shutdown();
    apacheHttpClient.destroy();
    }
    }
    • com.netflix.discovery.shared.transport.jersey.ApacheHttpClientConnectionCleaner ,Apache HttpClient 空閒連接清理器,負責週期性關閉處於 half-close 狀態的空閒連接。點擊 鏈接 查看帶中文註釋的 ApacheHttpClientConnectionCleaner。推薦閱讀:《HttpClient容易忽視的細節——連接關閉》

    2.2 EurekaJerseyClientBuilder

    EurekaJerseyClientBuilder ,EurekaJerseyClientImpl 內部類,用於創建 EurekaJerseyClientImpl 。

    調用 #build() 方法,創建 EurekaJerseyClientImpl ,實現代碼如下:

    // EurekaJerseyClientBuilder.java
    public EurekaJerseyClient build() {
    MyDefaultApacheHttpClient4Config config = new MyDefaultApacheHttpClient4Config();
    try {
    return new EurekaJerseyClientImpl(connectionTimeout, readTimeout, connectionIdleTimeout, config);
    } catch (Throwable e) {
    throw new RuntimeException("Cannot create Jersey client ", e);
    }
    }

    • MyDefaultApacheHttpClient4Config ,繼承自 com.sun.jersey.client.apache4.config.DefaultApacheHttpClient4Config ,實現自定義配置。點擊 鏈接 查看帶中文註釋的 MyDefaultApacheHttpClient4Config。例如 :
      • 自定義的請求、響應的編解碼器 com.netflix.discovery.provider.DiscoveryJerseyProvider
      • 禁用重定向,使用 RedirectingEurekaHttpClient 實現該特性。
      • 自定義 UserAgent 。
      • 自定義 Http Proxy 。
      • SSL 功能的增強。ApacheHttpClient4 使用的是 Apache HttpClient 4.1.1 版本,com.netflix.discovery.shared.transport.jersey.SSLSocketFactoryAdapter 將 Apache HttpClient 4.3.4 對 SSL 功能的增強適配到老版本 API 。點擊 鏈接 查看帶中文註釋的 SSLSocketFactoryAdapter。


    3. EurekaHttpClient

    com.netflix.discovery.shared.transport.EurekaHttpClient ,Eureka-Server HTTP 訪問客戶端,定義了具體的 Eureka-Server API 調用方法 。點擊 鏈接 查看帶中文註釋的 EurekaHttpClient。


    3.1 EurekaHttpResponse

    com.netflix.discovery.shared.transport.EurekaHttpResponse ,請求響應對象,實現代碼如下:


    public class EurekaHttpResponse<T> {
    /
    * 返回狀態碼
    /
    private final int statusCode;
    /
    返回對象( Entity )
    /
    private final T entity;
    /
    返回 header
    /
    private final Map<String, String> headers;
    /
    重定向地址
    */
    private final URI location;
    // ... 省略 setting / getting 和 Builder
    }

    3.2 TransportClientFactory

    com.netflix.discovery.shared.transport.TransportClientFactory ,創建 EurekaHttpClient 的工廠接口。接口代碼如下:


    public interface TransportClientFactory {
    /
    * 創建 EurekaHttpClient
    @param serviceUrl Eureka-Server 地址
    * @return EurekaHttpClient
    /
    EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
    /
    關閉工廠
    */
    void shutdown();
    }

    大多數 EurekaHttpClient 實現類都有其對應的工廠實現類

    4. AbstractJerseyEurekaHttpClient

    com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient ,實現 EurekaHttpClient 的抽象類真正實現了具體的 Eureka-Server API 調用方法。實現代碼如下:


    1: public abstract class AbstractJerseyEurekaHttpClient implements EurekaHttpClient {
    2:
    3: private static final Logger logger = LoggerFactory.getLogger(AbstractJerseyEurekaHttpClient.class);
    4:
    5: /
    6: * Jersey Client
    7: /
    8: protected final Client jerseyClient;
    9: /
    10: 請求的 Eureka-Server 地址
    11: */
    12: protected final String serviceUrl;
    13:
    14: protected AbstractJerseyEurekaHttpClient(Client jerseyClient, String serviceUrl) {
    15: this.jerseyClient = jerseyClient;
    16: this.serviceUrl = serviceUrl;
    17: logger.debug("Created client for url: {}", serviceUrl);
    18: }
    19:
    20: @Override
    21: public EurekaHttpResponse<Void> register(InstanceInfo info) {
    22: // 設置 請求地址
    23: String urlPath = "apps/" + info.getAppName();
    24: ClientResponse response = null;
    25: try {
    26: Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
    27: // 設置 請求頭
    28: addExtraHeaders(resourceBuilder);
    29: // 請求 Eureka-Server
    30: response = resourceBuilder
    31: .header("Accept-Encoding", "gzip") // GZIP
    32: .type(MediaType.APPLICATION_JSON_TYPE) // 請求參數格式 JSON
    33: .accept(MediaType.APPLICATION_JSON) // 響應結果格式 JSON
    34: .post(ClientResponse.class, info); // 請求參數
    35: // 創建 EurekaHttpResponse
    36: return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    37: } finally {
    38: if (logger.isDebugEnabled()) {
    39: logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
    40: response == null ? "N/A" : response.getStatus());
    41: }
    42: if (response != null) {
    43: response.close();
    44: }
    45: }
    46: }
    • jerseyClient 屬性,Jersey Client ,使用上文的 EurekaHttpClient#getClient(...) 方法,獲取 ApacheHttpClient4 。
    • serviceUrl 屬性,請求的 Eureka-Server 地址。
    • #register() 方法,實現向 Eureka-Server 註冊應用實例。其他方法代碼類似

      • 第 22 至 26 行 :設置請求地址。
      • 第 28 行 :調用 #addExtraHeaders(...) 方法,設置請求頭( header )。該方法是抽象方法,提供子類實現自定義的請求頭。代碼如下:

        protected abstract void addExtraHeaders(Builder webResource);
        • x
          • 第 29 至 34 行 :請求 Eureka-Server 。
          • 第 35 至 36 行 :解析響應結果,創建 EurekaHttpResponse 。




    4.1 JerseyApplicationClient

    com.netflix.discovery.shared.transport.jersey.JerseyApplicationClient ,實現 Eureka-Client 請求 Eureka-Server 的網絡通信。點擊 鏈接 查看帶中文註釋的 JerseyApplicationClient。


    4.1.1 JerseyEurekaHttpClientFactory

    com.netflix.discovery.shared.transport.jersey.JerseyEurekaHttpClientFactory ,創建 JerseyApplicationClient 的工廠類。實現代碼如下:


    public class JerseyEurekaHttpClientFactory implements TransportClientFactory {
    private final EurekaJerseyClient jerseyClient;
    private final ApacheHttpClient4 apacheClient;
    private final ApacheHttpClientConnectionCleaner cleaner;
    private final Map<String, String> additionalHeaders;
    public JerseyEurekaHttpClientFactory(ApacheHttpClient4 apacheClient, long connectionIdleTimeout, Map<String, String> additionalHeaders) {
    this(null, apacheClient, connectionIdleTimeout, additionalHeaders);
    }
    private JerseyEurekaHttpClientFactory(EurekaJerseyClient jerseyClient,
    ApacheHttpClient4 apacheClient,
    long connectionIdleTimeout,
    Map<String, String> additionalHeaders) {
    this.jerseyClient = jerseyClient;
    this.apacheClient = jerseyClient != null ? jerseyClient.getClient() : apacheClient;
    this.additionalHeaders = additionalHeaders;
    this.cleaner = new ApacheHttpClientConnectionCleaner(this.apacheClient, connectionIdleTimeout);
    }
    @Override
    public EurekaHttpClient newClient(EurekaEndpoint endpoint) {
    return new JerseyApplicationClient(apacheClient, endpoint.getServiceUrl(), additionalHeaders);
    }
    @Override
    public void shutdown() {
    cleaner.shutdown();
    if (jerseyClient != null) {
    jerseyClient.destroyResources();
    } else {
    apacheClient.destroy();
    }
    }
    }

    4.1.2 JerseyEurekaHttpClientFactoryBuilder

    JerseyEurekaHttpClientFactoryBuilder ,JerseyEurekaHttpClientFactory 內部類,用於創建 JerseyEurekaHttpClientFactory 。點擊 鏈接 查看帶中文註釋的 JerseyEurekaHttpClientFactory。

    調用 JerseyEurekaHttpClientFactory#create(...) 方法,創建 JerseyEurekaHttpClientFactory ,實現代碼如下:

    public static JerseyEurekaHttpClientFactory create(EurekaClientConfig clientConfig,
    Collection<ClientFilter> additionalFilters,
    InstanceInfo myInstanceInfo,
    AbstractEurekaIdentity clientIdentity) {
    JerseyEurekaHttpClientFactoryBuilder clientBuilder = newBuilder()
    .withAdditionalFilters(additionalFilters) // 客戶端附加過濾器
    .withMyInstanceInfo(myInstanceInfo) // 應用實例
    .withUserAgent("Java-EurekaClient") // UA
    .withClientConfig(clientConfig)
    .withClientIdentity(clientIdentity);
    // 設置 Client Name
    if ("true".equals(System.getProperty("com.netflix.eureka.shouldSSLConnectionsUseSystemSocketFactory"))) {
    clientBuilder.withClientName("DiscoveryClient-HTTPClient-System").withSystemSSLConfiguration();
    } else if (clientConfig.getProxyHost() != null && clientConfig.getProxyPort() != null) {
    clientBuilder.withClientName("Proxy-DiscoveryClient-HTTPClient")
    .withProxy(
    clientConfig.getProxyHost(), Integer.parseInt(clientConfig.getProxyPort()),
    clientConfig.getProxyUserName(), clientConfig.getProxyPassword()
    ); // http proxy
    } else {
    clientBuilder.withClientName("DiscoveryClient-HTTPClient");
    }
    return clientBuilder.build();
    }
    public static JerseyEurekaHttpClientFactoryBuilder newBuilder() {
    return new JerseyEurekaHttpClientFactoryBuilder().withExperimental(false);
    }

    4.2 JerseyReplicationClient

    com.netflix.eureka.transport.JerseyReplicationClient ,Eureka-Server 集羣內,Eureka-Server 請求 其它的Eureka-Server 的網絡通信。

    4.2.1 沒有工廠

    JerseyReplicationClient 沒有專屬的工廠

    調用 JerseyReplicationClient#createReplicationClient(...) 靜態方法,創建 JerseyReplicationClient 。點擊 鏈接 查看帶中文註釋的方法代碼。

    5. EurekaHttpClientDecorator

    com.netflix.discovery.shared.transport.decorator.EurekaHttpClientDecorator,EurekaHttpClient 委託者抽象類。實現代碼如下:


    public abstract class EurekaHttpClientDecorator implements EurekaHttpClient {
    /*
    執行請求
    @param requestExecutor 請求執行器
    * @param <R> 請求泛型
    * @return 響應
    */
    protected abstract <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor);
    @Override
    public EurekaHttpResponse<Void> register(final InstanceInfo info) {
    return execute(new RequestExecutor<Void>() {
    @Override
    public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
    return delegate.register(info);
    }
    @Override
    public RequestType getRequestType() {
    return RequestType.Register;
    }
    });
    }
    }
    • #execute(...) 抽象方法,子類實現該方法,實現自己的特性。
    • #register() 方法,實現向 Eureka-Server 註冊應用實例。其他方法代碼類似

    • RequestType ,請求類型枚舉類。代碼如下:


      // EurekaHttpClientDecorator.java
      public enum RequestType {
      Register,
      Cancel,
      SendHeartBeat,
      StatusUpdate,
      DeleteStatusOverride,
      GetApplications,
      GetDelta,
      GetVip,
      GetSecureVip,
      GetApplication,
      GetInstance,
      GetApplicationInstance
      }

    • RequestExecutor ,請求執行器接口。接口代碼如下:


      // EurekaHttpClientDecorator.java
      public interface RequestExecutor<R> {
      /
      * 執行請求
      @param delegate 委託的 EurekaHttpClient
      * @return 響應
      /
      EurekaHttpResponse<R> execute(EurekaHttpClient delegate);
      /
      @return 請求類型
      */
      RequestType getRequestType();
      }


    EurekaHttpClientDecorator 的每個實現類實現一個特性,代碼非常非常非常清晰。

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

    我們在上圖的基礎上,增加委託的關係,如下圖( 打開大圖 ):

    • 請注意,每個委託着實現類,上面可能有類型爲 EurekaHttpClientFactory 的屬性,用於創建其委託的 EurekaHttpClient 。爲什麼會有 Factory ?例如,RetryableEurekaHttpClient 重試請求多個 Eureka-Server 地址時,每個 Eureka-Server 地址會創建一個 EurekaHttpClient 。所以,下文涉及到 EurekaHttpClientFactory 和委託的 EurekaHttpClient 的地方,你都需要仔細理解。

    5.1 MetricsCollectingEurekaHttpClient

    com.netflix.discovery.shared.transport.decorator.MetricsCollectingEurekaHttpClient ,監控指標收集 EurekaHttpClient ,配合 Netflix Servo 實現監控信息採集。

    #execute() 方法,代碼如下:

    1: @Override
    2: protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    3: // 獲得 請求類型 的 請求指標
    4: EurekaHttpClientRequestMetrics requestMetrics = metricsByRequestType.get(requestExecutor.getRequestType());
    5: Stopwatch stopwatch = requestMetrics.latencyTimer.start();
    6: try {
    7: // 執行請求
    8: EurekaHttpResponse<R> httpResponse = requestExecutor.execute(delegate);
    9: // 增加 請求指標
    10: requestMetrics.countersByStatus.get(mappedStatus(httpResponse)).increment();
    11: return httpResponse;
    12: } catch (Exception e) {
    13: requestMetrics.connectionErrors.increment();
    14: exceptionsMetric.count(e);
    15: throw e;
    16: } finally {
    17: stopwatch.stop();
    18: }
    19: }

    • 第 10 行 :調用 RequestExecutor#execute(...) 方法,繼續執行請求。
      • delegate 屬性,對應 JerseyApplicationClient 。


    5.2 RedirectingEurekaHttpClient

    com.netflix.discovery.shared.transport.decorator.RedirectingEurekaHttpClient尋找非 302 重定向的 Eureka-Server 的 EurekaHttpClient 。

    #execute() 方法,代碼如下:

    1: @Override
    2: protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    3: EurekaHttpClient currentEurekaClient = delegateRef.get();
    4: if (currentEurekaClient == null) { // 未找到非 302 的 Eureka-Server
    5: AtomicReference<EurekaHttpClient> currentEurekaClientRef = new AtomicReference<>(factory.newClient(serviceEndpoint));
    6: try {
    7: EurekaHttpResponse<R> response = executeOnNewServer(requestExecutor, currentEurekaClientRef);
    8: // 關閉原有的委託 EurekaHttpClient ,並設置當前成功非 302 請求的 EurekaHttpClient
    9: TransportUtils.shutdown(delegateRef.getAndSet(currentEurekaClientRef.get()));
    10: return response;
    11: } catch (Exception e) {
    12: logger.error("Request execution error", e);
    13: TransportUtils.shutdown(currentEurekaClientRef.get());
    14: throw e;
    15: }
    16: } else { // 已經找到非 302 的 Eureka-Server
    17: try {
    18: return requestExecutor.execute(currentEurekaClient);
    19: } catch (Exception e) {
    20: logger.error("Request execution error", e);
    21: delegateRef.compareAndSet(currentEurekaClient, null);
    22: currentEurekaClient.shutdown();
    23: throw e;
    24: }
    25: }
    26: }

    • 注意:和我們理解的常規的 302 狀態返回處理不同!!!
    • 整個分成兩部分:【第 4 至 15 行】、【第 16 至 24 行】。
      • 前者,意味着未找到非返回 302 狀態碼的 Eureka-Server ,此時通過在原始傳遞進來的 serviceUrls 執行請求,尋找非 302 狀態碼返回的 Eureka-Server。
        • 當返回非 302 狀態碼時,找到非返回 302 狀態碼的 Eureka-Server 。
        • 當返回 302 狀態碼時,向新的重定向的 Eureka-Server 執行請求直到成功找到或超過最大次數。
          • 後者,意味着當前已經找到非返回 302 狀態碼的 Eureka-Server ,直接執行請求。注意 :此時 Eureka-Server 再返回 302 狀態碼,不再處理。
          • 目前 Eureka 1.x 的 Eureka-Server 不存在返回 302 狀態碼,猜測和 Eureka 2.X TODO[0028]:寫入集羣和讀取集羣 有關。



    • 【前者】第 5 行 :使用初始的 serviceEndpoint ( 相當於 serviceUrls ) 創建委託 EurekaHttpClient 。
    • 【前者】第 7 行 :調用 #executeOnNewServer(...) 方法,通過執行請求的方式,尋找非 302 狀態碼返回的 Eureka-Server。實現代碼,點擊 鏈接 查看帶中文註釋的代碼實現。
    • 【前者】【前者】第 9 行 :關閉原有的 delegateRef ( 因爲此處可能存在併發,多個線程都找到非 302 狀態碼返回的 Eureka-Server ),並設置當前成功非 302 請求的 EurekaHttpClient 到 delegateRef
    • 【前者】第 13 行 :關閉 currentEurekaClientRef ,當請求發生異常或者超過最大重定向次數。
    • 【後者】第 18 行 :意味着當前已經找到非返回 302 狀態碼的 Eureka-Server ,直接執行請求。
    • 【後者】第 21 至 22 行 :執行請求發生異常,關閉 currentEurekaClient ,後面要重新非返回 302 狀態碼的 Eureka-Server 。

    5.2.1 工廠

    RedirectingEurekaHttpClient 提供 #createFactory(...) 靜態方法獲得創建其的工廠,點擊 鏈接 查看。


    5.3 RetryableEurekaHttpClient

    com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient ,支持向多個 Eureka-Server 請求重試的 EurekaHttpClient 。

    #execute() 方法,代碼如下:

    1: @Override
    2: protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    3: List<EurekaEndpoint> candidateHosts = null;
    4: int endpointIdx = 0;
    5: for (int retry = 0; retry < numberOfRetries; retry++) {
    6: EurekaHttpClient currentHttpClient = delegate.get();
    7: EurekaEndpoint currentEndpoint = null;
    8:
    9: // 當前委託的 EurekaHttpClient 不存在
    10: if (currentHttpClient == null) {
    11: // 獲得候選的 Eureka-Server 地址數組
    12: if (candidateHosts == null) {
    13: candidateHosts = getHostCandidates();
    14: if (candidateHosts.isEmpty()) {
    15: throw new TransportException("There is no known eureka server; cluster server list is empty");
    16: }
    17: }
    18:
    19: // 超過候選的 Eureka-Server 地址數組上限
    20: if (endpointIdx >= candidateHosts.size()) {
    21: throw new TransportException("Cannot execute request on any known server");
    22: }
    23:
    24: // 創建候選的 EurekaHttpClient
    25: currentEndpoint = candidateHosts.get(endpointIdx++);
    26: currentHttpClient = clientFactory.newClient(currentEndpoint);
    27: &##125;
    28:
    29: try {
    30: // 執行請求
    31: EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
    32: // 判斷是否爲可接受的相應,若是,返回。
    33: if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
    34: delegate.set(currentHttpClient);
    35: if (retry > 0) {
    36: logger.info("Request execution succeeded on retry #{}", retry);
    37: }
    38: return response;
    39: }
    40: logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
    41: } catch (Exception e) {
    42: logger.warn("Request execution failed with message: {}", e.getMessage()); // just log message as the underlying client should log the stacktrace
    43: }
    44:
    45: // 請求失敗,若是 currentHttpClient ,清除 delegate
    46: // Connection error or 5xx from the server that must be retried on another server
    47: delegate.compareAndSet(currentHttpClient, null);
    48:
    49: // 請求失敗,將 currentEndpoint 添加到隔離集合
    50: if (currentEndpoint != null) {
    51: quarantineSet.add(currentEndpoint);
    52: }
    53: }
    54: throw new TransportException("Retry limit reached; giving up on completing the request");
    55: }

    • 第 10 行 :當前 currentHttpClient 不存在,意味着原有 delegate 不存在向 Eureka-Server 成功請求的 EurekaHttpClient 。
      • 此時需要從配置中的 Eureka-Server 數組重試請求,獲得可以請求的 Eureka-Server 。
      • 如果已經存在請求成功的 delegate ,直接使用它進行執行請求。

    • 第 11 至 17 行 :調用 #getHostCandidates() 方法,獲得候選的 Eureka-Server serviceUrls 數組。實現代碼如下:


      1: private List<EurekaEndpoint> getHostCandidates() {
      2: // 獲得候選的 Eureka-Server 地址數組
      3: List<EurekaEndpoint> candidateHosts = clusterResolver.getClusterEndpoints();
      4:
      5: // 保留交集(移除 quarantineSet 不在 candidateHosts 的元素)
      6: quarantineSet.retainAll(candidateHosts);
      7:
      8: // 在保證最小可用的候選的 Eureka-Server 地址數組,移除在隔離集合內的元素
      9: // If enough hosts are bad, we have no choice but start over again
      10: int threshold = (int) (candidateHosts.size() * transportConfig.getRetryableClientQuarantineRefreshPercentage()); // 0.66
      11: if (quarantineSet.isEmpty()) {
      12: // no-op
      13: } else if (quarantineSet.size() >= threshold) {
      14: logger.debug("Clearing quarantined list of size {}", quarantineSet.size());
      15: quarantineSet.clear();
      16: } else {
      17: List<EurekaEndpoint> remainingHosts = new ArrayList<>(candidateHosts.size());
      18: for (EurekaEndpoint endpoint : candidateHosts) {
      19: if (!quarantineSet.contains(endpoint)) {
      20: remainingHosts.add(endpoint);
      21: }
      22: }
      23: candidateHosts = remainingHosts;
      24: }
      25:
      26: return candidateHosts;
      27: }
      • 第 3 行 :調用 ClusterResolver#getClusterEndpoints() 方法,獲得候選的 Eureka-Server 地址數組( candidateHosts )。注意:該方法返回的 Eureka-Server 地址數組,使用以本機 IP 爲隨機種子,達到不同 IP 的應用實例獲得的數組順序不同,而相同 IP 的應用實例獲得的數組順序一致,效果類似基於 IP HASH 的負載均衡算法。實現該功能的代碼,在 《Eureka 源碼解析 —— EndPoint 與 解析器》搜索關鍵字【ResolverUtils#randomize(…)】 詳細解析。
      • 第 6 行 :調用 Set#retainAll() 方法,移除隔離的故障 Eureka-Server 地址數組( quarantineSet ) 中不在 candidateHosts 的元素。
      • 第 8 至 24 行 :在保證最小可用的 candidateHosts,移除在 quarantineSet 的元素。
        • 第 10 行 :最小可用的閥值,配置 eureka.retryableClientQuarantineRefreshPercentage 來設置百分比,默認值:0.66
        • 最 13 至 15 行 :quarantineSet 數量超過閥值,清空 quarantineSet ,全部 candidateHosts 重試。
        • 第 17 至 24 行 :quarantineSet 數量未超過閥值,移除 candidateHosts 中在 quarantineSet 的元素。


    • 第 19 至 22 行 :超過 candidateHosts 上限,全部 Eureka-Server 請求失敗,拋出異常。


    • 第 24 至 26 行 :創建委託的 EurekaHttpClient ,用於下面請求執行。
    • 第 31 行 :執行請求。
    • 第 33 行 :調用 ServerStatusEvaluator#accept() 方法,判斷響應狀態碼和請求類型是否能夠接受。實現代碼如下:


      // ServerStatusEvaluators.java
      private static final ServerStatusEvaluator LEGACY_EVALUATOR = new ServerStatusEvaluator() {
      @Override
      public boolean accept(int statusCode, RequestType requestType) {
      if (statusCode >= 200 && statusCode < 300 || statusCode == 302) {
      return true;
      } else if (requestType == RequestType.Register && statusCode == 404) { // 註冊,404 可接受
      return true;
      } else if (requestType == RequestType.SendHeartBeat && statusCode == 404) { // 心跳,404 可接受
      return true;
      } else if (requestType == RequestType.Cancel) { // cancel is best effort 下線,接受全部
      return true;
      } else if (requestType == RequestType.GetDelta && (statusCode == 403 || statusCode == 404)) { // 增量獲取註冊信息,403 404 可接受
      return true;
      }
      return false;
      }
      };

    • 第 34 行 :請求成功,設置 delegate 。下次請求,優先使用 delegate ,失敗才進行候選的 Eureka-Server 地址數組重試。


    • 第 47 行 :請求失敗,delegate 若等於 currentHttpClient ,進行清除。
    • 第 50 至 52 行 :請求失敗,將請求的 Eureka-Server 地址添加到 quarantineSet
    • 總結來說:

      • 【第一步】若當前有請求成功的 EurekaHttpClient ,繼續使用。若請求失敗,執行【第二步】。
      • 【第二步】若當前無請求成功的 EurekaHttpClient ,獲取候選的 Eureka-Server 地址數組順序創建新的 EurekaHttpClient,直到成功,或者超過最大重試次數。當請求成功,保存該 EurekaHttpClient ,下次繼續使用,直到請求失敗。


    5.3.1 工廠

    RetryableEurekaHttpClient 提供 #createFactory(...) 靜態方法獲得創建其的工廠,點擊 鏈接 查看。


    5.4 SessionedEurekaHttpClient

    com.netflix.discovery.shared.transport.decorator.SessionedEurekaHttpClient ,支持會話的 EurekaHttpClient 。執行定期的重建會話,防止一個 Eureka-Client 永遠只連接一個特定的 Eureka-Server 。反過來,這也保證了 Eureka-Server 集羣變更時,Eureka-Client 對 Eureka-Server 連接的負載均衡。

    #execute(...) ,代碼如下:

    1: @Override
    2: protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
    3: long now = System.currentTimeMillis();
    4: long delay = now - lastReconnectTimeStamp;
    5:
    6: // 超過 當前會話時間,關閉當前委託的 EurekaHttpClient 。
    7: if (delay >= currentSessionDurationMs) {
    8: logger.debug("Ending a session and starting anew");
    9: lastReconnectTimeStamp = now;
    10: currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
    11: TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
    12: }
    13:
    14: // 獲得委託的 EurekaHttpClient 。若不存在,則創建新的委託的 EurekaHttpClient 。
    15: EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
    16: if (eurekaHttpClient == null) {
    17: eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
    18: }
    19: return requestExecutor.execute(eurekaHttpClient);
    20: }

    • 第 7 至 12 行 :超過當前會話時間,關閉當前委託的 EurekaHttpClient 。

      • 第 10 行 :調用 #randomizeSessionDuration(...) 方法,計算計算下一次會話超時時長,公式爲 sessionDurationMs * (0.5, 1.5) ,代碼如下:

        protected long randomizeSessionDuration(long sessionDurationMs) {
        long delta = (long) (sessionDurationMs * (random.nextDouble() - 0.5));
        return sessionDurationMs + delta;
        }
        • 增加會話過期的隨機性,實現所有 Eureka-Client 的會話過期重連的發生時間更加離散,避免集中時間過期。目前猜測這麼做的目的和 TODO[0028]:寫入集羣和讀取集羣 有關,即返回 302 。關聯 1.x new transport enhancements


    • 第 15 至 18 行 :獲得委託的 EurekaHttpClient 。若不存在,創建新的委託的 EurekaHttpClient 。TransportUtils#getOrSetAnotherClient(...) 方法代碼如下:


      1: public static EurekaHttpClient getOrSetAnotherClient(AtomicReference<EurekaHttpClient> eurekaHttpClientRef, EurekaHttpClient another) {
      2: EurekaHttpClient existing = eurekaHttpClientRef.get();
      3: // 爲空才設置
      4: if (eurekaHttpClientRef.compareAndSet(null, another)) {
      5: return another;
      6: }
      7: // 設置失敗,意味着另外一個線程已經設置
      8: another.shutdown();
      9: return existing;
      10: }
      • 該方法實現,獲得 eurekaHttpClientRef 裏的 EurekaHttpClient 。若獲取不到,將 another 設置到 eurekaHttpClientRef 。當有多個線程設置時,有且只有一個線程設置成功,另外的設置失敗的線程們,意味着當前 eurekaHttpClientRef 有 EurekaHttpClient ,返回 eurekaHttpClientRef
      • 目前該方法存在 BUG ,失敗的線程直接返回 existing 的是 null ,需要修改成 return eurekaHttpClientRef.get() 。模擬重現該 BUG 代碼如下 :

    • 第 19 行 :執行請求。



    5.4.1 沒有工廠

    在 SessionedEurekaHttpClient 類裏,沒有實現創建其的工廠。在 「6. 創建網絡通訊客戶端」搜索 canonicalClientFactory ,可以看到 EurekaHttpClients#canonicalClientFactory(...) 方法,內部有 SessionedEurekaHttpClient 的創建工廠。


    6. 創建網絡通訊客戶端

    對於 Eureka-Server 來說,調用 JerseyReplicationClient#createReplicationClient(...) 靜態方法即可創建用於 Eureka-Server 集羣內,Eureka-Server 請求 其它的Eureka-Server 的網絡通信客戶端。

    對於 Eureka-Client 來說,分成用於註冊應用實例( registrationClient )查詢註冊信息( newQueryClient )兩個不同網絡通信客戶端。在 DiscoveryClient 初始化時進行創建,代碼如下:

    // DiscoveryClient.class
    1: private void scheduleServerEndpointTask(EurekaTransport eurekaTransport,
    2: AbstractDiscoveryClientOptionalArgs args) {
    3:
    4: Collection<?> additionalFilters = args == null
    5: ? Collections.emptyList()
    6: : args.additionalFilters;
    7:
    8: EurekaJerseyClient providedJerseyClient = args == null
    9: ? null
    10: : args.eurekaJerseyClient;
    11:
    12: TransportClientFactories argsTransportClientFactories = null;
    13: if (args != null && args.getTransportClientFactories() != null) {
    14: argsTransportClientFactories = args.getTransportClientFactories();
    15: }
    16:
    17: // Ignore the raw types warnings since the client filter interface changed between jersey 1/2
    18: @SuppressWarnings("rawtypes")
    19: TransportClientFactories transportClientFactories = argsTransportClientFactories == null
    20: ? new Jersey1TransportClientFactories()
    21: : argsTransportClientFactories;
    22:
    23: // If the transport factory was not supplied with args, assume they are using jersey 1 for passivity
    24: // noinspection unchecked
    25: eurekaTransport.transportClientFactory = providedJerseyClient == null
    26: ? transportClientFactories.newTransportClientFactory(clientConfig, additionalFilters, applicationInfoManager.getInfo())
    27: : transportClientFactories.newTransportClientFactory(additionalFilters, providedJerseyClient);
    28:
    29: // (省略代碼)初始化 應用解析器的應用實例數據源 TODO[0028]寫入集羣和讀取集羣
    30:
    31: // (省略代碼)創建 EndPoint 解析器
    32: eurekaTransport.bootstrapResolver = EurekaHttpClients.newBootstrapResolver(...)
    33:
    34: if (clientConfig.shouldRegisterWithEureka()) {
    35: EurekaHttpClientFactory newRegistrationClientFactory = null;
    36: EurekaHttpClient newRegistrationClient = null;
    37: try {
    38: newRegistrationClientFactory = EurekaHttpClients.registrationClientFactory(
    39: eurekaTransport.bootstrapResolver,
    40: eurekaTransport.transportClientFactory,
    41: transportConfig
    42: );
    43: newRegistrationClient = newRegistrationClientFactory.newClient();
    44: } catch (Exception e) {
    45: logger.warn("Transport initialization failure", e);
    46: }
    47: eurekaTransport.registrationClientFactory = newRegistrationClientFactory;
    48: eurekaTransport.registrationClient = newRegistrationClient;
    49: }
    50:
    51: // new method (resolve from primary servers for read)
    52: // Configure new transport layer (candidate for injecting in the future)
    53: if (clientConfig.shouldFetchRegistry()) {
    54: EurekaHttpClientFactory newQueryClientFactory = null;
    55: EurekaHttpClient newQueryClient = null;
    56: try {
    57: newQueryClientFactory = EurekaHttpClients.queryClientFactory(
    58: eurekaTransport.bootstrapResolver,
    59: eurekaTransport.transportClientFactory,
    60: clientConfig,
    61: transportConfig,
    62: applicationInfoManager.getInfo(),
    63: applicationsSource
    64: );
    65: newQueryClient = newQueryClientFactory.newClient();
    66: } catch (Exception e) {
    67: logger.warn("Transport initialization failure", e);
    68: }
    69: eurekaTransport.queryClientFactory = newQueryClientFactory;
    70: eurekaTransport.queryClient = newQueryClient;
    71: }
    72: }

    • 第 18 至 27 行 :調用 Jersey1TransportClientFactories#newTransportClientFactory(...) 方法,創建 registrationClientqueryClient 公用的委託的 EurekaHttpClientFactory ,代碼如下:

      // Jersey1TransportClientFactories.java
      public TransportClientFactory newTransportClientFactory(final EurekaClientConfig clientConfig,
      final Collection<ClientFilter> additionalFilters,
      final InstanceInfo myInstanceInfo) {
      // JerseyEurekaHttpClientFactory
      final TransportClientFactory jerseyFactory = JerseyEurekaHttpClientFactory.create(
      clientConfig,
      additionalFilters,
      myInstanceInfo,
      new EurekaClientIdentity(myInstanceInfo.getIPAddr())
      );
      // TransportClientFactory
      final TransportClientFactory metricsFactory = MetricsCollectingEurekaHttpClient.createFactory(jerseyFactory); // 委託 TransportClientFactory
      return new TransportClientFactory() {
      @Override
      public EurekaHttpClient newClient(EurekaEndpoint serviceUrl) {
      return metricsFactory.newClient(serviceUrl);
      }
      @Override
      public void shutdown() {
      metricsFactory.shutdown();
      jerseyFactory.shutdown();
      }
      };
      }
      • 在 TransportClientFactory 裏委託 JerseyEurekaHttpClientFactory 。

    • 第 34 至 49 行 :調用 EurekaHttpClients#registrationClientFactory(...) 方法,創建 registrationClient 的 EurekaHttpClientFactory ,代碼如下 :


      // EurekaHttpClients.java
      public static EurekaHttpClientFactory registrationClientFactory(ClusterResolver bootstrapResolver,
      TransportClientFactory transportClientFactory,
      EurekaTransportConfig transportConfig) {
      return canonicalClientFactory(EurekaClientNames.REGISTRATION, transportConfig, bootstrapResolver, transportClientFactory);
      }
      static EurekaHttpClientFactory canonicalClientFactory(final String name,
      final EurekaTransportConfig transportConfig,
      final ClusterResolver<EurekaEndpoint> clusterResolver,
      final TransportClientFactory transportClientFactory) {
      return new EurekaHttpClientFactory() { // SessionedEurekaHttpClientFactory
      @Override
      public EurekaHttpClient newClient() {
      return new SessionedEurekaHttpClient(
      name,
      RetryableEurekaHttpClient.createFactory( // RetryableEurekaHttpClient
      name,
      transportConfig,
      clusterResolver,
      RedirectingEurekaHttpClient.createFactory(transportClientFactory), // RedirectingEurekaHttpClient
      ServerStatusEvaluators.legacyEvaluator()),
      transportConfig.getSessionedClientReconnectIntervalSeconds() * 1000
      );
      }
      @Override
      public void shutdown() {
      wrapClosable(clusterResolver).shutdown();
      }
      };
      }

    • 第 51 至 71 行 :調用 EurekaHttpClients#queryClientFactory(...) 方法,創建 queryClient 的 EurekaHttpClientFactory ,代碼如下 :


      // EurekaHttpClients.java
      public static EurekaHttpClientFactory queryClientFactory(ClusterResolver bootstrapResolver,
      TransportClientFactory transportClientFactory,
      EurekaClientConfig clientConfig,
      EurekaTransportConfig transportConfig,
      InstanceInfo myInstanceInfo,
      ApplicationsResolver.ApplicationsSource applicationsSource) {
      ClosableResolver queryResolver = transportConfig.useBootstrapResolverForQuery()
      ? wrapClosable(bootstrapResolver)
      : queryClientResolver(bootstrapResolver, transportClientFactory,
      clientConfig, transportConfig, myInstanceInfo, applicationsSource);
      return canonicalClientFactory(EurekaClientNames.QUERY, transportConfig, queryResolver, transportClientFactory); // 該方法上面有
      }

發佈了22 篇原創文章 · 獲贊 2 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章