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

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

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

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

Eureka Client 結構

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

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

logging:
 level:
    org.springframework: DEBUG
複製代碼

查看spring-cloud-netflix-eureka-clientsrc/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+屬性名進行覆蓋。app

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.hostnameeureka.instance.status-page-url-patheureka.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);
		}
複製代碼

經過組合EurekaInstanceConfigInstanceInfo建立了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,經過ApplicationInfoManagerEurekaClientConfig組合建立,即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等, RegistrationServiceInstance的一個門面類

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

複製代碼

在上述代碼中,該類監聽了ServletWebServerInitializedEventContextClosedEvent兩個事件,即在應用初始化階段調用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的配置信息類,如上面說起的EurekaInstanceConfigInstanceInfo,其中也包含了相似AmazonInfoDataCenterInfo等與AWS中的架構適配密切相關的接口,在此不作詳解的介紹,有興趣讀者能夠自行去了解。
  • com.netflix.discovery: 主要實現Eureka-Client的服務發現和服務註冊功能。
    • com.netflix.discovery.converters: 主要解決Eureka服務之間的數據傳輸的編碼與解碼,支持JSON、XML等格式。
    • com.netflix.discovery.guice: Googleguice依賴注入配置包,相似Spring的configuration。
    • com.netflix.discovery.provider: 提供的Jersey中請求與響應的序列化與反序列化實現,默認實現是DefaultJerseyProvider
    • com.netflix.discovery.providers: 目前只有DefaultEurekaClientConfigProvider,提供EurekaClientConfig工廠生成方法。
    • com.netflix.discovery.shared: Eureka Client與Eureka Server共享重用的方法。
      • com.netflix.discovery.shared.dns: DNS解析器。
      • com.netflix.discovery.shared.resolver: Euraka Endpoint解析器,EurekaEndpoint指的是服務端點,通常指的是Eureka Server的訪問地址,默認實現爲DefaultEndpointClusterResolver將配置的Eureka Server地址解析爲EurekaEndpoint,這裏面用到了委託者設計模式,類圖以下,有很明顯的請求委託的處理過程。
      • com.netflix.discovery.shared.transport: Eureka Client與Eureka Server之間進行HTTP通訊的客戶端以及通訊的request和response的封裝類。

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這個接口屬於比較穩定的接口,即便在下一大階段也會被依舊保留。

EurekaCientLookupService的基礎上擴充了更多的接口,提供了更豐富的獲取服務實例的功能,主要有:

  • 提供了多種的方式獲取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中的HealthAggregatorHealthIndicator實現了對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) 
複製代碼

ApplicationInfoManagerEurekaClientConfig在前面的介紹中已經瞭解,一個是封裝當前服務實例的配置信息的類,另外一個是封裝了client與server交互配置信息的類, AbstractDiscoveryClientOptionalArgsBackupRegistry是未介紹過的

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是用於注入一些可選參數的,以及一些jersey1jersey2通用的過濾器,@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)。關於ScheduledExecutorServiceThreadPoolExecutor之間的關係在此不展開。

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);
複製代碼

EurekaTransportDiscoveryClient中的一個內部類,其內封裝了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;
 	
 	....

}
複製代碼

關於AWSregion中的相關配置略過。

客戶端的更多內容,將會在下篇文章介紹,敬請關注。

詳細瞭解本書:地址

推薦閱讀

微服務合集

訂閱最新文章,歡迎關注個人公衆號

微信公衆號
相關文章
相關標籤/搜索