服務註冊與發現組件 Eureka 客戶端實現原理解析

  • 2019 年 10 月 7 日
  • 筆記

在前面的文章介紹了,如何使用服務註冊發現組件:Eureka,並給出使用示例。本文在此基礎上,將會講解 Eureka 客戶端實現的內幕,結合源碼深入實現的細節,知其所以然。客戶端需要重點關注以下幾點:

  • 從Eureka Server中拉取註冊表資訊
  • 全量拉取註冊表資訊
  • 增量式拉取註冊表資訊
  • 註冊錶快取刷新定時器與續租(心跳)定時器
  • 服務註冊與服務按需註冊
  • 服務實例的下線

本文摘錄於筆者出版的書籍 《Spring Cloud 微服務架構進階》

Eureka Client 結構

在Finchley版本的SpringCloud中,不需要添加任何的額外的註解就可以登記為Eureka Client,只需要在pom文件中添加 spring-cloud-starter-netflix-eureka-client的依賴。

為了跟蹤Eureka的運行機制,讀者可以打開SpringBoot的Debug模式來查看更多的輸出日誌:

logging:    level:      org.springframework: DEBUG

查看 spring-cloud-netflix-eureka-client的 src/main/resource.META-INF/spring.factories,查看Eureka Client有哪些自動配置類:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=  org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,  org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,  org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,  org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,  org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration    org.springframework.cloud.bootstrap.BootstrapConfiguration=  org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

排除掉與配置中心相關的自動配置類,從中可以找到三個與Eureka Client密切相關的自動配置類:

  • EurekaClientAutoConfiguration
  • RibbonEurekaAutoConfiguration
  • EurekaDiscoveryClientConfiguration

下面將對這些類進行分析,看看一個正常的Eureka Client需要做哪一些初始化配置。

EurekaClientAutoConfiguration

Eureke Client的自動配置類,負責了Eureka Client中關鍵的bean的配置和初始化,以下是其內比較重要的bean的介紹與作用。

EurekaClientConfig

提供了Eureka Client註冊到Eureka Server所需要的配置資訊,SpringCloud為其提供了一個默認配置類的 EurekaClientConfigBean,可以在配置文件中通過前綴 eureka.client+屬性名進行覆蓋。

ApplicationInfoManager

該類管理了服務實例的資訊類 InstanceInfo,其內包括Eureka Server上的註冊表所需要的資訊,代表了每個Eureka Client提交到註冊中心的數據,用以供服務發現。同時管理了實例的配置資訊 EurekaInstanceConfig,SpringCloud提供了一個 EurekaInstanceConfigBean的配置類進行默認配置,也可以在配置文件 application.yml中通過 eureka.instance+屬性名進行自定義配置。

EurekaInstanceConfigBean

繼承了 EurekaInstanceConfig介面,是Eureka Client註冊到伺服器上需要提交的關於服務實例自身的相關資訊,主要用於服務發現:

通常這些資訊在配置文件中的 eureka.instance前綴下進行設置,SpringCloud通過 EurekaInstanceConfigBean配置類提供了相關的默認配置。以下是一些比較關鍵的屬性,這些資訊都將註冊到註冊中心上。

public class EurekaInstanceConfigBean implements CloudEurekaInstanceConfig, EnvironmentAware {    // 服務實例的應用名  private String appname;  // 服務實例的Id,通常和appname共同唯一標記一個服務實例  private String instanceId;  // 自定義添加的元數據,由用戶使用以適配擴展業務需求  private Map<String, String> metadataMap;  // 如果服務實例部署在AWS上,該類將持有服務實例部署所在的數據中心的準確資訊  private DataCenterInfo dataCenterInfo;  // 服務實例的Ip地址  private String ipAddress;  // 服務實例主頁地址  private String homePageUrl;  // 服務實例健康檢查地址  private String healthCheckUrlPath;  // 服務實例的狀態地址  private String statusPageUrlPath    .....  }

DiscoveryClient

這是SpringCloud定義的用來服務發現的頂級介面,在Netflix Eureka或者consul都有相應的具體實現類,提供的方法如下:

public interface DiscoveryClient {       // 獲取實現類的描述      String description();        // 通過服務Id獲取服務實例的資訊      List<ServiceInstance> getInstances(String serviceId);        // 獲取所有的服務實例的Id      List<String> getServices();    }

其在Eureka方面的實現的相關的類結構圖:

EurekaDiscoveryClient繼承了 DiscoveryClient,但是通過查看 EurekaDiscoveryClient中的程式碼,會發現它是通過組合類 EurekaClient實現介面的功能,如下的 getInstance介面:

@Override  public List<ServiceInstance> getInstances(String serviceId) {      List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,false);      List<ServiceInstance> instances = new ArrayList<>();      for (InstanceInfo info : infos) {          instances.add(new EurekaServiceInstance(info));      }      return instances;  }

EurekaClient來自於 com.netflix.discovery包中,其默認實現為 com.netflix.discovery.DiscoveryClient,這屬於eureka-client的源程式碼,它提供了Eureka Client註冊到Server上、續租,下線以及獲取Server中註冊表資訊等諸多關鍵功能。SpringCloud通過組合方式調用了Eureka中的的服務發現方法,關於 EurekaClient的詳細程式碼分析將放在客戶端核心程式碼中介紹。為了適配 spring-cloud,spring提供了一個 CloudEurekaClient繼承了 com.netflix.discovery.DiscoveryClient,同時覆蓋了 onCacheRefreshed防止在 spring-boot還沒初始化時調用該介面出現 NullPointException。

上述的幾個配置類之間的關係非常緊密,數據之間存在一定的耦合,所以下面介紹一下它們之間的關係

首先是 EurekaInstanceConfig,程式碼位於 EurekaClientAutoConfiguration

@Bean  @ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)  public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils, ManagementMetadataProvider managementMetadataProvider) {      // 從配置文件中讀取屬性      String hostname = getProperty("eureka.instance.hostname");      boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));      String ipAddress = getProperty("eureka.instance.ipAddress");      boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));        String serverContextPath = env.getProperty("server.context-path", "/");      int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080")));        Integer managementPort = env.getProperty("management.server.port", Integer.class);// nullable. should be wrapped into optional      String managementContextPath = env.getProperty("management.server.context-path");// nullable. should be wrapped into optional      Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port", Integer.class);//nullable      EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);     // 設置非空屬性      instance.setNonSecurePort(serverPort);      instance.setInstanceId(getDefaultInstanceId(env));      instance.setPreferIpAddress(preferIpAddress);      if (StringUtils.hasText(ipAddress)) {          instance.setIpAddress(ipAddress);      }        if(isSecurePortEnabled) {          instance.setSecurePort(serverPort);      }        if (StringUtils.hasText(hostname)) {          instance.setHostname(hostname);      }      String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");      String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");        if (StringUtils.hasText(statusPageUrlPath)) {          instance.setStatusPageUrlPath(statusPageUrlPath);      }      if (StringUtils.hasText(healthCheckUrlPath)) {          instance.setHealthCheckUrlPath(healthCheckUrlPath);      }        ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort, serverContextPath, managementContextPath, managementPort);       .....      return instance;  }

從上面的程式碼可以發現, EurekaInstanceConfig的屬性主要通過 EurekaInstanceConfigBean的實現提供,同時也會嘗試從配置文件中讀取一部分配置,在例如 eureka.instance.hostname、 eureka.instance.status-page-url-path、 eureka.instance.health-check-url-path等等,它代表了應用實例的應該具備的資訊,然後這部分資訊會被封裝成 InstanceInfo,被註冊到Eureka Server中。

InstanceInfo是通過 InstanceInfoFactory(org.springframework.cloud.netflix.eureka)封裝 EurekaInstanceConfig中的屬性創建的,其中 InstanceInfo的屬性基本是 volatile,保證了記憶體中的該類資訊的一致性和原子性。

程式碼位於 InstanceInfoFactory。

public InstanceInfo create(EurekaInstanceConfig config) {          LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()                  .setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())                  .setDurationInSecs(config.getLeaseExpirationDurationInSeconds());          // 創建服務實例的資訊用來註冊到eureka server上          InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();            String namespace = config.getNamespace();          if (!namespace.endsWith(".")) {              namespace = namespace + ".";          }          builder.setNamespace(namespace).setAppName(config.getAppname())                  .setInstanceId(config.getInstanceId())                  .setAppGroupName(config.getAppGroupName())                  .setDataCenterInfo(config.getDataCenterInfo())                  .setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))                  .setPort(config.getNonSecurePort())                  .enablePort(InstanceInfo.PortType.UNSECURE,                          config.isNonSecurePortEnabled())                  .setSecurePort(config.getSecurePort())                  .enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())                  .setVIPAddress(config.getVirtualHostName())                  .setSecureVIPAddress(config.getSecureVirtualHostName())                  .setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())                  .setStatusPageUrl(config.getStatusPageUrlPath(),                          config.getStatusPageUrl())                  .setHealthCheckUrls(config.getHealthCheckUrlPath(),                          config.getHealthCheckUrl(), config.getSecureHealthCheckUrl())                  .setASGName(config.getASGName());           ....          InstanceInfo instanceInfo = builder.build();          instanceInfo.setLeaseInfo(leaseInfoBuilder.build());          return instanceInfo;      }

接著是 ApplicationInfoManager,程式碼位於 EurekaClientAutoConfiguration。

@Bean  @ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)  public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {      InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);      return new ApplicationInfoManager(config, instanceInfo);          }

通過組合 EurekaInstanceConfig和 InstanceInfo創建了 ApplicationInfoManager,屬於應用資訊管理器。

@Bean  @ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT)  public EurekaClientConfigBean eurekaClientConfigBean() {      EurekaClientConfigBean client = new EurekaClientConfigBean();      if ("bootstrap".equals(propertyResolver.getProperty("spring.config.name"))) {          // We don't register during bootstrap by default, but there will be another          // chance later.          client.setRegisterWithEureka(false);      }      return client;  }

前面有講到, EurekaClientConfig持有Eureka Client與Eureka Server進行交互的關鍵性配置資訊,類似 serviceUrl(Server地址), EurekaClient通過 EurekaClientConfig中配置資訊與Eureka Server進行服務註冊與發現。

最後是 EurekaClient,通過 ApplicationInfoManager與 EurekaClientConfig組合創建,即 EurekaClient同時持有了client的服務實例資訊用於服務發現,與Eureka Server註冊的配置用於服務註冊。

@Bean(destroyMethod = "shutdown")  @ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)  public EurekaClient eurekaClient(ApplicationInfoManager manager, EurekaClientConfig config){          return new CloudEurekaClient(manager, config, this.optionalArgs, this.context);  }

整體的類結構如下

EurekaRegistration、EurekaServiceRegistry、EurekaAutoServiceRegistration

這是SpringCloud適配Eureka所加的將服務註冊到服務註冊中心的相關類,先來看一下相關的類結構

Registration繼承了 ServiceInstance,代表了一個被註冊到服務發現系統的一個服務實例,必須具備的資訊如hostname和port等, Registration是 ServiceInstance的一個門面類

public interface ServiceInstance {        //獲取服務實例的serviceId      String getServiceId();        //獲取服務實例的hostname      String getHost();       //獲取服務實例的埠號      int getPort();        boolean isSecure();        //獲取服務實例的uri地址      URI getUri();        //獲取服務實例的元數據key-value對      Map<String, String> getMetadata();  }

對應Eureka, EurekaRegistration實現了 Registration,查看其中的程式碼,只是照搬了 EurekaInstanceConfigBean中的配置資訊,同時注入了 EurekaClient,為Eureka Client的服務註冊提供實現。

@Bean  @ConditionalOnBean(AutoServiceRegistrationProperties.class)  @ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)  public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient, CloudEurekaInstanceConfig instanceConfig, ApplicationInfoManager applicationInfoManager, ObjectProvider<HealthCheckHandler> healthCheckHandler) {          return EurekaRegistration.builder(instanceConfig)                  .with(applicationInfoManager)                  .with(eurekaClient)                  .with(healthCheckHandler)                  .build();      }

ServiceRegistry裡面提供了將服務實例註冊到服務註冊中心的相關介面:

public interface ServiceRegistry<R extends Registration> {        //註冊服務實例,registration當中通常有關於服務實例的資訊,例如hostname和port      void register(R registration);       //註銷服務實例      void deregister(R registration);        //關閉ServiceRegistry,通常在服務關閉的時候被調用      void close();        //設置服務實例的狀態      void setStatus(R registration, String status);        //獲取服務實例的狀態      <T> T getStatus(R registration);  }

其中在 EurekaServiceRegistry的註冊和下線的實現如下:

@Override  public void register(EurekaRegistration reg) {     // 初始化EurekaRegistration中的EurekaClient,如果為null      maybeInitializeClient(reg);        // 修改服務的狀態為UP      reg.getApplicationInfoManager()              .setInstanceStatus(reg.getInstanceConfig().getInitialStatus());      // 設置健康檢查      reg.getHealthCheckHandler().ifAvailable(healthCheckHandler ->              reg.getEurekaClient().registerHealthCheck(healthCheckHandler));  }    private void maybeInitializeClient(EurekaRegistration reg) {      reg.getApplicationInfoManager().getInfo();      reg.getEurekaClient().getApplications();  }    @Override  public void deregister(EurekaRegistration reg) {      if (reg.getApplicationInfoManager().getInfo() != null) {          // 設置服務的狀態為DOWN          reg.getApplicationInfoManager().setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);      }  }

在上面的程式碼中可以發現,對服務的註冊和下線僅僅是修改了服務當前的狀態,其實在 EurekaClient的介面實現類中有專門對 InstanceStatus狀態修改的監聽,當服務實例的資訊改變時就會觸發不同的事件進行處理。

EurekaAutoServiceRegistration,顧名思義,就是服務實例的自動註冊,由前面的類圖可知,該類繼承了 SmartLifecycle的介面,這是 org.springframework.context包中的相關類,說明 EurekaAutoServiceRegistration類受到了Spring的生命周期的管理。

...  @EventListener(ServletWebServerInitializedEvent.class)  public void onApplicationEvent(ServletWebServerInitializedEvent event) {      int localPort = event.getWebServer().getPort();      if (this.port.get() == 0) {          log.info("Updating port to " + localPort);          this.port.compareAndSet(0, localPort);          start();      }  }    @EventListener(ContextClosedEvent.class)  public void onApplicationEvent(ContextClosedEvent event) {      if( event.getApplicationContext() == context ) {          stop();      }  }

在上述程式碼中,該類監聽了 ServletWebServerInitializedEvent和 ContextClosedEvent兩個事件,即在應用初始化階段調用 start()和應用上下文關閉階段調用 stop(),其實就是在應用啟動和關閉時分別進行服務的註冊和下線的自動操作。

EurekaAutoServiceRegistration的服務註冊和下線是直接調用了EurekaServiceRegistry中的方法。

@Override  public void start() {      // only set the port if the nonSecurePort or securePort is 0 and this.port != 0      if (this.port.get() != 0) {          if (this.registration.getNonSecurePort() == 0) {              this.registration.setNonSecurePort(this.port.get());          }            if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {              this.registration.setSecurePort(this.port.get());          }      }        if (!this.running.get() && this.registration.getNonSecurePort() > 0) {           // 註冊服務          this.serviceRegistry.register(this.registration);            this.context.publishEvent(                  new InstanceRegisteredEvent<>(this, this.registration.getInstanceConfig()));          this.running.set(true);      }  }  @Override  public void stop() {     // 服務下線      this.serviceRegistry.deregister(this.registration);      this.running.set(false);  }

EurekaDiscoveryClientConfiguration

EurekaDiscoveryClientConfiguration只做了兩件事,監聽了 RefreshScopeRefreshedEvent事件以及注入 EurekaHealthCheckHandler介面的實現類。

RefreshScopeRefreshedEvent事件一般在spring管理的bean被刷新的時候被拋出,此時說明應用環境的配置和參數有可能發生變化,於是需要重新註冊服務,防止註冊中心的服務實例資訊與本地資訊不一致。

@EventListener(RefreshScopeRefreshedEvent.class)  public void onApplicationEvent(RefreshScopeRefreshedEvent event) {      // 保證了一個刷新事件發生後client的重新註冊      if(eurekaClient != null) {          eurekaClient.getApplications();      }      if (autoRegistration != null) {          // 重新註冊防止本地資訊與註冊表中的資訊不一致          this.autoRegistration.stop();          this.autoRegistration.start();      }  }

RibbonEurekaAutoConfiguration

Eureka中配置負載均衡的配置類,具體關於Ribbon的內容將在其他章節進行講解,這裡就略過

客戶端核心程式碼

包結構

主要的程式碼位於 eureka-client中,項目的module為 eureka-client,版本為v1.8.7,這是Finchley版本的Spring Cloud所依賴的eureka版本

包結構如下

簡要的包介紹:

  • com.netflix.appinfo: 主要是關於eureka-client的配置資訊類,如上面提及的 EurekaInstanceConfig, InstanceInfo,其中也包含了類似 AmazonInfo、 DataCenterInfo等與AWS中的架構適配密切相關的介面,在此不做詳解的介紹,有興趣讀者可以自行去了解。
  • com.netflix.discovery: 主要實現Eureka-Client的服務發現和服務註冊功能。
    • com.netflix.discovery.shared.dns: DNS解析器。
    • com.netflix.discovery.shared.resolver: Euraka Endpoint解析器, EurekaEndpoint指的是服務端點,一般指的是Eureka Server的訪問地址,默認實現為 DefaultEndpoint, ClusterResolver將配置的Eureka Server地址解析為 EurekaEndpoint,這裡面用到了委託者設計模式,類圖如下,有很明顯的請求委託的處理過程。
    • com.netflix.discovery.shared.transport:Eureka Client與Eureka Server之間進行HTTP通訊的客戶端以及通訊的request和response的封裝類。
    • com.netflix.discovery.converters: 主要解決Eureka服務之間的數據傳輸的編碼與解碼,支援JSON、XML等格式。
    • com.netflix.discovery.guice: Google的 guice依賴注入配置包,類似 Spring的configuration。
    • com.netflix.discovery.provider: 提供的Jersey中請求與響應的序列化與反序列化實現,默認實現是 DefaultJerseyProvider。
    • com.netflix.discovery.providers: 目前只有 DefaultEurekaClientConfigProvider,提供 EurekaClientConfig工廠生成方法。
    • com.netflix.discovery.shared: Eureka Client與Eureka Server共享重用的方法。

DiscoveryClient

DiscoveryClient可以說是Eureka Client的核心類,負責了與Eureka Server交互的關鍵邏輯,具備了以下的職能:

  • 註冊服務實例到Eureka Server中;
  • 更新與Eureka Server的契約;
  • 在服務關閉時從Eureka Server中取消契約;
  • 查詢在Eureka Server中註冊的服務/實例的列表。

DiscoverClient的核心類圖如下:

DiscoveryClient的頂層介面為 LookupService,主要的目的是為了發現活躍中的服務實例。

public interface LookupService<T> {        //根據服務實例註冊的appName來獲取,獲取一個封裝有相同appName的服務實例資訊的容器     Application getApplication(String appName);      //返回當前註冊的所有的服務實例資訊     Applications getApplications();         //根據服務實例的id獲取         List<InstanceInfo> getInstancesById(String id);     //獲取下一個可能的Eureka Server來處理當前對註冊表資訊的處理,一般是通過循環的方式來獲取下一個Server     InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);  }

Application中持有一個特定應用的多個實例的列表,可以理解成同一個服務的集群資訊,它們都掛在同一個服務名appName下, InstanceInfo代表一個服務實例,部分程式碼如下:

public class Application {        private static Random shuffleRandom = new Random();        //服務名      private String name;        @XStreamOmitField      private volatile boolean isDirty = false;        @XStreamImplicit      private final Set<InstanceInfo> instances;        private final AtomicReference<List<InstanceInfo>> shuffledInstances;        private final Map<String, InstanceInfo> instancesMap;        .....  }

為了保證原子性操作以及數據的唯一性,防止臟數據, Application中對 InstanceInfo的操作都是同步操作,感受一下 Application.addInstance方法。

public void addInstance(InstanceInfo i) {      instancesMap.put(i.getId(), i);      synchronized (instances) {      instances.remove(i);      instances.add(i);      isDirty = true;      }  }

通過同步程式碼塊,保證每次只有有一個執行緒對instances進行修改,同時注意instancesMap採用的是 ConcurrentHashMap實現,保證了原子性的操作,所以不需要通過同步程式碼塊進行控制。

Applications中代表的是Eureka Server中已註冊的服務實例的集合資訊,主要是對 Application的封裝,裡面的操作大多也是的同步操作。

EurekaClient繼承了 LookupService介面,為 DiscoveryClient提供了一個上層的介面,目的是試圖方便從eureka 1.x 到eureka 2.x 的過渡,這說明 EurekaClient這個介面屬於比較穩定的介面,即使在下一大階段也會被依舊保留。

EurekaCient在 LookupService的基礎上擴充了更多的介面,提供了更豐富的獲取服務實例的功能,主要有:

  • 提供了多種的方式獲取 InstanceInfo,例如根據region,Eureka Server地址等獲取;
  • 提供了本地客戶端(位於的區域,可用區等)的數據,這部分與AWS密切相關;
  • 提供了為客戶端註冊和獲取健康檢查處理器;

除去查詢相關的介面,關注 EurekaClient中的以下兩個介面:

// 為Eureka Client註冊健康檢查處理器      // 一旦註冊,客戶端將通過調用新註冊的健康檢查處理器來對註冊中instanceInfo      // 進行一個按需更新,隨後按照eurekaclientconfig.getinstanceinforeplicationintervalseconds()      // 中配置的指定時間調用HealthCheckHandler      public void registerHealthCheck(HealthCheckHandler healthCheckHandler);        // 為eureka client註冊一個EurekaEventListener(事件監聽器)      // 一旦註冊,當eureka client的內部狀態發生改變的時候,將會調用EurekaEventListener.onEvent()      // 觸發一定的事件。可以通過這種方式監聽client的更新而非通過輪詢的方式詢問client      public void registerEventListener(EurekaEventListener eventListener);

Eureka Server一般通過心跳(heartbeats)來識別一個實例的狀態。Eureka Client中存在一個定時任務定時通過 HealthCheckHandler檢測當前client的狀態,如果client的狀態發生改變,將會觸發新的註冊事件,同步Eureka Server的註冊表中該服務實例的相關資訊。

public interface HealthCheckHandler {      InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);  }

在 spring-cloud-netflix-eureka-client中實現了這個的介面, EurekaHealthCheckHandler,主要的組合了 spring-boot-actuator中的 HealthAggregator和 HealthIndicator實現了對 spring-boot應用的狀態檢測。

主要有以下的狀態:

public enum InstanceStatus {      UP, // 可以接受服務請求      DOWN, // 無法發送流量-健康檢查失敗      STARTING, // 正在啟動,無法發送流量      OUT_OF_SERVICE, // 服務關閉,不接受流量      UNKNOWN; // 未知狀態      }

Eureka中的事件模式,這是一個很明顯的觀察者模式,以下為它的類圖類圖:

客戶端的服務註冊與發現

在 DiscoveryClient的程式碼中,有實現服務註冊與發現的功能的具體程式碼。在 DiscoveryClient構造函數中,Eureka Client會執行從Eureka Server中拉取註冊表資訊,註冊自身等操作。DiscoveryClient的構造函數如下:

DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,  AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider)

ApplicationInfoManager和 EurekaClientConfig在前面的介紹中已經了解,一個是封裝當前服務實例的配置資訊的類,另一個是封裝了client與server交互配置資訊的類, AbstractDiscoveryClientOptionalArgs和 BackupRegistry是未介紹過的

BackupRegistry的介面程式碼如下:

@ImplementedBy(NotImplementedRegistryImpl.class)  public interface BackupRegistry {        Applications fetchRegistry();        Applications fetchRegistry(String[] includeRemoteRegions);  }

它充當了備份註冊中心的職責,當Eureka Client無法從任何一個Eureka Server中獲取註冊表資訊時, BackupRegistry將被調用以獲取註冊表資訊,但是默認的實現是 NotImplementedRegistryImpl,即沒有實現。

public abstract class AbstractDiscoveryClientOptionalArgs<T> {      // 生成健康檢查回調的工廠類,HealthCheckCallback已廢棄         Provider<HealthCheckCallback> healthCheckCallbackProvider;     // 生成健康處理器的工廠類     Provider<HealthCheckHandler> healthCheckHandlerProvider;     // 向Eureka Server註冊之前的預處理器     PreRegistrationHandler preRegistrationHandler;     // Jersey過濾器集合,Jersey1和Jersey2均可使用     Collection<T> additionalFilters;     // Jersey客戶端,主要用於client與server之間的HTTP交互     EurekaJerseyClient eurekaJerseyClient;     // 生成Jersey客戶端的工廠     TransportClientFactory transportClientFactory;     // 生成Jersey客戶端的工廠的工廠     TransportClientFactories transportClientFactories;     // Eureka事件的監聽器     private Set<EurekaEventListener> eventListeners;  ....  }

AbstractDiscoveryClientOptionalArgs是用於注入一些可選參數的,以及一些 jersey1和 jersey2通用的過濾器, @Inject(optional=true)屬性說明了該參數的可選性

在構造方法中,忽略掉大部分的賦值操作,逐步了解配置類中的屬性會對 DiscoveryClient的行為造成什麼影響

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;  }

config.shouldFetchRegistry()(對應配置為 eureka.client.fetch-register),為true表示Eureka Client將從Eureka Server中拉取註冊表的資訊,config.shouldRegisterWithEureka(對應配置為 eureka.client.register-with-eureka),為true表示Eureka Client將註冊到Eureka Server中。

如果上述的兩個配置均為false,那麼Discovery的初始化就直接結束,表示該客戶端既不進行服務註冊也不進行服務發現

接著初始化一個基於執行緒池的定時器執行緒池 ScheduledExecutorService,執行緒池大小為2,一個用於心跳,一個用於快取刷新,同時初始化了心跳和快取刷新執行緒池(ThreadPoolExecutor)。關於 ScheduledExecutorService與 ThreadPoolExecutor之間的關係在此不展開。

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

接著初始化了Eureka Client與Eureka Server進行HTTP交互的Jersy客戶端,將 AbstractDiscoveryClientOptionalArgs中的屬性用來構建 EurekaTransport。

eurekaTransport = new EurekaTransport();  scheduleServerEndpointTask(eurekaTransport, args);

EurekaTransport是 DiscoveryClient中的一個內部類,其內封裝了 DiscoveryClient與Eureka Server進行HTTP調用的Jersy客戶端:

private static final class EurekaTransport {     // Server endPoint解析器      private ClosableResolver bootstrapResolver;      // Jersy客戶端生成工廠      private TransportClientFactory transportClientFactory;      // 註冊客戶端      private EurekaHttpClient registrationClient;      // 註冊客戶端生成工廠      private EurekaHttpClientFactory registrationClientFactory;      // 發現服務客戶端      private EurekaHttpClient queryClient;      // 發現服務客戶端生成工廠       private EurekaHttpClientFactory queryClientFactory;         ....    }

關於AWS region中的相關配置略過。

Tags: