Eureka 源碼解析 —— Eureka-Client 初始化(三)之 EurekaClient

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

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

  1. 創建 EurekaInstanceConfig對象
  2. 使用 EurekaInstanceConfig對象 創建 InstanceInfo對象
  3. 使用 EurekaInstanceConfig對象 + InstanceInfo對象 創建 ApplicationInfoManager對象
  4. 創建 EurekaClientConfig對象
  5. 使用 ApplicationInfoManager對象 + EurekaClientConfig對象 創建 EurekaClient對象

考慮到整個初始化的過程中涉及的配置特別多,拆分成三篇文章:

  1. (一)EurekaInstanceConfig)
  2. (二)EurekaClientConfig
  3. 【本文】(三)EurekaClient

下面我們來看看每個的實現。

推薦 Spring Cloud 書籍

2. EurekaClient

com.netflix.discovery.EurekaClient,Eureka-Client 接口,聲明如下方法:

  • 提供多種方法獲取應用集合(com.netflix.discovery.shared.Applications) 和 應用實例信息集合( com.netflix.appinfo.InstanceInfo )。
  • 提供方法獲取本地客戶端信息,例如,應用管理器( com.netflix.appinfo.ApplicationInfoManager )和 Eureka-Client 配置( com.netflix.discovery.EurekaClientConfig )。
  • 提供方法註冊本地客戶端的健康檢查和 Eureka 事件監聽器。

另外,Eureka 2.X 版本正在開發,該接口爲 Eureka 1.X 和 2.X 提供平滑過渡接口。

This interface does NOT try to clean up the current client interface for eureka 1.x. Rather it tries to provide an easier transition path from eureka 1.x to eureka 2.x.

2.1 LookupService

com.netflix.discovery.shared.LookupService,查找服務接口,提供簡單單一的方式獲取應用集合(com.netflix.discovery.shared.Applications) 和 應用實例信息集合( com.netflix.appinfo.InstanceInfo )。

  • 在 Eureka-Client 裏,EurekaClient 繼承該接口。
  • 在 Eureka-Server 裏,com.netflix.eureka.registry.InstanceRegistry 繼承該接口。

3. DiscoveryClient

com.netflix.discovery.DiscoveryClient,實現 EurekaClient 接口,用於與 Eureka-Server 交互。實現如下方法:

  • 向 Eureka-Server 註冊自身服務
  • 向 Eureka-Server 續約自身服務
  • 向 Eureka-Server 取消自身服務,當關閉時
  • 從 Eureka-Server 查詢應用集合和應用實例信息
  • 簡單來理解,對 Eureka-Server 服務的增刪改查

3.1 構造方法參數

DiscoveryClient 完整構造方法需要傳入四個參數,實現代碼如下:


DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider) {
// .... 省略代碼
}
  • ApplicationInfoManager,在《Eureka 源碼解析 —— Eureka-Client 初始化(一)之 EurekaInstanceConfig》有詳細解析。
  • EurekaClientConfig,在《Eureka 源碼解析 —— Eureka-Client 初始化(二)之 EurekaClientConfig》有詳細解析。
  • com.netflix.discovery.BackupRegistry,備份註冊中心接口。當 Eureka-Client 啓動時,無法從 Eureka-Server 讀取註冊信息(可能掛了),從備份註冊中心讀取註冊信息。實現代碼如下:

    // BackupRegistry.java
    public interface BackupRegistry {
    Applications fetchRegistry();
    Applications fetchRegistry(String[] includeRemoteRegions);
    }
    // NotImplementedRegistryImpl.java
    public class NotImplementedRegistryImpl implements BackupRegistry {
    @Override
    public Applications fetchRegistry() {
    return null;
    }
    @Override
    public Applications fetchRegistry(String[] includeRemoteRegions) {
    return null;
    }
    }
    • com.netflix.discovery.NotImplementedRegistryImpl 可以看出,目前 Eureka-Client 未提供合適的默認實現。

  • com.netflix.discovery.AbstractDiscoveryClientOptionalArgs,DiscoveryClient 可選參數抽象基類。不同於上面三個必填參數,該參數是選填參數,實際生產下使用較少。實現代碼如下:


    public abstract class AbstractDiscoveryClientOptionalArgs<T> {
    /
    * 生成健康檢查回調的工廠
    /
    Provider<HealthCheckCallback> healthCheckCallbackProvider;
    /
    生成健康檢查處理器的工廠
    /
    Provider<HealthCheckHandler> healthCheckHandlerProvider;
    /
    向 Eureka-Server 註冊之前的處理器
    /
    PreRegistrationHandler preRegistrationHandler;
    /
    Jersey 過濾器集合
    /
    Collection<T> additionalFilters;
    /
    Jersey 客戶端
    /
    EurekaJerseyClient eurekaJerseyClient;
    /
    生成 Jersey 客戶端的工廠的工廠
    /
    TransportClientFactories transportClientFactories;
    /*
    * Eureka 事件監聽器集合
    */
    private Set<EurekaEventListener> eventListeners;
    }
    • com.netflix.appinfo.HealthCheckCallback,健康檢查回調接口,目前已經廢棄,使用 HealthCheckHandler 替代,你可以不關注該參數
    • com.netflix.appinfo.HealthCheckHandler,健康檢查處理器接口,目前暫未提供合適的默認實現,唯一提供的 com.netflix.appinfo.HealthCheckCallbackToHandlerBridge,用於將 HealthCheckCallback 橋接成 HealthCheckHandler,實現代碼如下:

      // HealthCheckHandler.java
      public interface HealthCheckHandler {
      InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);
      }
      // HealthCheckCallbackToHandlerBridge.java
      public class HealthCheckCallbackToHandlerBridge implements HealthCheckHandler {
      private final HealthCheckCallback callback;
      public HealthCheckCallbackToHandlerBridge() {
      callback = null;
      }
      public HealthCheckCallbackToHandlerBridge(HealthCheckCallback callback) {
      this.callback = callback;
      }
      @Override
      public InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus) {
      if (null == callback || InstanceInfo.InstanceStatus.STARTING == currentStatus
      || InstanceInfo.InstanceStatus.OUT_OF_SERVICE == currentStatus) { // Do not go to healthcheck handler if the status is starting or OOS.
      return currentStatus;
      }
      return callback.isHealthy() ? InstanceInfo.InstanceStatus.UP : InstanceInfo.InstanceStatus.DOWN;
      }
      }

    • com.netflix.discovery.PreRegistrationHandler,向 Eureka-Server 註冊之前的處理器接口,目前暫未提供默認實現。通過實現該接口,可以在註冊前做一些自定義的處理。實現代碼如下:


      public interface PreRegistrationHandler {
      void beforeRegistration();
      }
      • x
      • additionalFilters,Jersey 過濾器集合。這裏聲明泛型 <T> 的原因,Jersey 1.X 和 Jersey 2.X 的過濾器接口不同,通過泛型來支持。實現代碼如下:

        // Jersey1DiscoveryClientOptionalArgs.java
        public class Jersey1DiscoveryClientOptionalArgs extends AbstractDiscoveryClientOptionalArgs<ClientFilter> {
        }
        // Jersey2DiscoveryClientOptionalArgs.java
        public class Jersey2DiscoveryClientOptionalArgs extends AbstractDiscoveryClientOptionalArgs<ClientRequestFilter> {
        }
        // DiscoveryClientOptionalArgs.java
        public static class DiscoveryClientOptionalArgs extends Jersey1DiscoveryClientOptionalArgs {
        }
        • Jersey 1.X 使用 ClientFilter 。ClientFilter 目前有兩個過濾器實現:EurekaIdentityHeaderFilter 、DynamicGZIPContentEncodingFilter 。
        • Jersey 2.X 使用 ClientRequestFilter 。
        • DiscoveryClient 使用 DiscoveryClientOptionalArgs,即 Jersey 1.X 。


    • eurekaJerseyClient,Jersey 客戶端。該參數目前廢棄,使用下面 TransportClientFactories 參數來進行生成。
    • com.netflix.discovery.shared.transport.jersey.TransportClientFactories,生成 Jersey 客戶端工廠的工廠接口。目前有 Jersey1TransportClientFactories 、Jersey2TransportClientFactories 兩個實現。TransportClientFactories 實現代碼如下:

      // TransportClientFactories.java
      public interface TransportClientFactories<F> {
      @Deprecated
      TransportClientFactory newTransportClientFactory(final Collection<F> additionalFilters,
      final EurekaJerseyClient providedJerseyClient);
      TransportClientFactory newTransportClientFactory(final EurekaClientConfig clientConfig,
      final Collection<F> additionalFilters,
      final InstanceInfo myInstanceInfo);
      }
      // TransportClientFactory.java
      public interface TransportClientFactory {
      EurekaHttpClient newClient(EurekaEndpoint serviceUrl);
      void shutdown();
      }
      • 第一個方法已經廢棄,這就是爲什麼說上面的 eurekaJerseyClient 參數( 不是 EurekaJerseyClient 類)已經廢棄,被第二個方法取代。相比來說,第二個方法對 EurekaJerseyClient 創建封裝會更好。

    • com.netflix.discovery.EurekaEventListener,Eureka 事件監聽器。實現代碼如下:


      // EurekaEventListener.java
      public interface EurekaEventListener {
      }
      // EurekaEvent.java
      public interface EurekaEvent {
      }
      // DiscoveryEvent.java
      public abstract class DiscoveryEvent implements EurekaEvent {
      private final long timestamp;
      }



3.2 構造方法

DiscoveryClient 的構造方法實現代碼相對較多,已經將代碼切塊 + 中文註冊,點擊 DiscoveryClient 鏈接,對照下面每個小結閱讀理解。


3.2.1 賦值 AbstractDiscoveryClientOptionalArgs

// DiscoveryClient.java 構造方法
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}

3.2.2 賦值 ApplicationInfoManager、EurekaClientConfig

// DiscoveryClient.java 構造方法
this.applicationInfoManager = applicationInfoManager;
InstanceInfo myInfo = applicationInfoManager.getInfo();
clientConfig = config;
staticClientConfig = clientConfig;
transportConfig = config.getTransportConfig();
instanceInfo = myInfo;
if (myInfo != null) {
appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId(); // 無實際業務用途,用於打 logger
} else {
logger.warn("Setting instanceInfo to a passed in null value");
}

3.2.3 賦值 BackupRegistry

this.backupRegistryProvider = backupRegistryProvider;

3.2.4 初始化 InstanceInfoBasedUrlRandomizer

TODO[0016]:InstanceInfoBasedUrlRandomizer


this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);

3.2.5 初始化 Applications 在本地的緩存

// DiscoveryClient.java 變量
/
* Applications 在本地的緩存
/
private final AtomicReference<Applications> localRegionApps = new AtomicReference<Applications>();
/
拉取註冊信息次數
* monotonically increasing generation counter to ensure stale threads do not reset registry to an older version
*/
private final AtomicLong fetchRegistryGeneration;
// DiscoveryClient.java 構造方法
localRegionApps.set(new Applications());
fetchRegistryGeneration = new AtomicLong(0);

3.2.6 獲取哪些 Region 集合的註冊信息

// DiscoveryClient.java 變量
/
* 獲取哪些區域( Region )集合的註冊信息
/
private final AtomicReference<String> remoteRegionsToFetch;
/
獲取哪些區域( Region )集合的註冊信息
*/
private final AtomicReference<String[]> remoteRegionsRef;
// DiscoveryClient.java 構造方法
remoteRegionsToFetch = new AtomicReference<>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));

3.2.7 初始化拉取、心跳的監控

// DiscoveryClient.java 變量
/
* 最後成功從 Eureka-Server 拉取註冊信息時間戳
/
private volatile long lastSuccessfulRegistryFetchTimestamp = -1;
/
最後成功向 Eureka-Server 心跳時間戳
/
private volatile long lastSuccessfulHeartbeatTimestamp = -1;
/
心跳監控
/
private final ThresholdLevelsMetric heartbeatStalenessMonitor;
/
拉取監控
*/
private final ThresholdLevelsMetric registryStalenessMonitor;
// DiscoveryClient.java 構造方法
if (config.shouldFetchRegistry()) {
this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
if (config.shouldRegisterWithEureka()) {
this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}

  • 每次成功向 Eureka-Serve 心跳或者從從 Eureka-Server 拉取註冊信息後,都會更新相應時間戳。
  • 配合 Netflix Servo 實現監控信息採集。
  • com.netflix.discovery.util.ThresholdLevelsMetric 感興趣的同學可以點擊鏈接查看。本文暫不拓展開,後面另開文章分享。(TODO[0012]:監控相關)

3.2.8 結束初始化,當無需和 Eureka-Server 交互

// DiscoveryClient.java 構造方法
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, this.getApplications().size());
return; // no need to setup up an network tasks and we are done
}

3.2.9 初始化線程池

// DiscoveryClient.java 變量
/
* 線程池
A scheduler to be used for the following 3 tasks: 【目前只有兩個】
* - updating service urls
* - scheduling a TimedSuperVisorTask
/
private final ScheduledExecutorService scheduler;
// additional executors for supervised subtasks
/
心跳執行器
/
private final ThreadPoolExecutor heartbeatExecutor;
/*
* {@link #localRegionApps} 刷新執行器
*/
private final ThreadPoolExecutor cacheRefreshExecutor;
// DiscoveryClient.java 構造方法
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff

  • scheduler定時任務線程池,初始化大小爲 2,一個給 heartbeatExecutor,一個給 cacheRefreshExecutor
  • heartbeatExecutorcacheRefreshExecutor 在提交給 scheduler 才聲明具體的任務

3.2.10 初始化 Eureka 網絡通信相關

// DiscoveryClient.java 構造方法
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);

3.2.11 初始化 InstanceRegionChecker

// DiscoveryClient.java 構造方法
AzToRegionMapper azToRegionMapper;
if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
} else {
azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
}
if (null != remoteRegionsToFetch.get()) {
azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
}
instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());

  • com.netflix.discovery.AzToRegionMapper,主要用於亞馬遜 AWS,跳過。
  • com.netflix.discovery.InstanceRegionChecker,應用實例信息區域( region )校驗,實現代碼如下:

    public class InstanceRegionChecker {
    // ... 省略和亞馬遜 AWS 相關的屬性和方法
    /**
    * 本地區域( Region )
    */
    private final String localRegion;
    public boolean isLocalRegion(@Nullable String instanceRegion) {
    return null == instanceRegion || instanceRegion.equals(localRegion); // no region == local
    }
    public String getLocalRegion() {
    return localRegion;
    }
    }

3.2.12 從 Eureka-Server 拉取註冊信息

// DiscoveryClient.java 構造方法
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}

  • 調用 #fetchRegistry(false) 方法,從 Eureka-Server 初始拉取註冊信息。在(TO後文鏈接)詳細解析。
  • 調用 #fetchRegistryFromBackup() 方法,若初始拉取註冊信息失敗,從備份註冊中心獲取。實現代碼如下:

    // DiscoveryClient.java
    private void fetchRegistryFromBackup() {
    try {
    @SuppressWarnings("deprecation")
    BackupRegistry backupRegistryInstance = newBackupRegistryInstance();
    if (null == backupRegistryInstance) { // backward compatibility with the old protected method, in case it is being used.
    backupRegistryInstance = backupRegistryProvider.get();
    }
    if (null != backupRegistryInstance) {
    Applications apps = null;
    if (isFetchingRemoteRegionRegistries()) {
    String remoteRegionsStr = remoteRegionsToFetch.get();
    if (null != remoteRegionsStr) {
    apps = backupRegistryInstance.fetchRegistry(remoteRegionsStr.split(","));
    }
    } else {
    apps = backupRegistryInstance.fetchRegistry();
    }
    if (apps != null) {
    final Applications applications = this.filterAndShuffle(apps);
    applications.setAppsHashCode(applications.getReconcileHashCode());
    localRegionApps.set(applications);
    logTotalInstances();
    logger.info("Fetched registry successfully from the backup");
    }
    } else {
    logger.warn("No backup registry instance defined & unable to find any discovery servers.");
    }
    } catch (Throwable e) {
    logger.warn("Cannot fetch applications from apps although backup registry was specified", e);
    }
    }
    • BackupRegistry 目前暫未提供默認實現,需要自行相關邏輯。


3.2.13 執行向 Eureka-Server 註冊之前的處理器

// DiscoveryClient.java 構造方法
// call and execute the pre registration handler before all background tasks (inc registration) is started
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}

3.2.14 初始化定時任務

// DiscoveryClient.java 構造方法
initScheduledTasks();
// DiscoveryClient.java
private void initScheduledTasks() {
// 從 Eureka-Server 拉取註冊信息執行器
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
// 向 Eureka-Server 心跳(續租)執行器
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else &#12#123;
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}

3.2.15 向 Servo 註冊監控

// DiscoveryClient.java 構造方法
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}

3.2.16 初始化完成

// DiscoveryClient.java 變量
/*
初始化完成時間戳
*/
private final long initTimestampMs;
// DiscoveryClient.java 構造方法
// 【3.2.16】初始化完成
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, this.getApplications().size());

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