攜程架構部開源的配置中心Apollo深度解讀
隨著程式功能的日益複雜,程式的配置日益增多:各種功能的開關、引數的配置、伺服器的地址……
對程式配置的期望值也越來越高:配置修改後實時生效,灰度釋出,分環境、分叢集管理配置,完善的許可權、稽核機制……
在這樣的大環境下,傳統的通過配置檔案、資料庫等方式已經越來越無法滿足開發人員對配置管理的需求。
想必大家都深有體會,我們做的專案都伴隨著各種配置檔案,而且總是本地配置一套配置檔案,測試服配置一套,正式服配置一套,有時候一不小心就改錯了,捱罵是小事,扣績效那可就鬧大了。
而且每當專案釋出的時候,配置檔案也會被打包進去,也就是配置檔案會跟著專案一起釋出。然後每次出現問題需要我們修改配置檔案的時候,我們總是得先在本地修改,然後重新發布才能讓新的配置生效。
當請求壓力越來越大,你的專案也會從 1 個節點變成多個節點,這個時候如果配置需要發生變化,對應的修改操作也是相同的,只需要在專案中修改一次即可,但對於釋出操作工作量就比之前大了很多,因為要釋出多個節點。
修改這些配置,增加的釋出的工作量降低了整體的工作效率,為了能夠提升工作效率,配置中心應運而生了,我們可以將配置統一存放在配置中心來進行管理。
總體而言在沒有引入配置中心之前,我們都會面臨以下問題:
-
配置散亂格式不標準,有的用 properties 格式,有的用 xml 格式,還有的存 DB,團隊傾向自造輪子,做法五花八門。
-
主要採用本地靜態配置,配置修改麻煩,配置修改一般需要經過一個較長的測試釋出週期。在分散式微服務環境下,當服務例項很多時,修改配置費時費力。
-
易引發生產事故,這個是我親身經歷,之前在一家網際網路公司,有團隊在釋出的時候將測試環境的配置帶到生產上,引發百萬級資損事故。
-
配置缺乏安全審計和版本控制功能,誰改的配置?改了什麼?什麼時候改的?無從追溯,出了問題也無法及時回滾。
-
增加了運維小哥哥的工作量,極大的損害了運維小哥哥和開發小哥哥的基情。
到底什麼是配置中心
配置中心就是把專案中各種個樣的配置、引數、開關,全部都放到一個集中的地方進行統一管理,並提供一套標準的介面。當各個服務需要獲取配置的時候,就來配置中心的介面拉取。當配置中心中的各種引數有更新的時候,也能通知到各個服務實時的過來同步最新的資訊,使之動態更新。
Apollo 簡介
Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,能夠集中化管理應用不同環境、不同叢集的配置,配置修改後能夠實時推送到應用端,並且具備規範的許可權、流程治理等特性。
Apollo支援4個維度管理Key-Value格式的配置(下面會詳細說明):
1、application (應用)
2、environment (環境)
3、cluster (叢集)
4、namespace (名稱空間)
什麼是配置
既然Apollo定位於配置中心,那麼在這裡有必要先簡單介紹一下什麼是配置。
按照我們的理解,配置有以下幾個屬性:
-
配置是獨立於程式的只讀變數
-
配置首先是獨立於程式的,同一份程式在不同的配置下會有不同的行為。
-
其次,配置對於程式是隻讀的,程式通過讀取配置來改變自己的行為,但是程式不應該去改變配置。
-
常見的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、Feature Switch、Server Urls等。
-
-
配置伴隨應用的整個生命週期
-
配置貫穿於應用的整個生命週期,應用在啟動時通過讀取配置來初始化,在執行時根據配置調整行為。
-
-
配置可以有多種載入方式
-
配置也有很多種載入方式,常見的有程式內部hard code,配置檔案,環境變數,啟動引數,基於資料庫等
-
-
配置需要治理
-
還有一類比較特殊的配置 - 框架類元件配置,比如CAT客戶端的配置。
-
雖然這類框架類元件是由其他團隊開發、維護,但是執行時是在業務實際應用內的,所以本質上可以認為框架類元件也是應用的一部分。
-
這類元件對應的配置也需要有比較完善的管理方式。
-
同一份程式在不同的環境(開發,測試,生產)、不同的叢集(如不同的資料中心)經常需要有不同的配置,所以需要有完善的環境、叢集配置管理
-
由於配置能改變程式的行為,不正確的配置甚至能引起災難,所以對配置的修改必須有比較完善的許可權控制
-
許可權控制
-
不同環境、叢集配置管理
-
框架類元件配置管理
-
為什麼使用Apollo,Apollo有哪些特徵
正是基於配置的特殊性,所以Apollo從設計之初就立志於成為一個有治理能力的配置釋出平臺,目前提供了以下的特性:
-
統一管理不同環境、不同叢集的配置
-
Apollo提供了一個統一介面集中式管理不同環境(environment)、不同叢集(cluster)、不同名稱空間(namespace)的配置。
-
同一份程式碼部署在不同的叢集,可以有不同的配置,比如zookeeper的地址等
-
通過名稱空間(namespace)可以很方便地支援多個不同應用共享同一份配置,同時還允許應用對共享的配置進行覆蓋
-
-
配置修改實時生效(熱釋出)
-
使用者在Apollo修改完配置併發布後,客戶端能實時(1秒)接收到最新的配置,並通知到應用程式
-
-
版本釋出管理
-
所有的配置釋出都有版本概念,從而可以方便地支援配置的回滾
-
-
灰度釋出
-
支援配置的灰度釋出,比如點了釋出後,只對部分應用例項生效,等觀察一段時間沒問題後再推給所有應用例項
-
-
許可權管理、釋出稽核、操作審計
-
應用和配置的管理都有完善的許可權管理機制,對配置的管理還分為了編輯和釋出兩個環節,從而減少人為的錯誤。
-
所有的操作都有審計日誌,可以方便地追蹤問題
-
-
客戶端配置資訊監控
-
可以在介面上方便地看到配置在被哪些例項使用
-
-
提供Java和.Net原生客戶端
-
提供了Java和.Net的原生客戶端,方便應用整合
-
支援Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便應用使用(需要Spring 3.1.1+)
-
同時提供了Http介面,非Java和.Net應用也可以方便地使用
-
-
提供開放平臺API
-
Apollo自身提供了比較完善的統一配置管理介面,支援多環境、多資料中心配置管理、許可權、流程治理等特性。不過Apollo出於通用性考慮,不會對配置的修改做過多限制,只要符合基本的格式就能儲存,不會針對不同的配置值進行鍼對性的校驗,如資料庫使用者名稱、密碼,Redis服務地址等
-
對於這類應用配置,Apollo支援應用方通過開放平臺API在Apollo進行配置的修改和釋出,並且具備完善的授權和許可權控制
-
-
部署簡單
-
配置中心作為基礎服務,可用性要求非常高,這就要求Apollo對外部依賴儘可能地少
-
目前唯一的外部依賴是MySQL,所以部署非常簡單,只要安裝好Java和MySQL就可以讓Apollo跑起來
-
Apollo還提供了打包指令碼,一鍵就可以生成所有需要的安裝包,並且支援自定義執行時引數
-
Apollo整體架構
首先我們來看看Applo的基本工作流程如下圖所示
1.使用者在配置中心對配置進行修改併發布
2.配置中心通知Apollo客戶端有配置更新
3.Apollo客戶端從配置中心拉取最新的配置、更新本地配置並通知到應用
接下來我們來看看Apollo的整體架構圖
上圖簡要描述了Apollo的總體設計,我們可以從下往上看:
-
Config Service提供配置的讀取、推送等功能,服務物件是Apollo客戶端
-
Admin Service提供配置的修改、釋出等功能,服務物件是Apollo Portal(管理介面)
-
Config Service和Admin Service都是多例項、無狀態部署,所以需要將自己註冊到Eureka中並保持心跳
-
在Eureka之上我們架了一層Meta Server用於封裝Eureka的服務發現介面
-
Client通過域名訪問Meta Server獲取Config Service服務列表(IP+Port),而後直接通過IP+Port訪問服務,同時在Client側會做load balance、錯誤重試
-
Portal通過域名訪問Meta Server獲取Admin Service服務列表(IP+Port),而後直接通過IP+Port訪問服務,同時在Portal側會做load balance、錯誤重試
-
為了簡化部署,我們實際上會把Config Service、Eureka和Meta Server三個邏輯角色部署在同一個JVM程序中
Why Eureka
為什麼我們採用Eureka作為服務註冊中心,而不是使用傳統的zk、etcd呢?我大致總結了一下,有以下幾方面的原因:
-
它提供了完整的Service Registry和Service Discovery實現
首先是提供了完整的實現,並且也經受住了Netflix自己的生產環境考驗,相對使用起來會比較省心。
和Spring Cloud無縫整合 -
的專案本身就使用了Spring Cloud和Spring Boot,同時Spring Cloud還有一套非常完善的開原始碼來整合Eureka,所以使用起來非常方便。
-
另外,Eureka還支援在我們應用自身的容器中啟動,也就是說我們的應用啟動完之後,既充當了Eureka的角色,同時也是服務的提供者。這樣就極大的提高了服務的可用性。
-
這一點是我們選擇Eureka而不是zk、etcd等的主要原因,為了提高配置中心的可用性和降低部署複雜度,我們需要儘可能地減少外部依賴。
Open Source -
最後一點是開源,由於程式碼是開源的,所以非常便於我們瞭解它的實現原理和排查問題。
各模組概要介紹
Config Service
-
提供配置獲取介面
-
提供配置更新推送介面(基於Http long polling)
-
服務端使用Spring DeferredResult實現非同步化,從而大大增加長連線數量
-
目前使用的tomcat embed預設配置是最多10000個連線(可以調整),使用了4C8G的虛擬機器實測- 可以支撐10000個連線,所以滿足需求(一個應用例項只會發起一個長連線)。介面服務物件為Apollo客戶端
Admin Service
-
提供配置管理介面
-
提供配置修改、釋出等介面
-
介面服務物件為Portal
Meta Server
-
Portal通過域名訪問Meta Server獲取Admin Service服務列表(IP+Port)
-
Client通過域名訪問Meta Server獲取Config Service服務列表(IP+Port)
-
Meta Server從Eureka獲取Config Service和Admin Service的服務資訊,相當於是一個Eureka Client
增設一個Meta Server的角色主要是為了封裝服務發現的細節,對Portal和Client而言,永遠通過一個 -
Http介面獲取Admin Service和Config Service的服務資訊,而不需要關心背後實際的服務註冊和發現元件
-
Meta Server只是一個邏輯角色,在部署時和Config Service是在一個JVM程序中的,所以IP、埠和Config Service一致
Eureka
-
基於Eureka和Spring Cloud Netflix提供服務註冊和發現
-
Config Service和Admin Service會向Eureka註冊服務,並保持心跳
-
為了簡單起見,目前Eureka在部署時和Config Service是在一個JVM程序中的(通過Spring Cloud Netflix)
Portal
-
提供Web介面供使用者管理配置
-
通過Meta Server獲取Admin Service服務列表(IP+Port),通過IP+Port訪問服務
-
在Portal側做load balance、錯誤重試
Client
-
Apollo提供的客戶端程式,為應用提供配置獲取、實時更新等功能
-
通過Meta Server獲取Config Service服務列表(IP+Port),通過IP+Port訪問服務
-
在Client側做load balance、錯誤重試
Apollo核心概念介紹
1、application
1、Apollo 客戶端在執行時需要知道當前應用是誰,從而可以根據不同的應用來獲取對應應用的配置。
2、每個應用都需要有唯一的身份標識,可以在程式碼中配置 app.id 引數來標識當前應用,Apollo 會根據此指來辨別當前應用。
2、environment
在實際開發中,我們的應用經常要部署在不同的環境中,一般情況下分為 開發、測試、生產 等等不同環境,不同環境中的配置也是不同的,在 Apollo 中預設提供了
四種環境:
FAT:功能測試環境
UAT:整合測試環境
DEV:開發環境
PRO:生產環境
在程式中如果想指定使用哪個環境,可以配置變數 env 的值為對應環境名稱即可。
3、cluster
1、一個應用下不同例項的分組,比如典型的可以按照資料中心分,把上海機房的應用例項分為一個叢集,把北京機房的應用例項分為另一個叢集。
2、對不同的叢集,同一個配置可以有不一樣的值,比如說上面所指的兩個北京、上海兩個機房設定兩個叢集,都有 mysql 配置引數,其中引數中配置的地址是不一樣的。
4、namespace
一個應用中不同配置的分組,可以簡單地把 namespace 類比為不同的配置檔案,不同型別的配置存放在不同的檔案中,如資料庫配置檔案,RPC 配置檔案等。
熟悉 SpringBoot 的都知道,SpringBoot 專案都有一個預設配置檔案 application.yml,如果還想用多個配置,可以建立多個配置檔案來存放不同的配置資訊,通過
指定 spring.profiles.active 引數指定應用不同的配置檔案。這裡的 namespace 概念與其類似,將不同的配置放到不同的配置 namespace 中。
Namespace 分為兩種許可權,分別為:
public(公共的):public許可權的 Namespace,能被任何應用獲取。
private(私有的):只能被所屬的應用獲取到。一個應用嘗試獲取其它應用 private 的 Namespace,Apollo 會報 “404” 異常。
Apollo實時釋出配置
1. 配置釋出後的實時推送設計
配置中心最重要的一個特性就是實時推送,正因為有這個特性,我們才可以依賴配置中心做很多事情。如圖所示。
圖 1 簡要描述了配置釋出的大致過程。
-
使用者在 Portal 中進行配置的編輯和釋出。
-
Portal 會呼叫 Admin Service 提供的介面進行釋出操作。
-
Admin Service 收到請求後,傳送 ReleaseMessage 給各個 Config Service,通知 Config Service 配置發生變化。
-
Config Service 收到 ReleaseMessage 後,通知對應的客戶端,基於 Http 長連線實現。
2. 傳送 ReleaseMessage 的實現方式
ReleaseMessage 訊息是通過 Mysql 實現了一個簡單的訊息佇列。之所以沒有采用訊息中介軟體,是為了讓 Apollo 在部署的時候儘量簡單,儘可能減少外部依賴,如圖所示。
上圖簡要描述了傳送 ReleaseMessage 的大致過程:
-
Admin Service 在配置釋出後會往 ReleaseMessage 表插入一條訊息記錄。
-
Config Service 會啟動一個執行緒定時掃描 ReleaseMessage 表,來檢視是否有新的訊息記錄。
-
Config Service 發現有新的訊息記錄,就會通知到所有的訊息監聽器。
-
訊息監聽器得到配置釋出的資訊後,就會通知對應的客戶端。
3. Config Service 通知客戶端的實現方式
通知採用基於 Http 長連線實現,主要分為下面幾個步驟:
-
客戶端會發起一個 Http 請求到 Config Service 的 notifications/v2 介面。
-
notifications/v2 介面通過 Spring DeferredResult 把請求掛起,不會立即返回。
-
如果在 60s 內沒有該客戶端關心的配置釋出,那麼會返回 Http 狀態碼 304 給客戶端。
-
如果發現配置有修改,則會呼叫 DeferredResult 的 setResult 方法,傳入有配置變化的 namespace 資訊,同時該請求會立即返回。
-
客戶端從返回的結果中獲取到配置變化的 namespace 後,會立即請求 Config Service 獲取該 namespace 的最新配置。
4. 原始碼解析實時推送設計
Apollo 推送涉及的程式碼比較多,本教程就不做詳細分析了,筆者把推送這裡的程式碼稍微簡化了下,給大家進行講解,這樣理解起來會更容易。
當然,這些程式碼比較簡單,很多細節就不做考慮了,只是為了能夠讓大家明白 Apollo 推送的核心原理。
傳送 ReleaseMessage 的邏輯我們就寫一個簡單的介面,用佇列儲存,測試的時候就呼叫這個介面模擬配置有更新,傳送 ReleaseMessage 訊息。具體程式碼如下所示。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
// 模擬配置更新, 向其中插入資料表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
@GetMapping("/addMsg")
public String addMsg() {
queue.add("xxx");
return "success";
}
}
訊息傳送之後,根據前面講過的 Config Service 會啟動一個執行緒定時掃描 ReleaseMessage 表,檢視是否有新的訊息記錄,然後取通知客戶端,在這裡我們也會啟動一個執行緒去掃描,具體程式碼如下所示。
@Component
public class ReleaseMessageScanner implements InitializingBean {
@Autowired
private NotificationControllerV2 configController;
@Override
public void afterPropertiesSet() throws Exception {
// 定時任務從資料庫掃描有沒有新的配置釋出
new Thread(() -> {
for (;;) {
String result = NotificationControllerV2.queue.poll();
if (result != null) {
ReleaseMessage message = new ReleaseMessage();
message.setMessage(result);
configController.handleMessage(message);
}
}
}).start();
;
}
}
迴圈讀取 NotificationControllerV2 中的佇列,如果有訊息的話就構造一個 Release-Message 的物件,然後呼叫 NotificationControllerV2 中的 handleMessage() 方法進行訊息的處理。
ReleaseMessage 就一個欄位,模擬訊息內容,具體程式碼如下所示。
public class ReleaseMessage {
private String message;
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
接下來,我們來看 handleMessage 做了哪些工作。
NotificationControllerV2 實現了 ReleaseMessageListener 介面,ReleaseMessageListener 中定義了 handleMessage() 方法,具體程式碼如下所示。
public interface ReleaseMessageListener {
void handleMessage(ReleaseMessage message);
}
handleMessage 就是當配置發生變化的時候,傳送通知的訊息監聽器。訊息監聽器在得到配置釋出的資訊後,會通知對應的客戶端,具體程式碼如下所示。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
.synchronizedSetMultimap(HashMultimap.create());
@Override
public void handleMessage(ReleaseMessage message) {
System.err.println("handleMessage:" + message);
List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));
for (DeferredResultWrapper deferredResultWrapper : results) {
List<ApolloConfigNotification> list = new ArrayList<>();
list.add(new ApolloConfigNotification("application", 1));
deferredResultWrapper.setResult(list);
}
}
}
Apollo 的實時推送是基於 Spring DeferredResult 實現的,在 handleMessage() 方法中可以看到是通過 deferredResults 獲取 DeferredResult,deferredResults 就是第一行的 Multimap,Key 其實就是訊息內容,Value 就是 DeferredResult 的業務包裝類 DeferredResultWrapper,我們來看下 DeferredResultWrapper 的程式碼,程式碼如下所示。
public class DeferredResultWrapper {
private static final long TIMEOUT = 60 * 1000;// 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(
HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {
result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}
public void onTimeout(Runnable timeoutCallback) {
result.onTimeout(timeoutCallback);
}
public void onCompletion(Runnable completionCallback) {
result.onCompletion(completionCallback);
}
public void setResult(ApolloConfigNotification notification) {
setResult(Lists.newArrayList(notification));
}
public void setResult(List<ApolloConfigNotification> notifications) {
result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));
}
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {
return result;
}
}
通過 setResult() 方法設定返回結果給客戶端,以上就是當配置發生變化,然後通過訊息監聽器通知客戶端的原理,那麼客戶端是在什麼時候接入的呢?具體程式碼如下。
@RestController
publicclassNotificationControllerV2implementsReleaseMessageListener{
// 模擬配置更新, 向其中插入資料表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
.synchronizedSetMultimap(HashMultimap.create());
@GetMapping("/getConfig")
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {
DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();
List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();
if (!CollectionUtils.isEmpty(newNotifications)) {
deferredResultWrapper.setResult(newNotifications);
} else {
deferredResultWrapper.onTimeout(() -> {
System.err.println("onTimeout");
});
deferredResultWrapper.onCompletion(() -> {
System.err.println("onCompletion");
});
deferredResults.put("xxxx", deferredResultWrapper);
}
return deferredResultWrapper.getResult();
}
private List<ApolloConfigNotification> getApolloConfigNotifications() {
List<ApolloConfigNotification> list = new ArrayList<>();
String result = queue.poll();
if (result != null) {
list.add(new ApolloConfigNotification("application", 1));
}
return list;
}
}
NotificationControllerV2 中提供了一個 /getConfig 的介面,客戶端在啟動的時候會呼叫這個介面,這個時候會執行 getApolloConfigNotifications() 方法去獲取有沒有配置的變更資訊,如果有的話證明配置修改過,直接就通過 deferredResultWrapper.setResult(newNotifications) 返回結果給客戶端,客戶端收到結果後重新拉取配置的資訊覆蓋本地的配置。
如果 getApolloConfigNotifications() 方法沒有返回配置修改的資訊,則證明配置沒有發生修改,那就將 DeferredResultWrapper 物件新增到 deferredResults 中,等待後續配置發生變化時訊息監聽器進行通知。
同時這個請求就會掛起,不會立即返回,掛起是通過 DeferredResultWrapper 中的下面這部分程式碼實現的,具體程式碼如下所示。
privatestaticfinallongTIMEOUT=60*1000;//60seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST
=newResponseEntity<>(HttpStatus.NOT_MODIFIED);
privateDeferredResult<ResponseEntity<List<ApolloConfigNotification>>>result;
public DeferredResultWrapper() {
result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}
在建立 DeferredResult 物件的時候指定了超時的時間和超時後返回的響應碼,如果 60s 內沒有訊息監聽器進行通知,那麼這個請求就會超時,超時後客戶端收到的響應碼就是 304。
整個 Config Service 的流程就走完了,接下來我們來看一下客戶端是怎麼實現的,我們簡單地寫一個測試類模擬客戶端註冊,具體程式碼如下所示。
public class ClientTest {
public static void main(String[] args) {
reg();
}
private static void reg() {
System.err.println("註冊");
String result = request("http://localhost:8081/getConfig");
if (result != null) {
// 配置有更新, 重新拉取配置
// ......
}
// 重新註冊
reg();
}
private static String request(String url) {
HttpURLConnection connection = null;
BufferedReader reader = null;
try {
URL getUrl = new URL(url);
connection = (HttpURLConnection) getUrl.openConnection();
connection.setReadTimeout(90000);
connection.setConnectTimeout(3000);
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept-Charset", "utf-8");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Charset", "UTF-8");
System.out.println(connection.getResponseCode());
if (200 == connection.getResponseCode()) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
StringBuilder result = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
result.append(line);
}
System.out.println("結果 " + result);
return result.toString();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
}
return null;
}
}
首先啟動 /getConfig 介面所在的服務,然後啟動客戶端,然後客戶端就會發起註冊請求,如果有修改直接獲取到結果,則進行配置的更新操作。如果無修改,請求會掛起,這裡客戶端設定的讀取超時時間是 90s,大於服務端的 60s 超時時間。
每次收到結果後,無論是有修改還是無修改,都必須重新進行註冊,通過這樣的方式就可以達到配置實時推送的效果。
我們可以呼叫之前寫的 /addMsg 介面來模擬配置發生變化,呼叫之後客戶端就能馬上得到返回結果。
Apollo客戶端設計
上圖簡要描述了Apollo客戶端的實現原理:
-
客戶端和服務端保持了一個長連線,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
-
客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
-
這是一個fallback機制,為了防止推送機制失效導致配置不更新
-
客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
-
定時頻率預設為每5分鐘拉取一次,客戶端也可以通過在執行時指定System Property: apollo.refreshInterval來覆蓋,單位為分鐘。
-
-
客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會儲存在記憶體中
-
客戶端會把從服務端獲取到的配置在本地檔案系統快取一份(在遇到服務不可用,或網路不通的時候,依然能從本地恢復配置)
-
應用程式可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知
Apollo客戶端用法
Apollo支援API方式和Spring整合方式,該怎麼選擇用哪一種方式?
-
API方式靈活,功能完備,配置值實時更新(熱釋出),支援所有Java環境。
-
Spring方式接入簡單,結合Spring有N種酷炫的玩法,如
-
程式碼中直接使用,如:
@Value("${someKeyFromApollo:someDefaultValue}")
-
配置檔案中使用替換placeholder,如:
spring.datasource.url: ${someKeyFromApollo:someDefaultValue}
-
直接託管spring的配置,如在apollo中直接配置
spring.datasource.url=jdbc:mysql://localhost:3306/somedb?characterEncoding=utf8
-
Placeholder方式:
-
Spring boot的@ConfigurationProperties方式
-
從v0.10.0開始的版本支援placeholder在執行時自動更新,具體參見PR #972。(v0.10.0之前的版本在配置變化後不會重新注入,需要重啟才會更新,如果需要配置值實時更新,可以參考後續3.2.2 Spring Placeholder的使用的說明)
-
-
Spring方式也可以結合API方式使用,如注入Apollo的Config物件,就可以照常通過API方式獲取配置了:
@ApolloConfig private Config config; //inject config for namespace application
-
更多有意思的實際使用場景和示例程式碼,請參考apollo-use-cases
1、API使用方式
API方式是最簡單、高效使用Apollo配置的方式,不依賴Spring框架即可使用。
獲取預設namespace的配置(application)
Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null
String someKey = "someKeyFromDefaultNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);
通過上述的config.getProperty可以獲取到someKey對應的實時最新的配置值。
另外,配置值從記憶體中獲取,所以不需要應用自己做快取。
監聽配置變化事件
監聽配置變化事件只在應用真的關心配置變化,需要在配置變化時得到通知時使用,比如:資料庫連線串變化後需要重建連線等。
如果只是希望每次都取到最新的配置的話,只需要按照上面的例子,呼叫config.getProperty即可。
Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never null
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
System.out.println("Changes for namespace " + changeEvent.getNamespace());
for (String key : changeEvent.changedKeys()) {
ConfigChange change = changeEvent.getChange(key);
System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));
}
}
});
獲取公共Namespace的配置
String somePublicNamespace = "CAT";
Config config = ConfigService.getConfig(somePublicNamespace); //config instance is singleton for each namespace and is never null
String someKey = "someKeyFromPublicNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);
獲取非properties格式namespace的配置
1.yaml/yml格式的namespace
apollo-client 1.3.0版本開始對yaml/yml做了更好的支援,使用起來和properties格式一致。
Config config = ConfigService.getConfig("application.yml");
String someKey = "someKeyFromYmlNamespace";
String someDefaultValue = "someDefaultValueForTheKey";
String value = config.getProperty(someKey, someDefaultValue);
2.非yaml/yml格式的namespace
獲取時需要使用ConfigService.getConfigFile
介面並指定Format,如ConfigFileFormat.XML
。
String someNamespace = "test";
ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML);
String content = configFile.getContent();
Spring整合方式
配置
Apollo也支援和Spring整合(Spring 3.1.1+),只需要做一些簡單的配置就可以了。
Apollo目前既支援比較傳統的基於XML
的配置,也支援目前比較流行的基於Java(推薦)
的配置。
如果是Spring Boot環境,建議參照3.2.1.3 Spring Boot整合方式(推薦)配置。
需要注意的是,如果之前有使用org.springframework.beans.factory.config.PropertyPlaceholderConfigurer
的,請替換成org.springframework.context.support.PropertySourcesPlaceholderConfigurer
。Spring 3.1以後就不建議使用PropertyPlaceholderConfigurer了,要改用PropertySourcesPlaceholderConfigurer。
如果之前有使用<context:property-placeholder>
,請注意xml中引入的spring-context.xsd
版本需要是3.1以上(一般只要沒有指定版本會自動升級的),建議使用不帶版本號的形式引入,如:http://www.springframework.org/schema/context/spring-context.xsd
注1:yaml/yml格式的namespace從1.3.0版本開始支援和Spring整合,注入時需要填寫帶字尾的完整名字,比如application.yml
注2:非properties、非yaml/yml格式(如xml,json等)的namespace暫不支援和Spring整合。
基於XML的配置
注:需要把apollo相關的xml namespace加到配置檔案頭上,不然會報xml語法錯誤。
1.注入預設namespace的配置到Spring中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:apollo="http://www.ctrip.com/schema/apollo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
<!-- 這個是最簡單的配置形式,一般應用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環境中 -->
<apollo:config/>
<bean class="com.ctrip.framework.apollo.spring.TestXmlBean">
<property name="timeout" value="${timeout:100}"/>
<property name="batch" value="${batch:200}"/>
</bean>
</beans>
2.注入多個namespace的配置到Spring中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:apollo="http://www.ctrip.com/schema/apollo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
<!-- 這個是最簡單的配置形式,一般應用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環境中 -->
<apollo:config/>
<!-- 這個是稍微複雜一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中 -->
<apollo:config namespaces="FX.apollo,application.yml"/>
<bean class="com.ctrip.framework.apollo.spring.TestXmlBean">
<property name="timeout" value="${timeout:100}"/>
<property name="batch" value="${batch:200}"/>
</bean>
</beans>
3.注入多個namespace,並且指定順序
Spring的配置是有順序的,如果多個property source都有同一個key,那麼最終是順序在前的配置生效。
apollo:config如果不指定order,那麼預設是最低優先順序。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:apollo="http://www.ctrip.com/schema/apollo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
<apollo:config order="2"/>
<!-- 這個是最複雜的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中,並且順序在application前面 -->
<apollo:config namespaces="FX.apollo,application.yml" order="1"/>
<bean class="com.ctrip.framework.apollo.spring.TestXmlBean">
<property name="timeout" value="${timeout:100}"/>
<property name="batch" value="${batch:200}"/>
</bean>
</beans>
基於Java的配置(推薦)
相對於基於XML的配置,基於Java的配置是目前比較流行的方式。
注意@EnableApolloConfig
要和@Configuration
一起使用,不然不會生效。
1.注入預設namespace的配置到Spring中
//這個是最簡單的配置形式,一般應用用這種形式就可以了,用來指示Apollo注入application namespace的配置到Spring環境中
@Configuration
@EnableApolloConfig
public class AppConfig {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
2.注入多個namespace的配置到Spring中
@Configuration
@EnableApolloConfig
public class SomeAppConfig {http://www.jintianxuesha.com/
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
//這個是稍微複雜一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中
@Configuration
@EnableApolloConfig({"FX.apollo", "application.yml"})
public class AnotherAppConfig {}
3.注入多個namespace,並且指定順序
//這個是最複雜的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring環境中,並且順序在application前面
@Configuration
@EnableApolloConfig(order = 2)
public class SomeAppConfig {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
@Configuration
@EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1)
public class AnotherAppConfig {}
Spring Boot整合方式(推薦)
Spring Boot除了支援上述兩種整合方式以外,還支援通過application.properties/bootstrap.properties來配置,該方式能使配置在更早的階段注入,比如使用@ConditionalOnProperty
的場景或者是有一些spring-boot-starter在啟動階段就需要讀取配置做一些事情(如dubbo-spring-boot-project),所以對於Spring Boot環境建議通過以下方式來接入Apollo(需要0.10.0及以上版本)。
使用方式很簡單,只需要在application.properties/bootstrap.properties中按照如下樣例配置即可。
注入預設application
namespace的配置示例
#willinject'application'namespaceinbootstrapphase
apollo.bootstrap.enabled=true
注入非預設application
namespace或多個namespace的配置示例
apollo.bootstrap.enabled=true
#willinject'application','FX.apollo'and'application.yml'namespacesinbootstrapphase
apollo.bootstrap.namespaces=application,FX.apollo,application.yml
將Apollo配置載入提到初始化日誌系統之前(1.2.0+)
從1.2.0版本開始,如果希望把日誌相關的配置(如logging.level.root=info
或logback-spring.xml
中的引數)也放在Apollo管理,那麼可以額外配置apollo.bootstrap.eagerLoad.enabled=true
來使Apollo的載入順序放到日誌系統載入之前,不過這會導致Apollo的啟動過程無法通過日誌的方式輸出(因為執行Apollo載入的時候,日誌系統壓根沒有準備好呢!所以在Apollo程式碼中使用Slf4j的日誌輸出便沒有任何內容),更多資訊可以參考PR 1614。參考配置示例如下:
#willinject'application'namespaceinbootstrapphase
apollo.bootstrap.enabled=true
#putapolloinitializationbeforeloggingsysteminitialization
apollo.bootstrap.eagerLoad.enabled=true
Spring Placeholder的使用
Spring應用通常會使用Placeholder來注入配置,使用的格式形如{someKey:someDefaultValue},如someKey:someDefaultValue,如{timeout:100}。冒號前面的是key,冒號後面的是預設值。
建議在實際使用時儘量給出預設值,以免由於key沒有定義導致執行時錯誤。
從v0.10.0開始的版本支援placeholder在執行時自動更新,具體參見PR #972。
如果需要關閉placeholder在執行時自動更新功能,可以通過以下兩種方式關閉:
1. 通過設定System Propertyapollo.autoUpdateInjectedSpringProperties
,如啟動時傳入-Dapollo.autoUpdateInjectedSpringProperties=false
2.通過設定META-INF/app.properties中的apollo.autoUpdateInjectedSpringProperties
屬性,如