微服務治理1 - Eureka服務治理架構及客戶端裝配和啓動流程分析

服務治理是微服務架構中的核心模塊,主要用來實現各個微服務的自動化註冊和發現。隨着業務的增長和微服務實例的增長,服務治理可以大大減少手動配置的工作和手動配置錯誤,並且結合其他中間件實現服務的負載均衡。一個微服務治理框架一般包含三個核心要素:

  • 服務註冊中心:提供服務註冊和發現的功能
  • 服務提供者:服務提供者向服務註冊中心註冊自己的信息,如服務名、IP地址、端口號等信息
  • 服務消費者:服務消費者從服務註冊中心獲取服務列表,從而消費者可以知道去何處調用其所需要的服務

在實際場景中,一般服務註冊中心是單獨的微服務(在高可用環境下會是集羣),而服務提供者可能也是服務消費者,服務消費者也可能是服務提供者。

Spring Cloud Eurek是Spring Cloud社區提供的微服務中間件,使用Netflix Eureka來實現服務的註冊和發現。其包含兩部分:

  • Eureka服務端(Eureka Server),即服務註冊中心,支持集羣式部署
  • Eureka客戶端(Eureka Client),主要處理服務的註冊和發現,週期性的向Eureka服務端發送心跳信息來更新它的服務租約,當服務下線時通知Eureka服務端及時下線服務

1. Eureka服務治理架構

在實際使用中,Eureka的服務治理架構一般如下圖所示:

Eureka的高級架構圖

從圖可以看出在這個架構中,可以看到:

  • 有2個角色,即Eureka Server和Eureka Client
  • 每個區域有一個Eureka集羣,並且每個區域至少有一個eureka服務器可以處理區域故障,以防服務器癱瘓
  • Eureka Server間相互同步註冊信息
  • Eureka Client分爲Applicaton Service和Application Client,即服務提供者何服務消費者
  • Applicaton Service向Eureka Server註冊、續約和下線和獲得註冊表信息
  • Application Client獲得註冊信息並調用服務

在分佈式環境下需要考慮單點故障問題,因此需要在生產環境下爲各個服務部署多個服務結點,以提高服務的可用性, 對Eureka服務註冊中心同樣如此。

接下來介紹一下多個Eureka服務註冊中心的的關鍵配置,基於Spring Boot搭建Eureka服務的步驟請參考Eureka幫助文檔。當工程初始化好後,新建兩個配置文件applicaiton-peer1.yml和application-peer2.yml,內容分別是:

application-peer1.yml

spring:
  application:
    name: eureka-server
server:
  port: 9000
eureka:
  instance:
    hostname: peer1
  client:
    service-url:
      defaultZone: http://peer2:9001/eureka

aplication-peer2.yml

spring:
  application:
    name: eureka-server
server:
  port: 9001
eureka:
  instance:
    hostname: peer2
  client:
    service-url:
      defaultZone: http://peer1:9000/eureka

並在/etc/host中添加如下配置:

127.0.0.1 peer1
127.0.0.1 peer2

然後執行如下Maven命令啓動兩個Eureka註冊服務中心:

mvn spring-boot:run -Dspring-boot.run.profiles=peer1&
mvn spring-boot:run -Dspring-boot.run.profiles=peer2&

在瀏覽器訪問http://peer1:9000,可以看到如下結果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-x7HL0OX4-1582968212541)(/Users/liulijun/Library/Application Support/typora-user-images/image-20200225131016057.png)]

服務消費者和服務提供者需要將eureka.client.service-url.defaultZone修改爲

eureka:
  client:
    service-url:
      defaultZone: http://peer1:9000/eureka, http://peer2:9001/eureka

重啓服務提供者和服務消費者,此時就可以在peer1和peer2中都能看到服務消費者和服務提供者的信息。

通過配置多個Eureka註冊中心可以看到只要有一個Eureka服務註冊中心可用,服務消費者和服務提供者就可以通過Eureka服務註冊中心找到其要消費的服務,從而達到了Eureka服務高可用。

接下來將進入Eureka源碼的分析,由於Eureka的源碼包含服務端和客戶端模塊,文章將分開分析並先從Eureka客戶端代碼開始分析。

2. Eureka客戶端裝配

在應用中一般通過@EnableDiscoveryClient註解開啓Eureka客戶端功能,從註釋可以看到該註解的作用是開啓DiscoveryClient實現,但是深入到@EnableDiscoveryClient源碼中去看,該註解並沒有做什麼初始化操作。

嘗試在應用中去掉@EnableDiscoveryClient註解,重啓應用後發現服務依然註冊到了Eureka註冊中心,由此說明Eureka客戶端的裝配並不是由@EnableDiscoveryClient註解觸發的,那麼Eureka客戶端是在哪裏裝配的呢?

在Spring中有一種類似於Java SPI的加載機制,它在META-INF/spring.factories文件中配置接口的實現類名稱,然後在程序中讀取這些配置文件並實例化。因此,看下spring-cloud-netflix-eureka-client下的spring.factories文件,發現其中有如下配置:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

可以判斷Eureka客戶端是在EurekaDiscoveryClientConfigServiceBootstrapConfiguration類中裝配的。看下該類的源碼:

@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
		matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
@Import({ 
   (1)EurekaDiscoveryClientConfiguration.class, // this emulates
		// @EnableDiscoveryClient, the import
		// selector doesn't run before the
		// bootstrap phase
		(2)EurekaClientAutoConfiguration.class,
		EurekaReactiveDiscoveryClientConfiguration.class,
		ReactiveCommonsClientAutoConfiguration.class })
public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration {

}

(1) EurekaDiscoveryClientConfiguration注入了三個bean

  • discoveryClient:該Bean是最Eureka的重點,實現類爲EurekaDiscoveryClient
  • eurekaHealthCheckHandler:Eureka健康檢查的處理類

(2) EurekaClientAutoConfiguration是最複雜的裝配類,其依賴的裝配非常多,需要用到時再講解

EurekaReactiveDiscoveryClientConfigurationReactiveCommonsClientAutoConfiguration用於Spring響應式編程,這裏不做介紹。

3. Eureka客戶端啓動流程

上一節中說了步驟(1)中註冊的discoveryClient是整個Eureka的重點,因此接下來將重點分析這個Bean,首先看一下該Bean的註冊代碼,位於EurekaDiscoveryClientConfiguration類中:

@Bean
@ConditionalOnMissingBean
(3)public EurekaDiscoveryClient discoveryClient(EurekaClient client,
      EurekaClientConfig clientConfig) {
   return new EurekaDiscoveryClient(client, clientConfig);
}

(3)可以看到註冊的discoverClientEurekaDiscoveryClient類的實例,其需要另外兩個Bean進行初始化,即類型分別爲EurekaClientEurekaClientConfig的Bean。這兩個Bean是在上一節步驟(2)中介紹的EurekaClientAutoConfiguration類中註冊的,代碼如下:

@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class,
      search = SearchStrategy.CURRENT)
(4)public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
   EurekaClientConfigBean client = new EurekaClientConfigBean();
   ......
   return client;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnRefreshScope
protected static class RefreshableEurekaClientConfiguration {

   @Bean(destroyMethod = "shutdown")
   @ConditionalOnMissingBean(value = EurekaClient.class,
         search = SearchStrategy.CURRENT)
   @org.springframework.cloud.context.config.annotation.RefreshScope
   @Lazy
   public EurekaClient eurekaClient(ApplicationInfoManager manager,
         EurekaClientConfig config, EurekaInstanceConfig instance,
         @Autowired(required = false) HealthCheckHandler healthCheckHandler) {
      ......
      (5)CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
            config, this.optionalArgs, this.context);
      cloudEurekaClient.registerHealthCheck(healthCheckHandler);
      return cloudEurekaClient;
   }
  ......
}

(4) 註冊類型爲EurekaClientConfig的Bean,該類是EurekaClientConfig接口的實現類,其對應配置文件中以**eureka.client**開頭的配置

(5) 註冊類型爲CloudEurekaClient的Bean

通過步驟(3)-(5),此便可以梳理出EurekaDiscoveryClient類的關係,如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JnwvgE1J-1582968212542)(/Users/liulijun/Library/Application Support/typora-user-images/image-20200227132851820.png)]

圖分爲三個部分,分別用不同的顏色標記出來:

  • 左邊是Spring Cloud定義的接口,定義了用來發現服務的常用方法,Spring Cloud通過該接口可以方便的切換不同的服務治理框架
  • 右邊的所有接口和類都是Netflix Eureka開源包的實現,主要定義了針對Eureka的服務發現的抽象方法
  • 中間則是對Netflix Eureka服務的的封裝並實現了Spring Cloud的DiscoveryClient接口

沿着CloudEurekaClient類的繼承關係看,可以發現在Netflix Eureka開源包中DiscoveryClient是服務發現的主要實現類,從基註釋中知道其主要有四個功能:

  • 向Eureka Server(即服務註冊中心)註冊服務實例(服務提供者)
  • 向Eureka Server續約服務
  • 服務關閉時,向Eureka Server取消租約
  • 查詢Eureka Server中註冊的服務列表

接着進入DiscoveryClient的構造函數(只摘取比較重要的操作):

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
        ......
        fetchRegistryGeneration = new AtomicLong(0);

        remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
        remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
        //<!--------------------(6)--------------------------
        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;
        }
        // -------------------------------------------------!>
      
        logger.info("Initializing Eureka in region {}", clientConfig.getRegion());

        (7)if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
            ......

            return;  // no need to setup up an network tasks and we are done
        }

        try {
            // default size of 2 - 1 each for heartbeat and cacheRefresh
            (8)scheduler = Executors.newScheduledThreadPool(2,
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-%d")
                            .setDaemon(true)
                            .build());

            (9)heartbeatExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

            (10)cacheRefreshExecutor = new ThreadPoolExecutor(
                    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(),
                    new ThreadFactoryBuilder()
                            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                            .setDaemon(true)
                            .build()
            );  // use direct handoff

          //<!--------------------(11)--------------------------
            eurekaTransport = new EurekaTransport();
            scheduleServerEndpointTask(eurekaTransport, args);
          //----------------------(11)--------------------------!>

          

        (12)if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
            fetchRegistryFromBackup();
        }

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

        (14)if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
            try {
                if (!register() ) {
                    throw new IllegalStateException("Registration error at startup. Invalid server response.");
                }
            } catch (Throwable th) {
                logger.error("Registration error at startup: {}", th.getMessage());
                throw new IllegalStateException(th);
            }
        }

        // finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
        (15)initScheduledTasks();

        try {
        (16)    Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register timers", e);
        }
}

(6) 初始化監視器

(7) 當前實例不需要註冊到Eureka Server和從Eureka Server摘取服務列表時,構造方法到此結束

(8) 初始化調度器,該調度器用於執行心跳線程和緩存刷新線程

(9) 初始化心跳線程池(用於向服務註冊中心續約)

(10) 初始化緩存刷新線程池(用於從服務註冊中心摘取服務列表)

(11) 初始化EurekaTransport,該類封裝的屬性用於Eureka Client和Eureka Server通信

(12) 從Eureka Server拉取服務列表,fetchRegistry是第一次拉取註冊信息,如果拉取不成功的話則執行fetchRegistryFromBackup從備份註冊中心獲取

(13) 註冊之前的擴展點,轉爲爲null

(14) 向Eureka Server發起註冊,由於clientConfig.shouldEnforceRegistrationAtInit()默認爲false,因此不執行該註冊邏輯,而實際的服務註冊是在步驟(15)完成的

(15) 初始化定時任務和向Eureka Server註冊服務

(16) 向監視器註冊該類

以下就是Eureka客戶端的裝配和啓動流程,Eureka服務註冊的流程將在下一篇文章中分析。

Github博客地址
知乎

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