微服務下的配置管理—Apollo
爲什麼需要統一管理配置
在微服務的架構下,架構的複雜度以及服務的數量都會比之前單體應用複雜很多,配置的集中管理,以及模塊化管理是非常有必要的。
服務對於配置的依賴程度也非常高,配置修改後的實時生效、灰度發佈、環境的區分等等。
我們的服務部署在k8s上,打包的方式基於docker所以一次構建所有環境部署這一點是毋庸置疑的.配置外置使容器達到真正的無狀態。
在面對這樣的大環境,普通的的配置文件管理已經無法滿足我們的需求了,所以需要尋找一個解決方案.最終我們選擇了使用Apollo來幫助我們完成上述的事情。
爲什麼選擇Apollo
- Apollo所包含的功能符合上文我所提到的需求
- 統一管理不同環境、不同集羣的配置;
- 配置修改實時生效;
- 發佈版本管理;
- 灰度發佈;
- 提供了權限管理、發佈審覈、操作審計的功能,很好地幫助我們記錄配置的變化方便回滾和追蹤問題;
- 提供了良好的UI操作頁面操作上手非常容易;
- Java原生
- Apollo使用Java+Spring全家桶進行開發技術棧與我們完全符合可以通過閱讀源碼去快速定位問題以及自由的定製化開發;
- 提供了非常好的Java-Client可以快速繼承到Spring框架中;
- 提供開發平臺API
- Apollo對外提供API可以發佈和修改已經發布的配置,這樣我們就可以根據自己的需求對Apollo的API進行封裝;
- 部署簡單
- 配置中心作爲基礎服務,對可用性要求非常高,這一點其自身就提供高可用的部署方式;
- Apollo的部署只需要依賴mysql,只需要安裝Java和Mysql就可以讓Apollo跑起來;
- 提供打包腳本,方便我們自定義鏡像;
接下來就不對Apollo配置中心進行過多的介紹了,有興趣的小夥伴可以通過官網自行了解。
Apollo在PaaS中的應用
- Apollo在k8s上的部署;
- Apollo的配置管理;
Apollo的部署
在我們的平臺中所有的服務都是基於Kubernetes(一下簡稱k8s)進行部署的,所以Apollo配置中心也不例外,我們將admin、config、portal分別打成鏡像部署到了k8s上。
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進程中
k8s部署
使用k8s部署首先需要製作對應的鏡像
官方文檔提供了k8s部署解決方案,但是其操作過於繁瑣,而且定義了大量的yaml文件,這些文件可以給我們提供參考價值,但是再生產過程中還是不要拿來直接使用,最好是使用我們自己構建的鏡像.
- 打開*scripts/build.sh*文件並將數據庫以及meta替換成我們指定的mysql以及service-name;
# config的數據庫配置
apollo_config_db_url=jdbc:mysql://fill-in-the-correct-server:3306/ApolloConfigDB?characterEncoding=utf8
apollo_config_db_username=FillInCorrectUser
apollo_config_db_password=FillInCorrectPassword
# protal的數據庫配置
apollo_portal_db_url=jdbc:mysql://fill-in-the-correct-server:3306/ApolloPortalDB?characterEncoding=utf8
apollo_portal_db_username=FillInCorrectUser
apollo_portal_db_password=FillInCorrectPassword
# 指定config 的 service-name
dev_meta=http://apollo-config:8080
-
執行build.sh文件完成打包;
-
將apollo-config、apollo-admin、apollo-portal打包成docker鏡像;
- 在對應目錄分別執行 mvn docker:build;
- 將鏡像推送到自己的docker私倉準備使用;
在k8s上完成部署
上文已經指出爲了簡化部署Config Service,Eureka以及Meta Server三個邏輯角色都被放在了apollo-config這一個項目裏了,所以使用Client連接Meta Server的時候應該注意實際連接的是apolo-config的service,同理Protal想要連接apollo-admin的時候也需要經過Meta Server去獲取admin的地址和端口列表.
配置build.sh時需要注意指定Meta Server的service-name
apollo-admin-7fcd446cf5-hlt7x 1/1 Running 0 20d
apollo-config-d46ccdf65-9nr5c 1/1 Running 0 20d
apollo-portal-7788fd9f8d-8rgw7 1/1 Running 0 20d
Apollo的配置託管
Apollo支持4個維度去管理Key-Value格式的配置:
- application(應用)
- evironment(環境)
- cluster(集羣)
- namespace(命名空間)
在這四個維度中,前三個維度都是逐層進行區分的,第四個維度是並列的關係.在使用的時候application與evironment都是必須指定的,client會根據指定的信息拉取對應的配置。
Apollo中多環境配置
基於雲原生構建部署的服務會存在多集羣多機房的情況,將配置對外暴露可以讓我們的服務真正的做到一次構建,多環境部署,不同環境的區分僅通過配置文件就可以實現.
Apollo對應用的劃分規則整好符合我們的需求:
application->服務鏡像
evironment->部署環境
cluster->多集羣的選擇
在apollo中對於這三個狀態的選擇有多種的實現方式,具體就不在這裏一一例舉了,我們的實現方式是通過JVM參數傳入來選擇應用所需要的配置
在上文中也提到在我們所有的服務都是基於k8s進行部署的,所以我們線上與預生產放在一個portal中,開發以及測試在其他的集羣中.
集羣的選擇通過k8s的yaml文件進行控制,將JVM參數配置成環境變量傳遞給Dockerfile
Apollo中配置的關聯
每個應用的配置是由命名空間構成的,在默認的情況下每個應用都會有一個application命名空間,這個命名空間默認是私有的.在應用中還可以創建公共的命名空間提供給其他應用進行關聯,在構建微服務時會有連接大量的中間件,在相同的環境下,這些中間件的連接方式基本類似,有的甚至是完全一樣.
在這種情況下可以定義一個template將一些基本的配置信息統一定義然後其他的應用去關聯這些配置,在使用的時候只對發生變化的內容進行修改即可.
Apollo在Spring中管理配置的方式
我們在使用Apollo的時候將應用所有的配置文件都保存在Apollo中,但是有的時候爲了方便測試本地的配置文件還是會保留大量的配置,這些配置再發布上線的時候會對線上Apollo的配置帶來影響麼?
答案是不會的,這是由Apollo加載配順序所導致的,通過觀察源碼我們可以發現,對於Spring託管的項目而言Apollo會將新獲取到的配置文件放在集合的最前面。
@Override
public void initialize(ConfigurableApplicationContext context) {
//獲取已經傳入的配置
ConfigurableEnvironment environment = context.getEnvironment();
//初始化Apollo的系統參數,就是我們熟知的'app.id'等
initializeSystemProperty(environment);
//解析配置文件獲取namespaces
String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
......
//創建一個容器來接收Apollo上的配置
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
......
//這裏講解析的配置文件添加到First(這裏說明下,由於Spring在解析配置文件的時候是前面的覆蓋後見面的,所以爲了讓Apollo的配置文件優先級高於本地的配置文件,這裏放在最前面)
environment.getPropertySources().addFirst(composite);
}
繼續查看PropertySources的getproperties方法可以發現在獲取配置的時會按順序遍歷集合一旦獲取到對應的配置則會跳出循環
//順序遍歷集合並根據key查找配置
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
//查找配置
Object value = propertySource.getProperty(key);
//找到配置跳出循環
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Could not find key '" + key + "' in any property source");
}
return null;
}
運行時監聽配置變化(熱部署)
在Apollo-Client內部會維護一個“長連接”,這是一個Long Polling(長輪詢),通過長輪詢來保證配置更新的實時性。
- 客戶端發起一個Http請求到服務端
- 服務端會保持住這個連接60秒
- 如果在60秒內有客戶端關心的配置變化,被保持住的客戶端請求會立即返回,並告知客戶端有配置變化的namespace信息,客戶端會據此拉取對應namespace的最新配置
- 如果在60秒內沒有客戶端關心的配置變化,那麼會返回Http狀態碼304給客戶端
- 客戶端在收到服務端請求後會立即重新發起連接,回到第一步
關鍵代碼分析
//單線程維持調用,在該線程內會維護Long polling的相關操作
static {
m_executorService = Executors.newScheduledThreadPool(1,
ApolloThreadFactory.create("RemoteConfigRepository", true));
}
......
//設置了5秒的等待時間,防止頻繁的刷新配置
if (!m_loadConfigRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
//wait at most 5 seconds
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
}
}
......
//assembleQueryConfigUrl()==組裝請求的URL(根據appid,cluster,namespace)
Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "queryConfig");
transaction.addData("Url", url);
......
//設置了請求超時時間,這裏指定的是90秒,而服務端的斷開時間是60秒
request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);
......
//和遠程建立連接
private void scheduleLongPollingRefresh() {
remoteConfigLongPollService.submit(m_namespace, this);
}
//處理結果之後繼續循環
Gateway動態路由實現
在PaaS項目中我們的動態路由就是通過開放平臺API以及熱部署實現的,操作流程如下圖所示:
在監聽到配置變更事件之後調用RouteDefinitionRepository所提供的方法對路由信息進行更改。
@ApolloConfigChangeListener(interestedKeyPrefixes = "spring.cloud.gateway.")
public void onChange(ConfigChangeEvent changeEvent) {
//清理被覆蓋的路由信息
preDestroyGatewayProperties(changeEvent);
//刷新配置
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
//更新Gateway路由列表
refreshGatewayRouteDefinition();
總結
本文開篇講述了我們爲什麼選擇Apollo來作爲配置管理中心,接着對Apollo的作用進行了簡要的分析後開始介紹Apollo在PaaS中的應用:
- k8s的部署方式;
- 多環境的區分;
- 配置屬性的模板化以及關聯引用;
- Spring中的配置管理方式;
- 運行時監聽配置變化。
實際上Apollo還有很多特性等待着我們去開發:比如使用namespace結合Spring的Start後者是ConfigurationProperties對配置進行模板化,使開發人員在引入新的中間件或者是三方服務使只需要引入一個namespace以及dependency就可以直接開始工作。
由於篇幅的限制介紹的內容不是很直觀還請見諒,如果有疑問非常歡迎在品論區進行交流.