Apollo(阿波羅)是攜程開源的分佈式配置中心,可以集中化管理應用不一樣環境、不一樣集羣的配置,支持配置熱發佈並實時推送到應用端,而且具有規範的權限及流程治理等特性,適用於分佈式微服務配置管理場景java
程序功能日益複雜,程序配置日益增多:各類功能開關、參數配置、服務器地址...對程序配置的指望也愈來愈高:熱部署並實時生效、灰度發佈、分環境分集羣管理配置、完善的權限審覈機制...在這樣的背景下,Apollo配置中心應運而生。Apollo支持四個維度Key-Value格式的配置* Application(應用) 實際使用配置的應用,Apollo客戶端在運行時須要知道當前應用是誰,從而能夠去獲取對應的配置。每一個應用都有對應的身份標識--appId,須要在代碼中配置數據庫
Apollo在建立項目的時候,都會默認建立一個"application"的Namespace,"application"是個應用自身使用的。例如Spring Boot中項目的默認配置文件application.yaml,這裏application.yaml就等同於"application"的Namespace。對於大多數應用來講,"application"Namespace已經能知足平常配置使用場景json
客戶端獲取"application"Namespace的代碼以下緩存
Config config = ConfigService.getAppConfig()複製代碼
客戶端獲取非"application"Namespace的代碼以下安全
Config config = ConfigService.getConfig(namespaceName)複製代碼
Namespace的格式 配置文件有多種格式,properties、xml、yml、yaml、json等,一樣Namespace也具備這些格式tips: 非properties格式的namespace,在客戶端使用時須要調用ConfigService.getConfigFile(String namespace, ConfigFileFormat configFileFormat)
來獲取,若是使用Htpp接口直接調用時,對應的namespace參數須要傳入namespace的名字加上後綴名,如datasource.jsonNamespace的獲取權限分類 此處權限相是對於Apollo客戶端來講的private(私有的)權限 private權限的Namespace,只能被所屬的應用獲取到。一個應用嘗試獲取其餘應用private的Namespace,Apollo客戶端會報"404"異常服務器
public(公共的)權限 具備public權限的Namespace,能被任何應用獲取網絡
Namespace的類型架構
使用場景 部門級別共享的配置、小組級別共享的配置、幾個項目之間共享的配置、中間件客戶端的配置併發
k1 = v1
k2 = v2複製代碼
而後應用A有一個關聯類型的Namespace關聯此公共Namespace,且以新值v3覆蓋配置項k1。那麼在應用A實際運行時,獲取到的公共Namespace的配置爲複製代碼
k1 = v3
k2 = v2複製代碼
使用場景 假設RPC框架的配置(如:timeout)有如下要求
* 提供一份全公司默認的配置,且可動態調整
* RPC客戶端項目能夠自定義某些配置項且可動態調整
結合Apollo的公共類型的Namespace和關聯類型的Namespace。RPC團隊在Apollo上維護一個叫「rpc-client」的公共Namespace,在"rpc-client"Namespace上配置默認的參數值。rpc-client.jar裏的代碼讀取"rpc-client"Namespace的配置便可;如須要調整默認的配置,只須要修改公共類型"rpc-client"Namespace的配置;若是客戶端項目想要自定義或動態修改某些配置項,只須要在Apollo本身項目下關聯"rpc-client",就能建立關聯類型"rpc-client"的Namespace,而後在關聯類型下修改配置項便可。這裏rpc-client.jar是在應用容器裏運行的,因此rpc-client獲取到"rpc-client"Namespace的配置是應用的關聯類型的Namespace加上公共類型的Namespace
例子 以下圖,有三個應用:應用A、應用B、應用C
應用A有兩個私有類型的Namespace:application和NS-Private,以及一個關聯類型的Namespace:NS-Public
應用B有一個私有類型的Namespace:application,以及一個公共類型的Namespace:NS-Public
應用C只有一個私有類型的Namespace:application
![](https://user-gold-cdn.xitu.io/2019/9/29/16d7d726928b404f?w=800&h=460&f=jpeg&s=22615)複製代碼
應用A獲取Apollo配置
//application
Config appConfig = ConfigService.getAppConfig();
appConfig.getProperty("k1", null); // k1 = v11
appConfig.getProperty("k2", null); // k2 = v21
//NS-Private
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.getProperty("k1", null); // k1 = v3
privateConfig.getProperty("k3", null); // k3 = v4
//NS-Public,覆蓋公共類型配置的狀況,k4被覆蓋
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.getProperty("k4", null); // k4 = v6 cover
publicConfig.getProperty("k6", null); // k6 = v6
publicConfig.getProperty("k7", null); // k7 = v7複製代碼
應用B獲取Apollo配置
//application
Config appConfig = ConfigService.getAppConfig();
appConfig.getProperty("k1", null); // k1 = v12
appConfig.getProperty("k2", null); // k2 = null
appConfig.getProperty("k3", null); // k3 = v32複製代碼
//NS-Private,因爲沒有NS-Private Namespace 因此獲取到default value
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.getProperty("k1", "default value");
//NS-Public
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.getProperty("k4", null); // k4 = v5
publicConfig.getProperty("k6", null); // k6 = v6
publicConfig.getProperty("k7", null); // k7 = v7複製代碼
應用C獲取Apollo配置
//application
Config appConfig = ConfigService.getAppConfig();
appConfig.getProperty("k1", null); // k1 = v12
appConfig.getProperty("k2", null); // k2 = null
appConfig.getProperty("k3", null); // k3 = v33
//NS-Private,因爲沒有NS-Private Namespace 因此獲取到default value
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.getProperty("k1", "default value");
//NS-Public,公共類型的Namespace,任何項目均可以獲取到
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.getProperty("k4", null); // k4 = v5
publicConfig.getProperty("k6", null); // k6 = v6
publicConfig.getProperty("k7", null); // k7 = v7
複製代碼
ChangeListener 以上代碼能夠看出,在客戶端Namespace映射成一個Config對象,Namespace配置變動的監聽器是註冊在Config對象上app
Config appConfig = ConfigService.getAppConfig();appConfig.addChangeListener(new ConfigChangeListener() {public void onChange(ConfigChangeEvent changeEvent) {//do something}})
在應用A中監聽 NS-Private 的 Namespace代碼以下
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.addChangeListener(new ConfigChangeListener() {
public void onChange(ConfigChangeEvent changeEvent) {
//do something
}
})
## 在應用A、應用B和應用C中監聽NS-Public Namespace代碼以下
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.addChangeListener(new ConfigChangeListener() {
public void onChange(ConfigChangeEvent changeEvent) {
//do something
}
})
複製代碼
配置的幾大屬性
配置獲取規則 僅當應用自定義了集羣或namespace才須要。有了cluster概念後,配置的規則就顯得重要了,好比應用部署在A機房,可是並無在Apollo新建cluster或者在運行時指定了cluster=SomeCluster,可是並無在Apollo新建cluster,這時候Apollo的行爲是怎樣的?下面介紹配置獲取的規則應用自身配置的獲取規則當應用使用下面的語句獲取配置時,稱之爲獲取應用自身的配置,也就是應用自身的application namespace的配置
Config config = ConfigService.getAppConfig();複製代碼
這種狀況的配置獲取規則簡而言之以下
因此,若是應用部署在A數據中心,可是用戶沒有在Apollo建立cluster,那麼獲取的配置就是默認cluster(default)的;若是應用部署在A數據中心,同時在運行時指定了apollo.cluster=SomeCluster,可是沒有在Apollo建立cluster,那麼獲取的配置就是A數據中心cluster的配置,若是A數據中心cluster沒有配置的話,那麼獲取的配置就是默認cluster(default)的
* 公共組件配置的獲取規則
以`FX.Hermes.Producer`爲例,hermes producer是hermes發佈的公共組件。當使用下面的語句獲取配置時,稱之爲獲取公共組件的配置複製代碼
Config config = ConfigService.getConfig("FX.Hermes.Producer")複製代碼
對於這種狀況獲取配置規則,簡而言之以下
FX.Hermes.Producer
namespace的配置 FX.Hermes.Producer
namespace的配置 經過這種方式實現對框架組件的配置管理,框架組件提供方提供配置的默認值,應用若是有特殊需求能夠自行覆蓋
整體設計
**Apollo架構V1** 若是不考慮分佈式微服務架構中的服務發現問題,Apollo的最簡架構以下圖所示
![Apollo架構V1](https://user-gold-cdn.xitu.io/2019/9/29/16d7d7275299a778?w=800&h=315&f=jpeg&s=16632)
要點
* ConfigService是一個獨立的微服務,服務於Client進行配置獲取
* Client和ConfigService保持長鏈接,經過一種推拉結合(push & pull)的模式,在實現配置實時更新的同時,保證配置更新不丟失
* AdminService是一個獨立的微服務,服務於Portal進行配置管理。Portal經過調用AdminService進行配置管理和發佈
* ConfigService和AdminService共享ConfigDB,ConfigDB中存放項目在某個環境中的配置信息。ConfigService/AdminService/ConfigDB三者在每一個環境(DEV/FAT/UAT/PRO)中都要部署一份
* Protal有一個獨立的PortalDB,存放用戶權限、項目和配置的元數據信息。Protal只需部署一份,它能夠管理多套環境複製代碼
**Apollo架構 V2** 爲了保證高可用,ConfigService和AdminService都是無狀態以集羣方式部署的,這時候就存在一個服務發現的問題:Client怎麼找到ConfigService?Portal怎麼找到AdminService?爲了解決這個問題,Apollo在其架構中引入Eureka服務註冊中心組件,實現微服務間的服務註冊和發現,更新後的架構以下圖所示
![Apollo架構V2](https://user-gold-cdn.xitu.io/2019/9/29/16d7d727764634a6?w=800&h=502&f=jpeg&s=23979)
要點
* ConfigService和AdminService啓動後都會註冊到Eureka服務註冊中心,並按期發送存活心跳
* Eureka採用集羣方式部署,使用分佈式一致性協議保證每一個實例的狀態最終一致
複製代碼
Apollo架構V3 Eureka是自帶服務發現的Java客戶端的,若是Apollo只支持Java客戶端接入,不支持其它語言客戶端接入的話,那麼Client和Portal只須要引入Eureka的Java客戶端,就能夠實現服務發現功能。發現目標服務後,經過客戶端軟負載(SLB,例如Ribbon)就能夠路由到目標服務實例。這是一個經典的微服務架構,基於Eureka實現服務註冊發現+客戶端Ribbon配合實現軟路由,以下圖所示
Apollo架構V4
爲支持多語言客戶端接入,Apollo引入MetaServer角色,它實際上是一個Eureka的Proxy,將Eureka的服務發現接口以更簡單明確的HTTP接口的形式暴露出來,方便Client/Protal經過簡單的HTTPClient就能夠查詢到ConfigService/AdminService的地址列表。獲取到服務實例地址列表以後,再以簡單的客戶端軟負載(Client SLB)策略路由定位到目標實例,併發起調用
另外一個問題,MetaServer自己也是無狀態以集羣方式部署的,那麼Client/Protal該如何發現MetaServer呢?一種傳統的作法是藉助硬件或者軟件負載均衡器,在攜程採用的是擴展後的NginxLB(Software Load Balancer),由運維爲MetaServer集羣配置一個域名,指向NginxLB集羣,NginxLB再對MetaServer進行負載均衡和流量轉發。Client/Portal經過域名+NginxLB間接訪問MetaServer集羣
引入MetaServer和NginxLB以後的架構以下圖
Apollo架構V5
還剩下最後一個環節,Portal也是無狀態的以集羣方式部署的,用戶如何發現和訪問Portal?答案也是簡單的傳統作法,用戶經過域名+NginxLB間接訪問Portal集羣。因此V5版本是包括用戶端的最終的Apollo架構全貌,以下圖所示
配置發佈後的實時推送設計 在配置中心中,一個重要的功能就是配置發佈後實時推送到客戶端。下面咱們簡要看一下這塊是怎麼設計實現的
![服務端設計](https://user-gold-cdn.xitu.io/2019/9/29/16d7d7280f721f5f?w=800&h=280&f=jpeg&s=15776)複製代碼
上圖簡要描述了配置發佈的大體過程
1. 用戶在Portal操做發佈配置
2. Portal調用Admin Service的接口操做發佈
3. Admin Service發佈配置後,發送ReleaseMessage給各Config Service
4. Config Service收到ReleaseMessage後通知對應的客戶端複製代碼
**發送ReleaseMessage的實現方式** Admin Service在配置發佈後,須要通知全部的Config Service有配置發佈,從而Config Service能夠通知對應的客戶端來拉取最新的配置。從概念上看,這是一個典型的消息使用場景,Admin Service做爲Producer發出消息,各個Config Service做爲consumer消費消息。經過一個消息組件(Message Queue)就能很好地實現Admin Service和Config Service的解耦。在實現上,Apollo爲儘可能減小外部依賴,沒有采用外部的消息中間件,而是經過數據庫實現了一個簡單的消息隊列複製代碼
實現方式以下
1. Admin Service在配置發佈後會往ReleaseMessage表插入一條消息記錄,消息內容就是配置發佈的AppId+Cluster+Namespace
2. Config Service有一個線程會每秒掃描一次ReleaseMessage表,看是否有新的消息記
3. Config Service若是發現有新的消息記錄,那麼會通知到全部的消息監聽器(ReleaseMessageListener),例如NotificationControllerV2
4. 消息監聽器獲得配置發佈的AppId+Cluster+Namespace後,會通知對應的客戶端
示意圖以下
![配置更新通知](https://user-gold-cdn.xitu.io/2019/9/29/16d7d728386cd0b0?w=800&h=788&f=jpeg&s=29770)複製代碼
Config Service通知客戶端的實現方式 消息監聽器在得知有新的配置發佈後是如何通知到客戶端的呢?其實現方式以下
客戶端設計上圖簡要描述了Apollo客戶端的實現原理
名稱解析
普通應用接入指南
公共組件接入步驟公共組件接入步驟幾乎與普通應用接入一致,惟一的區別是公共組件須要創建本身的惟一Namespace
1.建立項目
2.項目管理員權限
3.建立Namespace
4.添加配置項
5.發佈配置
6.應用讀取配置複製代碼
應用覆蓋公共組件配置步驟
1.關聯公共組件Namespace
2.覆蓋公共組件配置
3.發佈配置複製代碼
多個AppId共享同一份配置在一些狀況下,儘管應用自己不是公共組件,但仍是須要在多個AppId之間共用同一份配置,這種狀況下若是但願實現多個AppId使用同一份配置的話,基本概念和公共組件的配置是一致的。具體來講,就是在其中一個AppId下建立一個namespace,寫入公共的配置信息,而後在各個項目中讀取該namespace的配置便可;若是某個AppId須要覆蓋公共的配置信息,那麼在該AppId下關聯公共的namespace並寫入須要覆蓋的配置便可
應用接入策略這裏考慮非Java語言客戶端接入--直接經過Http接口獲取配置
**HTTP接口說明**複製代碼
**URL** {config_server_url}/configfiles/json/{appId}/{clusterName}/{namespaceName}?ip={clientIp}複製代碼
**Method** GET複製代碼
**參數說明 **
![file](https://user-gold-cdn.xitu.io/2019/9/29/16d7d728a24bb557?w=800&h=682&f=jpeg&s=68943)複製代碼
**HTTP接口返回格式** 該HTTP接口返回的是JSON格式、UTF-8編碼,包含了對應namespace中全部的配置項。返回內容Sample以下
{
"portal.elastic.document.type":"biz",
"portal.elastic.cluster.name":"hermes-es-fws"
}
*TIPS 經過{configserverurl}/configfiles/{appId}/{clusterName}/{namespaceName}?ip={clientIp}能夠獲取到properties形式的配置*
* 不帶緩存的HTTP接口從Apollo讀取配置
該接口會直接從數據庫中獲取配置,能夠配合配置推送通知實現實時更新配置複製代碼
**URL** {config_server_url}/configs/{appId}/{clusterName}/{namespaceName}?releaseKey={releaseKey}&ip={clientIp}複製代碼
**Method** GET複製代碼
**參數說明**
![file](https://user-gold-cdn.xitu.io/2019/9/29/16d7d728d1fafe5a?w=800&h=753&f=jpeg&s=77670)複製代碼
該HTTP接口返回的是JSON格式、UTF-8編碼。若是配置沒有變化(傳入的releaseKey和服務端的相等),則返回HttpStatus 304,Response Body爲空;若是配置有變化,則會返回HttpStatus 200,Response Body爲對應namespace的meta信息以及其中全部的配置項。返回內容Sample以下
{
"appId": "100004458",
"cluster": "default",
"namespaceName": "application",
"configurations": {
"portal.elastic.document.type":"biz",
"portal.elastic.cluster.name":"hermes-es-fws"
},
"releaseKey": "20170430092936-dee2d58e74515ff3"
}
複製代碼
**配置更新推送實現思路** 建議參考Apollo的Java實現RemoteConfigLongPollService.java複製代碼
初始化 首先須要肯定哪些namespace須要配置更新推送,Apollo的實現方式是程序第一次獲取某個namespace的配置時就會來註冊一下,咱們就知道有哪些namespace須要配置更新推送了。初始化後的結果就是獲得一個notifications的Map,內容是namespaceName -> notificationId(初始值爲-1)。運行過程當中若是發現有新的namespace須要配置更新推送,直接塞到notifications這個Map裏面便可
請求服務 有了notifications這個Map以後,就能夠請求服務了。這裏先描述一下請求服務的邏輯,具體的URL參數和說明請參見後面的接口說明
1.請求遠端服務,帶上本身的應用信息以及notifications信息複製代碼
2.服務端針對傳過來的每個namespace和對應的notificationId,檢查notificationId是不是最新的複製代碼
3.若是都是最新的,則保持住請求60秒,若是60秒內沒有配置變化,則返回HttpStatus 304。若是60秒內有配置變化,則返回對應namespace的最新notificationId, HttpStatus 200複製代碼
4.若是傳過來的notifications信息中發現有notificationId比服務端老,則直接返回對應namespace的最新notificationId, HttpStatus 200複製代碼
5.客戶端拿到服務端返回後,判斷返回的HttpStatus複製代碼
6.若是返回的HttpStatus是304,說明配置沒有變化,從新執行第1步複製代碼
7.若是返回的HttpStauts是200,說明配置有變化,針對變化的namespace從新去服務端拉取配置,參見1.3 經過不帶緩存的Http接口從Apollo讀取配置。同時更新notifications map中的notificationId。從新執行第1步複製代碼
HTTP接口說明複製代碼
URL {config_server_url}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}複製代碼
Method GET複製代碼
參數說明複製代碼
TIPS 因爲服務端會hold住60秒,因此請確保客戶端訪問服務端的超時時間要大於60秒;記得對參數進行URL Encode
HTTP返回格式 該Http接口返回的是JSON格式、UTF-8編碼,包含了有變化的namespace和最新的notificationId。返回內容Sample以下
[{
"namespaceName": "application",
"notificationId": 101
}]複製代碼
官方展現的部署策略,生產環境部署一套Apollo-Portal+ApolloPortalDB,其餘環境(PRO、UAT、FAT、DEV)單獨部署MetaServer+AdminService+ConfigService,使用獨立數據庫ApolloConfigDB及應用服務;MetaServer和Config Service部署在同一個JVM進程內,Admin Service部署在同一臺服務器的另外一個JVM進程內。部署示例以下圖網絡策略 分佈式部署的時候,apollo-configservice和apollo-adminservice須要把本身的IP和端口註冊到Meta Server(apollo-configservice自己)。Apollo客戶端和Portal會從Meta Server獲取服務的地址(IP+PORT),而後經過服務地址直接訪問。apollo-configservice和apollo-adminservice是基於內網可信網絡設計的,因此出於安全考慮,請不要將apollo-configservice和apollo-adminservice直接暴露在公網
部署步驟
建立數據庫 Apollo服務端依賴於MYSQL數據庫,因此須要事先建立並完成初始化
獲取安裝包 Apollo服務端安裝包共3個: Apollo-AdminService、Apollo-ConfigService、Apollo-Portal
部署Apollo服務端 獲取安裝包後就能夠部署到測試和生產環境複製代碼
文章較爲全面介紹開源分佈式配置中心Apollo的設計、使用、應用接入及部署方法,目前客戶端只有Java和.Net版本,其餘語言客戶端的接入能夠經過HTTP接口的方式定時拉取更新配置或經過Http Long Polling機制實時推送,實現應用感知配置更新