服務治理是微服務架構中的核心模塊,主要用來實現各個微服務的自動化註冊和發現。隨着業務的增長和微服務實例的增長,服務治理可以大大減少手動配置的工作和手動配置錯誤,並且結合其他中間件實現服務的負載均衡。一個微服務治理框架一般包含三個核心要素:
- 服務註冊中心:提供服務註冊和發現的功能
- 服務提供者:服務提供者向服務註冊中心註冊自己的信息,如服務名、IP地址、端口號等信息
- 服務消費者:服務消費者從服務註冊中心獲取服務列表,從而消費者可以知道去何處調用其所需要的服務
在實際場景中,一般服務註冊中心是單獨的微服務(在高可用環境下會是集羣),而服務提供者可能也是服務消費者,服務消費者也可能是服務提供者。
Spring Cloud Eurek是Spring Cloud社區提供的微服務中間件,使用Netflix Eureka來實現服務的註冊和發現。其包含兩部分:
- Eureka服務端(Eureka Server),即服務註冊中心,支持集羣式部署
- Eureka客戶端(Eureka Client),主要處理服務的註冊和發現,週期性的向Eureka服務端發送心跳信息來更新它的服務租約,當服務下線時通知Eureka服務端及時下線服務
1. 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
是最複雜的裝配類,其依賴的裝配非常多,需要用到時再講解
EurekaReactiveDiscoveryClientConfiguration
和ReactiveCommonsClientAutoConfiguration
用於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)可以看到註冊的discoverClient
爲EurekaDiscoveryClient
類的實例,其需要另外兩個Bean進行初始化,即類型分別爲EurekaClient
和EurekaClientConfig
的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服務註冊的流程將在下一篇文章中分析。