Apollo配置中心介紹
2016年 07月 09日
1、What is Apollo
1.1 背景
隨着程序功能的日益複雜,程序的配置日益增多:各種功能的開關、參數的配置、服務器的地址……
對程序配置的期望值也越來越高:配置修改後實時生效,灰度發佈,分環境、分集羣管理配置,完善的權限、審覈機制……
在這樣的大環境下,傳統的通過配置文件、數據庫等方式已經越來越無法滿足開發人員對配置管理的需求。
Apollo配置中心應運而生!
1.2 Apollo簡介
Apollo(阿波羅)是攜程框架部門研發的開源配置管理中心,能夠集中化管理應用不同環境、不同集羣的配置,配置修改後能夠實時推送到應用端,並且具備規範的權限、流程治理等特性。
Apollo支持4個維度管理Key-Value格式的配置:
- application (應用)
- environment (環境)
- cluster (集羣)
- namespace (命名空間)
同時,Apollo基於開源模式開發,開源地址:https://github.com/ctripcorp/apollo
1.3 配置基本概念
既然Apollo定位於配置中心,那麼在這裏有必要先簡單介紹一下什麼是配置。
按照我們的理解,配置有以下幾個屬性:
- 配置是獨立於程序的只讀變量
- 配置首先是獨立於程序的,同一份程序在不同的配置下會有不同的行爲。
- 其次,配置對於程序是隻讀的,程序通過讀取配置來改變自己的行爲,但是程序不應該去改變配置。
- 常見的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、Feature Switch、Server Urls等。
- 配置伴隨應用的整個生命週期
- 配置貫穿於應用的整個生命週期,應用在啓動時通過讀取配置來初始化,在運行時根據配置調整行爲。
- 配置可以有多種加載方式
- 配置也有很多種加載方式,常見的有程序內部hard code,配置文件,環境變量,啓動參數,基於數據庫等
- 配置需要治理
- 權限控制
- 由於配置能改變程序的行爲,不正確的配置甚至能引起災難,所以對配置的修改必須有比較完善的權限控制
- 不同環境、集羣配置管理
- 同一份程序在不同的環境(開發,測試,生產)、不同的集羣(如不同的數據中心)經常需要有不同的配置,所以需要有完善的環境、集羣配置管理
- 框架類組件配置管理
- 還有一類比較特殊的配置 - 框架類組件配置,比如CAT客戶端的配置。
- 雖然這類框架類組件是由其他團隊開發、維護,但是運行時是在業務實際應用內的,所以本質上可以認爲框架類組件也是應用的一部分。
- 這類組件對應的配置也需要有比較完善的管理方式。
- 權限控制
2、Why Apollo
正是基於配置的特殊性,所以Apollo從設計之初就立志於成爲一個有治理能力的配置管理平臺,目前提供了以下的特性:
- 統一管理不同環境、不同集羣的配置
- Apollo提供了一個統一界面集中式管理不同環境(environment)、不同集羣(cluster)、不同命名空間(namespace)的配置。
- 同一份代碼部署在不同的集羣,可以有不同的配置,比如zk的地址等
- 通過命名空間(namespace)可以很方便的支持多個不同應用共享同一份配置,同時還允許應用對共享的配置進行覆蓋
- 配置修改實時生效(熱發佈)
- 用戶在Apollo修改完配置併發布後,客戶端能實時(1秒)接收到最新的配置,並通知到應用程序
- 版本發佈管理
- 所有的配置發佈都有版本概念,從而可以方便地支持配置的回滾
- 灰度發佈
- 支持配置的灰度發佈,比如點了發佈後,只對部分應用實例生效,等觀察一段時間沒問題後再推給所有應用實例
- 權限管理、發佈審覈、操作審計
- 應用和配置的管理都有完善的權限管理機制,對配置的管理還分爲了編輯和發佈兩個環節,從而減少人爲的錯誤。
- 所有的操作都有審計日誌,可以方便的追蹤問題
- 客戶端配置信息監控
- 可以在界面上方便地看到配置在被哪些實例使用
- 提供Java和.Net原生客戶端
- 提供了Java和.Net的原生客戶端,方便應用集成
- 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便應用使用(需要Spring 3.1.1+)
- 同時提供了Http接口,非Java和.Net應用也可以方便的使用
- 提供開放平臺API
- Apollo自身提供了比較完善的統一配置管理界面,支持多環境、多數據中心配置管理、權限、流程治理等特性。
- 不過Apollo出於通用性考慮,對配置的修改不會做過多限制,只要符合基本的格式就能夠保存。
- 在我們的調研中發現,對於有些使用方,它們的配置可能會有比較複雜的格式,而且對輸入的值也需要進行校驗後方可保存,如檢查數據庫、用戶名和密碼是否匹配。
- 對於這類應用,Apollo支持應用方通過開放接口在Apollo進行配置的修改和發佈,並且具備完善的授權和權限控制
- 部署簡單
- 配置中心作爲基礎服務,可用性要求非常高,這就要求Apollo對外部依賴儘可能地少
- 目前唯一的外部依賴是MySQL,所以部署非常簡單,只要安裝好Java和MySQL就可以讓Apollo跑起來
- Apollo還提供了打包腳本,一鍵就可以生成所有需要的安裝包,並且支持自定義運行時參數
3、Apollo at a glance
3.1 基礎模型
如下即是Apollo的基礎模型:
- 用戶在配置中心對配置進行修改併發布
- 配置中心通知Apollo客戶端有配置更新
- Apollo客戶端從配置中心拉取最新的配置、更新本地配置並通知到應用
3.2 界面概覽
上圖是Apollo配置中心中一個項目的配置首頁
- 在頁面左上方的環境列表模塊展示了所有的環境和集羣,用戶可以隨時切換。
- 頁面中央展示了兩個namespace(application和FX.apollo)的配置信息,默認按照表格模式展示、編輯。用戶也可以切換到文本模式,以文件形式查看、編輯。
- 頁面上可以方便地進行發佈、回滾、灰度、授權、查看更改歷史和發佈歷史等操作
3.3 添加/修改配置項
用戶可以通過配置中心界面方便的添加/修改配置項:
輸入配置信息:
3.4 發佈配置
通過配置中心發佈配置:
填寫發佈信息:
3.5 客戶端獲取配置(Java API樣例)
配置發佈後,就能在客戶端獲取到了,以Java API方式爲例,獲取配置的示例代碼如下。更多客戶端使用說明請參見Java客戶端使用指南。
Config config = ConfigService.getAppConfig();
Integer defaultRequestTimeout = 200;
Integer requestTimeout =
config.getIntProperty("request.timeout",defaultRequestTimeout);
3.6 客戶端監聽配置變化(Java API樣例)
通過上述獲取配置代碼,應用就能實時獲取到最新的配置了。
不過在某些場景下,應用還需要在配置變化時獲得通知,比如數據庫連接的切換等,所以Apollo還提供了監聽配置變化的功能,Java示例如下:
Config config = ConfigService.getAppConfig();
config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
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()));
}
}
});
3.7 Spring集成樣例
Apollo和Spring也可以很方便地集成,只需要標註@EnableApolloConfig
後就可以通過@Value
獲取配置信息:
@Configuration
@EnableApolloConfig
public class AppConfig {}
@Component
public class SomeBean {
@Value("${request.timeout:200}")
private int timeout;
@ApolloConfigChangeListener
private void someChangeHandler(ConfigChangeEvent changeEvent) {
if (changeEvent.isChanged("request.timeout")) {
refreshTimeout();
}
}
}
4、Apollo in depth
通過上面的介紹,相信大家已經對Apollo有了一個初步的瞭解,並且相信已經覆蓋到了大部分的使用場景。
接下來會主要介紹Apollo的cluster管理(集羣)、namespace管理(命名空間)和對應的配置獲取規則。
4.1 Core Concepts
在介紹高級特性前,我們有必要先來了解一下Apollo中的幾個核心概念:
- application (應用)
- 這個很好理解,就是實際使用配置的應用,Apollo客戶端在運行時需要知道當前應用是誰,從而可以去獲取對應的配置
- 每個應用都需要有唯一的身份標識 - appId,我們認爲應用身份是跟着代碼走的,所以需要在代碼中配置,具體信息請參見Java客戶端使用指南。
- environment (環境)
- 配置對應的環境,Apollo客戶端在運行時需要知道當前應用處於哪個環境,從而可以去獲取應用的配置
- 我們認爲環境和代碼無關,同一份代碼部署在不同的環境就應該能夠獲取到不同環境的配置
- 所以環境默認是通過讀取機器上的配置(server.properties中的env屬性)指定的,不過爲了開發方便,我們也支持運行時通過System Property等指定,具體信息請參見文章末尾。
- cluster (集羣)
- 一個應用下不同實例的分組,比如典型的可以按照數據中心分,把上海機房的應用實例分爲一個集羣,把北京機房的應用實例分爲另一個集羣。
- 對不同的cluster,同一個配置可以有不一樣的值,如zookeeper地址。
- 集羣默認是通過讀取機器上的配置(server.properties中的idc屬性)指定的,不過也支持運行時通過System Property指定,具體信息請參見Java客戶端使用指南。
- namespace (命名空間)
- 一個應用下不同配置的分組,可以簡單地把namespace類比爲文件,不同類型的配置存放在不同的文件中,如數據庫配置文件,rpc配置文件,應用自身的配置文件等
- 應用可以直接讀取到公共組件的配置namespace,如DAL,RPC等
- 應用也可以通過繼承公共組件的配置namespace來對公共組件的配置做調整,如DAL的初始數據庫連接數
4.2 自定義Cluster
【本節內容僅對應用需要對不同集羣應用不同配置才需要,如沒有相關需求,可以跳過本節】
比如我們有應用在A數據中心和B數據中心都有部署,那麼如果希望兩個數據中心的配置不一樣的話,我們可以通過新建cluster來解決。
4.2.1 新建Cluster
新建Cluster只有項目的管理員纔有權限,管理員可以在頁面左側看到“添加集羣”按鈕。
點擊後就進入到集羣添加頁面,一般情況下可以按照數據中心來劃分集羣,如SHAJQ、SHAOY等。
不過也支持自定義集羣,比如可以爲A機房的某一臺機器和B機房的某一臺機創建一個集羣,使用一套配置。
4.2.2 在Cluster中添加配置併發布
集羣添加成功後,就可以爲該集羣添加配置了,首選需要按照下圖所示切換到SHAJQ集羣,之後配置添加流程和3.2添加/修改配置項一樣,這裏就不再贅述了。
4.2.3 指定應用實例所屬的Cluster
Apollo會默認使用應用實例所在的數據中心作爲cluster,所以如果兩者一致的話,不需要額外配置。
如果cluster和數據中心不一致的話,那麼就需要通過System Property方式來指定運行時cluster:
- -Dapollo.cluster=SomeCluster
- 這裏注意
apollo.cluster
爲全小寫
4.3 自定義Namespace
【本節僅對公共組件配置或需要多個應用共享配置才需要,如沒有相關需求,可以跳過本節】
如果應用有公共組件(如hermes-producer,cat-client等)供其它應用使用,就需要通過自定義namespace來實現公共組件的配置。
4.3.1 新建Namespace
以hermes-producer爲例,需要先新建一個namespace,新建namespace只有項目的管理員纔有權限,管理員可以在頁面左側看到“添加Namespace”按鈕。
點擊後就進入namespace添加頁面,Apollo會把應用所屬的部門作爲namespace的前綴,如FX。
4.3.2 關聯到環境和集羣
Namespace創建完,需要選擇在哪些環境和集羣下使用
4.3.3 在Namespace中添加配置項
接下來在這個新建的namespace下添加配置項
添加完成後就能在FX.Hermes.Producer的namespace中看到配置。
4.3.4 發佈namespace的配置
4.3.5 客戶端獲取Namespace配置
對自定義namespace的配置獲取,稍有不同,需要程序傳入namespace的名字。更多客戶端使用說明請參見Java客戶端使用指南。
Config config = ConfigService.getConfig("FX.Hermes.Producer");
Integer defaultSenderBatchSize = 200;
Integer senderBatchSize = config.getIntProperty("sender.batchsize", defaultSenderBatchSize);
4.3.6 客戶端監聽Namespace配置變化
Config config = ConfigService.getConfig("FX.Hermes.Producer");
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()));
}
}
});
4.3.7 Spring集成樣例
@Configuration
@EnableApolloConfig("FX.Hermes.Producer")
public class AppConfig {}
@Component
public class SomeBean {
@Value("${request.timeout:200}")
private int timeout;
@ApolloConfigChangeListener("FX.Hermes.Producer")
private void someChangeHandler(ConfigChangeEvent changeEvent) {
if (changeEvent.isChanged("request.timeout")) {
refreshTimeout();
}
}
}
4.4 配置獲取規則
【本節僅當應用自定義了集羣或namespace才需要,如無相關需求,可以跳過本節】
在有了cluster概念後,配置的規則就顯得重要了。
比如應用部署在A機房,但是並沒有在Apollo新建cluster,這個時候Apollo的行爲是怎樣的?
或者在運行時指定了cluster=SomeCluster,但是並沒有在Apollo新建cluster,這個時候Apollo的行爲是怎樣的?
接下來就來介紹一下配置獲取的規則。
4.4.1 應用自身配置的獲取規則
當應用使用下面的語句獲取配置時,我們稱之爲獲取應用自身的配置,也就是應用自身的application namespace的配置。
Config config = ConfigService.getAppConfig();
對這種情況的配置獲取規則,簡而言之如下:
- 首先查找運行時cluster的配置(通過apollo.cluster指定)
- 如果沒有找到,則查找數據中心cluster的配置
- 如果還是沒有找到,則返回默認cluster的配置
圖示如下:
所以如果應用部署在A數據中心,但是用戶沒有在Apollo創建cluster,那麼獲取的配置就是默認cluster(default)的。
如果應用部署在A數據中心,同時在運行時指定了SomeCluster,但是沒有在Apollo創建cluster,那麼獲取的配置就是A數據中心cluster的配置,如果A數據中心cluster沒有配置的話,那麼獲取的配置就是默認cluster(default)的。
4.4.2 公共組件配置的獲取規則
以FX.Hermes.Producer爲例,hermes producer是hermes發佈的公共組件。當使用下面的語句獲取配置時,我們稱之爲獲取公共組件的配置。
Config config = ConfigService.getConfig("FX.Hermes.Producer");
對這種情況的配置獲取規則,簡而言之如下:
- 首先獲取當前應用下的FX.Hermes.Producer namespace的配置
- 然後獲取hermes應用下FX.Hermes.Producer namespace的配置
- 上面兩部分配置的並集就是最終使用的配置,如有key一樣的部分,以當前應用優先
圖示如下:
通過這種方式,就實現了對框架類組件的配置管理,框架組件提供方提供配置的默認值,應用如果有特殊需求,可以自行覆蓋。
4.5 總體設計
上圖簡要描述了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進程中
4.5.1 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
- 最後一點是開源,由於代碼是開源的,所以非常便於我們瞭解它的實現原理和排查問題。
4.6 客戶端設計
上圖簡要描述了Apollo客戶端的實現原理:
- 客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。
- 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,爲了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率默認爲每5分鐘拉取一次,客戶端也可以通過在運行時指定System Property:
apollo.refreshInterval
來覆蓋,單位爲分鐘。
- 客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會保存在內存中
- 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份
- 在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置
- 應用程序可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知
4.6.1 配置更新推送實現
前面提到了Apollo客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。
長連接實際上我們是通過Http Long Polling實現的,具體而言:
- 客戶端發起一個Http請求到服務端
- 服務端會保持住這個連接30秒
- 如果在30秒內有客戶端關心的配置變化,被保持住的客戶端請求會立即返回,並告知客戶端有配置變化的namespace信息,客戶端會據此拉取對應namespace的最新配置
- 如果在30秒內沒有客戶端關心的配置變化,那麼會返回Http狀態碼304給客戶端
- 客戶端在服務端請求返回後會自動重連
考慮到會有數萬客戶端向服務端發起長連,在服務端我們使用了async servlet(Spring DeferredResult)來服務Http Long Polling請求。
4.7 可用性考慮
配置中心作爲基礎服務,可用性要求非常高,下面的表格描述了不同場景下Apollo的可用性:
場景 | 影響 | 降級 | 原因 |
---|---|---|---|
某臺config service下線 | 無影響 | Config service無狀態,客戶端重連其它config service | |
所有config service下線 | 客戶端無法讀取最新配置,Portal無影響 | 客戶端重啓時,可以讀取本地緩存配置文件 | |
某臺admin service下線 | 無影響 | Admin service無狀態,Portal重連其它admin service | |
所有admin service下線 | 客戶端無影響,portal無法更新配置 | ||
某臺portal下線 | 無影響 | Portal域名通過slb綁定多臺服務器,重試後指向可用的服務器 | |
全部portal下線 | 客戶端無影響,portal無法更新配置 | ||
某個數據中心下線 | 無影響 | 多數據中心部署,數據完全同步,Meta Server/Portal域名通過slb自動切換到其它存活的數據中心 |
5、Contribute to Apollo
Apollo從開發之初就是以開源模式開發的,所以也非常歡迎有興趣、有餘力的朋友一起加入進來。
服務端開發使用的是Java,基於Spring Cloud和Spring Boot框架。客戶端目前提供了Java和.Net兩種實現。
Github地址:https://github.com/ctripcorp/apollo
歡迎大家發起Pull Request!
本作品由nobodyiam創作,採用知識共享署名-相同方式共享 4.0 國際許可協議進行許可。
Copyright © nobodyiam.com, hosted by Coding Pages, powered by Jekyll.
末尾:
Java客戶端使用指南
一、準備工作
1.1 環境要求
- Java: 1.7+
- Guava: 15.0+
- Apollo客戶端默認會引用Guava 19,如果你的項目引用了其它版本,請確保版本號大於等於15.0
注:對於Apollo客戶端,如果有需要的話,可以做少量代碼修改來降級到Java 1.6,詳細信息可以參考Issue 483
1.2 必選設置
Apollo客戶端依賴於AppId
,Environment
等環境信息來工作,所以請確保閱讀下面的說明並且做正確的配置:
1.2.1 AppId
AppId是應用的身份信息,是從服務端獲取配置的一個重要信息。
請確保classpath:/META-INF/app.properties文件存在,並且其中內容形如:
app.id=YOUR-APP-ID
文件位置參考如下:
v0.7.0版本後,Apollo也支持通過System Property傳入app.id信息,如
-Dapp.id=YOUR-APP-ID
注:app.id是用來標識應用身份的唯一id,格式爲string。
1.2.2 Environment
Apollo支持應用在不同的環境有不同的配置,所以Environment是另一個從服務器獲取配置的重要信息。
Environment可以通過以下3種方式的任意一個配置:
- 通過Java System Property
- 可以通過Java的System Property
env
來指定環境 - 在Java程序啓動腳本中,可以指定
-Denv=YOUR-ENVIRONMENT
- 如果是運行jar文件,需要注意格式是
java -Denv=YOUR-ENVIRONMENT -jar xxx.jar
- 如果是運行jar文件,需要注意格式是
- 注意key爲全小寫
- 可以通過Java的System Property
- 通過操作系統的System Environment
- 還可以通過操作系統的System Environment
ENV
來指定 - 注意key爲全大寫
- 還可以通過操作系統的System Environment
- 通過配置文件
- 最後一個推薦的方式是通過配置文件來指定
env=YOUR-ENVIRONMENT
- 對於Mac/Linux,文件位置爲
/opt/settings/server.properties
- 對於Windows,文件位置爲
C:\opt\settings\server.properties
- 最後一個推薦的方式是通過配置文件來指定
文件內容形如:
env=DEV
目前,env
支持以下幾個值(大小寫不敏感):
- DEV
- Development environment
- FAT
- Feature Acceptance Test environment
- UAT
- User Acceptance Test environment
- PRO
- Production environment
更多環境定義,可以參考Env.java
注:運行時會通過env
信息匹配meta server信息,比如env
是DEV
的話,就會讀取apollo-core.jar中apollo-env.properties中的dev_meta
信息。如果希望在運行時覆蓋meta server信息,有兩個方法:
- 通過-D{env}_meta=xxx來覆蓋,如-Ddev_meta=http://someIp:8080
- 準備一份單獨的apollo-env.properties,放在程序運行的
classpath
中,或者放在程序運行目錄下,具體可以參考apollo-env.properties的加載邏輯 - ResourceUtils和apollo-env.properties原始文件,需要注意的是在apollo-env.properties中需要用形如{env}.meta=xxx來覆蓋,如dev.meta=http://someIp:8080。
1.2.3 本地緩存路徑
Apollo客戶端會把從服務端獲取到的配置在本地文件系統緩存一份,用於在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置,不影響應用正常運行。
本地緩存路徑位於以下路徑,所以請確保/opt/data
或C:\opt\data\
目錄存在,且應用有讀寫權限。
- Mac/Linux: /opt/data/{appId}/config-cache
- Windows: C:\opt\data\{appId}\config-cache
本地配置文件會以下面的文件名格式放置於本地緩存路徑下:
{appId}+{cluster}+{namespace}.properties
- appId就是應用自己的appId,如100004458
- cluster就是應用使用的集羣,一般在本地模式下沒有做過配置的話,就是default
- namespace就是應用使用的配置namespace,一般是application
文件內容以properties格式存儲,比如如果有兩個key,一個是request.timeout,另一個是batch,那麼文件內容就是如下格式:
request.timeout=2000
batch=2000
注:本地緩存路徑也可用於容災目錄,如果應用在所有config service都掛掉的情況下需要擴容,那麼也可以先把配置從已有機器上的緩存路徑複製到新機器上的相同緩存路徑
1.2.4 可選設置
Cluster(集羣)
Apollo支持配置按照集羣劃分,也就是說對於一個appId和一個環境,對不同的集羣可以有不同的配置。
如果需要使用這個功能,你可以通過以下方式來指定運行時的集羣:
- 通過Java System Property
- 我們可以通過Java的System Property設置
apollo.cluster
來指定運行時集羣(注意key爲全小寫) - 例如,可以在程序啓動時通過
-Dapollo.cluster=SomeCluster
來指定運行時的集羣爲SomeCluster
- 我們可以通過Java的System Property設置
- 通過配置文件
- 首先確保
/opt/settings/server.properties
(Mac/Linux)或C:\opt\settings\server.properties
(Windows)在目標機器上存在 - 在這個文件中,可以設置數據中心集羣,如
idc=xxx
- 注意key爲全小寫
- 首先確保
Cluster Precedence(集羣順序)
- 如果
apollo.cluster
和idc
同時指定:- 我們會首先嚐試從
apollo.cluster
指定的集羣加載配置 - 如果沒找到任何配置,會嘗試從
idc
指定的集羣加載配置 - 如果還是沒找到,會從默認的集羣(
default
)加載
- 我們會首先嚐試從
- 如果只指定了
apollo.cluster
:- 我們會首先嚐試從
apollo.cluster
指定的集羣加載配置 - 如果沒找到,會從默認的集羣(
default
)加載
- 我們會首先嚐試從
- 如果只指定了
idc
:- 我們會首先嚐試從
idc
指定的集羣加載配置 - 如果沒找到,會從默認的集羣(
default
)加載
- 我們會首先嚐試從
- 如果
apollo.cluster
和idc
都沒有指定:- 我們會從默認的集羣(
default
)加載配置
- 我們會從默認的集羣(
二、Maven Dependency
由於客戶端jar包中會包含meta server信息,無法上傳一個統一的jar包到中央倉庫,所以請按照分佈式部署指南的文檔說明打包並上傳到自己公司的Maven私服。應用在實際使用時只需要按照如下方式引入即可。
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>0.10.2</version>
</dependency>
三、客戶端用法
Apollo支持API方式和Spring整合方式,該怎麼選擇用哪一種方式?
- API方式靈活,功能完備,配置值實時更新(熱發佈),支持所有Java環境。
- Spring方式接入簡單,結合Spring有N種酷炫的玩法,如
- Placeholder方式:
- 代碼中使用,如:
@Value("${someKeyFromApollo:someDefaultValue}")
- application.properties中使用,如:
spring.datasource.url: ${someKeyFromApollo:someDefaultValue}
- 代碼中使用,如:
- Spring boot的@ConfigurationProperties方式
- 從v0.10.0開始的版本支持placeholder在運行時自動更新,具體參見PR #972。(v0.10.0之前的版本在配置變化後不會重新注入,需要重啓纔會更新,如果需要配置值實時更新,可以參考後續3.2.2 Spring Placeholder的使用的說明)
- Placeholder方式:
- Spring方式也可以結合API方式使用,如注入Apollo的Config對象,就可以照常通過API方式獲取配置了:
- @ApolloConfig
private Config config; //inject config for namespace application
- 更多有意思的實際使用場景和示例代碼,請參考apollo-use-cases
3.1 API使用方式
API方式是最簡單、高效使用Apollo配置的方式,不依賴Spring框架即可使用。
3.1.1 獲取默認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對應的實時最新的配置值。
另外,配置值從內存中獲取,所以不需要應用自己做緩存。
3.1.2 監聽配置變化事件
監聽配置變化事件只在應用真的關心配置變化,需要在配置變化時得到通知時使用,比如:數據庫連接串變化後需要重建連接等。
如果只是希望每次都取到最新的配置的話,只需要按照上面的例子,調用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()));
}
}
});
3.1.3 獲取公共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);
3.2 Spring整合方式
3.2.1 配置
Apollo也支持和Spring整合(Spring 3.1.1+),只需要做一些簡單的配置就可以了。
Apollo目前既支持比較傳統的基於
XML
的配置,也支持目前比較流行的基於
Java
(推薦)
的配置。
如果是Spring Boot環境,建議參照3.2.1.3-在spring-boot初始bootstrap階段注入配置配置。
需要注意的是,如果之前有使用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
3.2.1.1 基於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和FX.soa namespace的配置到Spring環境中 -->
<apollo:config namespaces="FX.apollo,FX.soa"/>
<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和FX.soa namespace的配置到Spring環境中,並且順序在application前面 -->
<apollo:config namespaces="FX.apollo,FX.soa" order="1"/>
<bean class="com.ctrip.framework.apollo.spring.TestXmlBean">
<property name="timeout" value="${timeout:100}"/>
<property name="batch" value="${batch:200}"/>
</bean>
</beans>
3.2.1.2 基於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 {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
//這個是稍微複雜一些的配置形式,指示Apollo注入FX.apollo和FX.soa namespace的配置到Spring環境中
@Configuration
@EnableApolloConfig({"FX.apollo", "FX.soa"})
public class AnotherAppConfig {}
3.注入多個namespace,並且指定順序
//這個是最複雜的配置形式,指示Apollo注入FX.apollo和FX.soa namespace的配置到Spring環境中,並且順序在application前面
@Configuration
@EnableApolloConfig(order = 2)
public class SomeAppConfig {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
@Configuration
@EnableApolloConfig(value = {"FX.apollo", "FX.soa"}, order = 1)
public class AnotherAppConfig {}
3.2.1.3 在Spring Boot初始bootstrap階段注入配置
使用上述兩種方式的配置形式後,Apollo會在Spring的postProcessBeanFactory階段注入配置到Spring的Environment中,早於bean的初始化階段,所以對於普通的bean注入配置場景已經能很好的滿足。
不過Spring Boot有一些場景需要配置在更早的階段注入,比如使用@ConditionalOnProperty
的場景或者是有一些spring-boot-starter在啓動階段就需要讀取配置做一些事情(如spring-boot-starter-dubbo),所以對於Spring Boot環境建議通過以下方式來接入Apollo(需要0.10.0及以上版本)。
使用方式很簡單,只需要在application.properties/bootstrap.properties中按照如下樣例配置即可。
- 在bootstrap階段注入默認
application
namespace的配置示例
# will inject 'application' namespace in bootstrap phase
apollo.bootstrap.enabled = true
- 在bootstrap階段注入非默認
application
namespace或多個namespace的配置示例
apollo.bootstrap.enabled = true
# will inject 'application' and 'FX.apollo' namespaces in bootstrap phase
apollo.bootstrap.namespaces = application,FX.apollo
3.2.2 Spring Placeholder的使用
Spring應用通常會使用Placeholder來注入配置,使用的格式形如${someKey:someDefaultValue},如${timeout:100}。冒號前面的是key,冒號後面的是默認值。
建議在實際使用時儘量給出默認值,以免由於key沒有定義導致運行時錯誤。
從v0.10.0開始的版本支持placeholder在運行時自動更新,具體參見PR #972。
如果需要關閉placeholder在運行時自動更新功能,可以通過以下兩種方式關閉:
- 通過設置System Property
apollo.autoUpdateInjectedSpringProperties
,如啓動時傳入-Dapollo.autoUpdateInjectedSpringProperties=false
- 通過設置META-INF/app.properties中的
apollo.autoUpdateInjectedSpringProperties
屬性,如
app.id=SampleApp
apollo.autoUpdateInjectedSpringProperties=false
對於v0.10.0之前的版本,placeholder只會在啓動的時候賦值,在運行過程中即便在Apollo上修改發佈了配置,placeholder的值也不會更新。如果需要運行時動態更新placeholder的值,有以下幾種方式:
- 程序監聽Apollo的ConfigChangeListener(如@ApolloConfigChangeListener),然後通過自己的代碼來更新placeholder的值。
- 不使用placeholder方式獲取配置,而是直接從config對象中獲取配置。樣例代碼可以參考3.2.3 Spring Annotation支持的
getBatch
和getTimeout
方法。 - 【推薦】使用Spring Cloud的RefreshScope
- 在需要刷新配置的bean上加@RefreshScope
- 加一個Apollo的ConfigChangeListener,然後在配置變化時調用RefreshScope的refreshAll()或者refresh(String name)的方法。
- 如果要刷新的不是自己的bean,而是spring cloud的bean,如spring cloud zuul,那麼可以調用org.springframework.cloud.context.refresh.ContextRefresher#refresh來刷新配置
- 相關代碼實現,可以參考apollo-demo項目中的AnnotatedBean.java和ApolloRefreshConfig.java
- 在非Spring Boot環境使用RefreshScope需要引入RefreshAutoConfiguration,引入方式可以參考apollo-demo項目中的相關配置代碼:JavaConfig示例,XmlConfig示例
3.2.2.1 XML使用方式
假設我有一個TestXmlBean,它有兩個配置項需要注入:
public class TestXmlBean {
private int timeout;
private int batch;
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public void setBatch(int batch) {
this.batch = batch;
}
public int getTimeout() {
return timeout;
}
public int getBatch() {
return batch;
}
}
那麼,我在XML中會使用如下方式來定義(假設應用默認的application namespace中有timeout和batch的配置項):
<?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/>
<bean class="com.ctrip.framework.apollo.spring.TestXmlBean">
<property name="timeout" value="${timeout:100}"/>
<property name="batch" value="${batch:200}"/>
</bean>
</beans>
3.2.2.2 Java Config使用方式
假設我有一個TestJavaConfigBean,通過Java Config的方式還可以使用@Value的方式注入:
public class TestJavaConfigBean {
@Value("${timeout:100}")
private int timeout;
private int batch;
@Value("${batch:200}")
public void setBatch(int batch) {
this.batch = batch;
}
public int getTimeout() {
return timeout;
}
public int getBatch() {
return batch;
}
}
在Configuration類中按照下面的方式使用(假設應用默認的application namespace中有timeout
和batch
的配置項):
@Configuration
@EnableApolloConfig
public class AppConfig {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
3.2.2.3 ConfigurationProperties使用方式
Spring Boot提供了@ConfigurationProperties把配置注入到bean對象中。
Apollo也支持這種方式,下面的例子會把redis.cache.expireSeconds
和redis.cache.commandTimeout
分別注入到SampleRedisConfig的expireSeconds
和commandTimeout
字段中。
@ConfigurationProperties(prefix = "redis.cache")
public class SampleRedisConfig {
private int expireSeconds;
private int commandTimeout;
public void setExpireSeconds(int expireSeconds) {
this.expireSeconds = expireSeconds;
}
public void setCommandTimeout(int commandTimeout) {
this.commandTimeout = commandTimeout;
}
}
在Configuration類中按照下面的方式使用(假設應用默認的application namespace中有redis.cache.expireSeconds
和redis.cache.commandTimeout
的配置項):
@Configuration
@EnableApolloConfig
public class AppConfig {
@Bean
public SampleRedisConfig sampleRedisConfig() {
return new SampleRedisConfig();
}
}
需要注意的是,@ConfigurationProperties
如果需要在Apollo配置變化時自動更新注入的值,需要配合Spring Cloud的RefreshScope使用。相關代碼實現,可以參考apollo-demo項目中的SampleRedisConfig.java和SpringBootApolloRefreshConfig.java
3.2.3 Spring Annotation支持
Apollo同時還增加了兩個新的Annotation來簡化在Spring環境中的使用。
- @ApolloConfig
- 用來自動注入Config對象
- @ApolloConfigChangeListener
- 用來自動註冊ConfigChangeListener
使用樣例如下:
public class TestApolloAnnotationBean {
@ApolloConfig
private Config config; //inject config for namespace application
@ApolloConfig("application")
private Config anotherConfig; //inject config for namespace application
@ApolloConfig("FX.apollo")
private Config yetAnotherConfig; //inject config for namespace FX.apollo
@Value("${batch:100}")
private int batch;
//config change listener for namespace application
@ApolloConfigChangeListener
private void someOnChange(ConfigChangeEvent changeEvent) {
//update injected value of batch if it is changed in Apollo
if (changeEvent.isChanged("batch")) {
batch = config.getIntProperty("batch", 100);
}
}
//config change listener for namespace application
@ApolloConfigChangeListener("application")
private void anotherOnChange(ConfigChangeEvent changeEvent) {
//do something
}
//config change listener for namespaces application and FX.apollo
@ApolloConfigChangeListener({"application", "FX.apollo"})
private void yetAnotherOnChange(ConfigChangeEvent changeEvent) {
//do something
}
//example of getting config from Apollo directly
//this will always return the latest value of timeout
public int getTimeout() {
return config.getIntProperty("timeout", 200);
}
//example of getting config from injected value
//the program needs to update the injected value when batch is changed in Apollo using @ApolloConfigChangeListener shown above
public int getBatch() {
return this.batch;
}
}
在Configuration類中按照下面的方式使用:
@Configuration
@EnableApolloConfig
public class AppConfig {
@Bean
public TestApolloAnnotationBean testApolloAnnotationBean() {
return new TestApolloAnnotationBean();
}
}
3.2.4 已有配置遷移
很多情況下,應用可能已經有不少配置了,比如Spring Boot的應用,就會有bootstrap.properties, application.properties等配置。
在應用接入Apollo之後,這些配置是可以非常方便的遷移到Apollo的,具體步驟如下:
- 在Apollo爲應用新建項目
- 在應用中配置好META-INF/app.properties
- 把原先配置(必須是properties格式)複製一下,然後通過Apollo提供的文本編輯模式全部粘帖到應用的application namespace,發佈配置
- 如果原來是其它格式,如yml,請先轉成properties格式
- 如http://www.toyaml.com/
- 把原先的配置文件如bootstrap.properties, application.properties從項目中刪除
- 如果需要保留本地配置文件,需要注意部分配置如
server.port
必須確保本地文件已經刪除該配置項
- 如果需要保留本地配置文件,需要注意部分配置如
如:
spring.application.name = reservation-service
server.port = 8080
logging.level = ERROR
eureka.client.serviceUrl.defaultZone = http://127.0.0.1:8761/eureka/
eureka.client.healthcheck.enabled = true
eureka.client.registerWithEureka = true
eureka.client.fetchRegistry = true
eureka.client.eurekaServiceUrlPollIntervalSeconds = 60
eureka.instance.preferIpAddress = true
3.3 Demo
項目中有一個樣例客戶端的項目:apollo-demo
,具體信息可以參考Apollo開發指南中的2.3 Java樣例客戶端啓動部分。
更多使用案例Demo可以參考Apollo使用場景和示例代碼。
四、客戶端設計
上圖簡要描述了Apollo客戶端的實現原理:
- 客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
- 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,爲了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率默認爲每5分鐘拉取一次,客戶端也可以通過在運行時指定System Property:
apollo.refreshInterval
來覆蓋,單位爲分鐘。
- 客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會保存在內存中
- 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份
- 在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置
- 應用程序可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知
五、本地開發模式
Apollo客戶端還支持本地開發模式,這個主要用於當開發環境無法連接Apollo服務器的時候,比如在郵輪、飛機上做相關功能開發。
在本地開發模式下,Apollo只會從本地文件讀取配置信息,不會從Apollo服務器讀取配置。
可以通過下面的步驟開啓Apollo本地開發模式。
5.1 修改環境
修改/opt/settings/server.properties(Mac/Linux)或C:\opt\settings\server.properties(Windows)文件,設置env爲Local:
env=Local
更多配置環境的方式請參考1.2.2 Environment
5.2 準備本地配置文件
在本地開發模式下,Apollo客戶端會從本地讀取文件,所以我們需要事先準備好配置文件。
5.2.1 本地配置目錄
本地配置目錄位於:
- Mac/Linux: /opt/data/{appId}/config-cache
- Windows: C:\opt\data\{appId}\config-cache
appId就是應用的appId,如100004458。
請確保該目錄存在,且應用程序對該目錄有讀權限。
【小技巧】 推薦的方式是先在普通模式下使用Apollo,這樣Apollo會自動創建該目錄並在目錄下生成配置文件。
5.2.2 本地配置文件
本地配置文件需要按照一定的文件名格式放置於本地配置目錄下,文件名格式如下:
{appId}+{cluster}+{namespace}.properties
- appId就是應用自己的appId,如100004458
- cluster就是應用使用的集羣,一般在本地模式下沒有做過配置的話,就是default
- namespace就是應用使用的配置namespace,一般是application
文件內容以properties格式存儲,比如如果有兩個key,一個是request.timeout,另一個是batch,那麼文件內容就是如下格式:
request.timeout=2000
batch=2000
5.3 修改配置
在本地開發模式下,Apollo不會實時監測文件內容是否有變化,所以如果修改了配置,需要重啓應用生效。