搞懂Spring Cloud Config配置信息自動更新原理
我們知道 Spring Cloud Config 是 Spring Cloud 提供的配置中心實現(xiàn)工具,我們可以通過它把配置信息存放在 Git 等第三方配置倉庫中。每當 Spring Cloud Config 客戶端啟動時,就會發(fā)送 HTTP 請求到服務器端獲取配置信息,這點比較好理解。但事實上,在 Git 中更改了配置信息之后,客戶端并不會主動再次請求最新配置,而是使用緩存到本地的原有配置信息。如圖 1。
圖 1 配置信息自動更新問題
那么問題就來了,在這種情況下, Spring Cloud Config 是如何實時獲取到更改后的配置呢?這就是今天我們要討論的內(nèi)容。通過理解 Spring Cloud Config 配置信息自動更新的執(zhí)行過程,有助于我們深入把握框架的底層原理。
在對底層原理進行詳細展開之前,我們先來給出 Spring Cloud Config 應對這一問題的具體做法。事實上,Spring Cloud Config 能夠做到配置信息的自動更新,是依賴于 Spring Cloud 中的另一個組件,即 Spring Cloud Bus。
Spring Cloud Bus 是 Spring Cloud 中用于實現(xiàn)消息總線的專用組件,它集成了 RabbitMQ、Kafka 等主流消息中間件。當我們在 Spring Cloud Config 服務器端代碼工程的類路徑中添加 Spring Cloud Bus 的引用并啟動應用程序之后,Spring Boot Actuator 就為我們提供了/actuator/bus-refresh 端點, 通過訪問該端點就可以達到對客戶端所有服務實例的配置信息進行自動更新的效果。在這種方案中,服務端會主動通知所有客戶端進行配置信息的更新,這樣我們就無需關注各個客戶端,而只對服務端進行操作即可。
是不是聽起來有點神奇?整個實現(xiàn)過程我們至少要搞清楚三大問題,如圖 2 所示。
圖 2 實現(xiàn)配置信息自動更新的三個問題
針對這三個問題,接下來我們將結合源碼逐一展開討論。
問題一:如何自動調(diào)用服務器端所暴露的/actuator/bus-refresh 端點?
在現(xiàn)代軟件開發(fā)過程中,開放式平臺是一種常見的軟件服務形態(tài)。我們可以把 Spring Cloud Config Server 所提供的 HTTP 端點視為一種開放式的接口,以供 Git 等第三方工具進行訪問和集成。
基于這種思想,我們可以把服務器端/actuator/bus-refresh 端點對外進行暴露,然后第三方工具通過這個暴露的端點進行集成。例如,在 Github 中就設計了一種 Webhook 機制,并提供了用戶界面供我們配置所需要集成的端點以及對應的操作,操作方法如圖 3 所示。
圖 3 Github 的 Webhook 配置界面(來自 Github 官網(wǎng))
我們可以在上圖的 Payload URL 中設置/actuator/bus-refresh 端點地址。所謂的 Webhook,實際上就是一種回調(diào)。通過 Webhook,當我們提交代碼時,Github 就會自動調(diào)用所配置的 HTTP 端點。也就是說,我們可以根據(jù)配置項信息的更新情況自動實現(xiàn)對/actuator/bus-refresh 端點的訪問。基于 Github 的配置倉庫實現(xiàn)方案,我們可以得到如下圖所示的系統(tǒng)結構圖。
圖 4 Github Webhook 機制執(zhí)行效果圖
現(xiàn)在,配置信息一旦有更新,Spring Cloud Config Server 就能從 Github 中獲取最新的配置信息了。
問題二:客戶端如何得知服務器端的配置信息已經(jīng)更新?
接下來我們關注第二個問題,即客戶端如何得知服務器端的配置信息已經(jīng)更新?
我們首先需要明確,調(diào)用了/actuator/bus-refresh 端點之后,系統(tǒng)內(nèi)部會發(fā)生了什么。這里我們快速瀏覽 Spring Cloud Bus 中的代碼工程,發(fā)現(xiàn)存在一個 RefreshBusEndpoint 端點類,如下所示:
@Endpoint(id = "bus-refresh")
public class RefreshBusEndpoint extends AbstractBusEndpoint {
@WriteOperation
public void busRefreshWithDestination(@Selector String destination) {
//發(fā)布 RefreshRemoteApplicationEvent 事件
publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), destination));
}
@WriteOperation
public void busRefresh() {
//發(fā)布 RefreshRemoteApplicationEvent 事件
publish(new RefreshRemoteApplicationEvent(this, getInstanceId(), null));
}
}
顯然,RefreshBusEndpoint 類對應于我們前面訪問的/bus-refresh 端點。可以看到,Spring Cloud Bus 在這里做的事情僅僅只是發(fā)布了一個新的 RefreshRemoteApplicationEvent 事件。
既然發(fā)送了事件,我們就需要尋找該事件的監(jiān)聽者。我們在 Spring Cloud Bus 中找到了 RefreshRemoteApplicationEvent 事件的監(jiān)聽器 RefreshListener,如下所示:
public class RefreshListener implements ApplicationListener<RefreshRemoteApplicationEvent> {
…
@Override
public void onApplicationEvent(RefreshRemoteApplicationEvent event) {
//執(zhí)行配置屬性的刷新操作
Set<String> keys = contextRefresher.refresh();
}
}
從類的定義中不難看出該監(jiān)聽器就是用來處理 RefreshRemoteApplicationEvent 事件。可以看到,在它的 onApplicationEvent 方法中同樣也是調(diào)用了 ContextRefresher 中的 refresh 方法進行配置屬性的刷新。
請注意,RefreshRemoteApplicationEvent 是一個遠程事件,將通過消息中間件進行發(fā)送,并被 Spring Cloud Config 客戶端所監(jiān)聽,處理流程如下圖所示:
圖 5 基于 Spring Cloud Bus 的事件傳播機制
問題三:客戶端如何實時獲取服務器端所更新的配置信息?
最后需要明確的第三個問題是,客戶端如何獲取服務器端所更新的配置信息,這就需要梳理 Spring Cloud Config Server 與注冊中心之間的關系。
我們知道配置中心作為整個微服務架構運行所需的基礎服務,需要確保其可用性。因為配置服務本身也是一個獨立的微服務,所以 Spring Cloud Config 實現(xiàn)高可用的方式很簡單。跟其他微服務一樣,它把自己注冊到注冊中心上,讓其他服務提供者或消費者通過注冊中心進行服務發(fā)現(xiàn)和獲取。
圖 6 Spring Cloud Config 與注冊中心之間的關系
顯然,在這種方式下,注冊中心的服務治理機制同時提供了服務器端的負載均衡和客戶端的配置功能,從而也就間接實現(xiàn)了高可用性。從另一個角度,我們也可以理解為,可以通過注冊中心獲取所有 Spring Cloud Config 客戶端服務的實例,從而在分布式環(huán)境下為獲取配置信息提供了一種簡便的手段。
Spring Cloud Config 提供了一個工具類 ConfigServerInstanceProvider 來完成與注冊中心之間的交互,代碼如下所示:
public class ConfigServerInstanceProvider {
private final DiscoveryClient client;
@Retryable(interceptor = "configServerRetryInterceptor")
public List<ServiceInstance> getConfigServerInstances(String serviceId) {
List<ServiceInstance> instances = this.client.getInstances(serviceId);
if (instances.isEmpty()) {
//拋出異常
}
return instances;
}
}
在這里,我們看到了熟悉的 DiscoveryClient,DiscoveryClient 通過同樣熟悉的 getInstances 方法從注冊中心中獲取 Spring Cloud Config 服務器實例,如下所示:
List<ServiceInstance> instances = this.client.getInstances(serviceId);
ConfigServerInstanceProvider 的調(diào)用者是 DiscoveryClientConfigServiceBootstrapConfiguration。現(xiàn)在我們來看這個 Spring Boot 自動配置類的定義,如下所示:
public class DiscoveryClientConfigServiceBootstrapConfiguration
implements SmartApplicationListener {
public void startup(ContextRefreshedEvent event) {
refresh();
}
}
可以看到,如果系統(tǒng)中生成了 ContextRefreshedEvent 事件就會觸發(fā)如下所示的 refresh 方法。
private void refresh() {
try {
////獲取 Spring Cloud Config 客戶端服務實例
String serviceId = this.config.getDiscovery().getServiceId();
List<String> listOfUrls = new ArrayList<>();
List<ServiceInstance> serviceInstances = this.instanceProvider.getConfigServerInstances(serviceId);
//遍歷服務實例列表
for (int i = 0; i < serviceInstances.size(); i++) {
ServiceInstance server = serviceInstances.get(i);
String url = getHomePage(server);
//獲取配置路徑
if (server.getMetadata().containsKey("configPath")) {
String path = server.getMetadata().get("configPath");
if (url.endsWith("/") && path.startsWith("/")) {
url = url.substring(0, url.length() - 1);
}
url = url + path;
}
//填充配置路徑
listOfUrls.add(url);
}
String[] uri = new String[listOfUrls.size()];
uri = listOfUrls.toArray(uri);
this.config.setUri(uri);
}
}
在上述 refresh 方法中,Spring Cloud Config 首先會獲取配置文件中配置項 spring.cloud.config.discovery.serviceId 所指定的服務實例 id,然后根據(jù) serviceId 從 ConfigServerInstanceProvider 中獲取注冊服務的實例對象集合 serviceInstances,最后循環(huán)遍歷 serviceInstances 來更新存儲在內(nèi)存中的配置屬性值。
至此,我們通過解答三個問題,引出了 Spring Cloud Config 中實現(xiàn)配置信息自動更新的三個步驟,并基于框架內(nèi)部一系列組件之間的交互過程剖析了底層實現(xiàn)原理。
總結
今天基于 Spring Cloud Config 框架剖析了實現(xiàn)配置信息自動更新的工作原理。拋出了三個與這個主題相關的核心問題,然后基于源碼對這些問題做了一一解答。
事實上,Spring Cloud Config 作為 Spring 自研的配置中心框架,其內(nèi)部大量使用了 Spring 現(xiàn)有的功能特性,比方說這節(jié)課中提到的 Spring 容器的事件發(fā)布和監(jiān)聽機制,又比方說 Spring Boot Acuator 中的端點機制以及 Spring Cloud Bus 所具備的消息通信總線機制。這點與我們學習 Netflix 旗下的 Eureka、Zuul 等框架不同。我們需要首先對 Spring 容器相關的知識體系有足夠的了解,才能更好地理解 Spring Cloud Config 的設計和實現(xiàn)方式。