在微服務架構中,配置中心是必不可少的基礎服務。ConfigKeeper已開源,本文將深度分析配置中心的核心內容,錯過「Spring Cloud中國社區北京沙龍-2018.10.28 」的同學將從本篇文章中收獲現場的分享內容。
背景
微服務+容器架構后,為了方便動態更新應用配置,需要把配置文件放到應用執行包之外的配置中心,這樣一來,一個可執行包就可以在不同的環境下運行,大幅度降低包的版本管理成本,也可以有效控制docker鏡像的版本管理成本。傳統的通過配置文件、數據庫等方式已經越來越無法滿足開發人員對配置管理的需求。對程序配置的期望值也越來越高:配置修改后實時生效,分環境、分集群管理配置,完善的權限、審核機制等等。於是便誕生了ConfigKeeper。
ConfigKeeper是隨行付架構部基於Spring Cloud研發的分布式配置中心,與Spring Boot、Spring Cloud應用無縫兼容。
雖然Spring Cloud 已經為我們提供了基於git或mongodb等實現的配置中心,但是這些方案實現都過於簡單,沒有達到實際可用的標准。比如:沒有提供統一的管理頁面,不便於操作和使用;沒有權限管理功能;沒有數據驗證功能等等。但Spring Cloud Config的核心技術還是可以為我們所用,沒有必要重新造輪子。
定制的原因
市面上已經有幾款比較成型的配置中心,大家耳熟能詳的攜程Apollo和百度Disconf,而我們的配置中心底層是基於Spring Cloud Config模塊進行擴展的,首先來看看Apollo、Spring Cloud Config、ConfigKeeper的功能差異:
功能點 | Apollo | Spring Cloud Config | ConfigKeeper |
---|---|---|---|
配置界面 | 一個界面管理不同環境、不同集群配置 | 無,需要通過git操作 | 配置信息落入數據庫中,友好頁面管理 |
配置生效時間 | 實時 | 重啟生效或者手動refresh生效 | 實時推送、重啟生效、手動refresh生效 |
版本管理 | 界面上直接提供發布歷史和回滾按鈕 | 無,需要通過git操作 | 管理頁面一鍵回滾 |
灰度發布 | 支持 | 不支持 | 支持,與Spring Cloud其他組件打通 |
授權、審核、審計 | 界面上直接支持,而且支持修改、發布權限分離 | 需要通過git倉庫設置,且不支持修改、發布權限分離 | 應用分配制權限管理 |
實例配置監控 | 可以方便的看到當前哪些客戶端在使用哪些配置 | 不支持 | 心跳推送,一目了然 |
配置獲取性能 | 快,通過數據庫訪問,還有緩存支持 | 較慢,需要從git clone repository,然后從文件系統讀取 | 本地式緩存文件,配置增量推送 |
客戶端支持 | 原生支持所有Java和.Net應用 | 支持Spring應用,提供annotation獲取配置 | Spring、Spring Boot、Spring Cloud |
支持YAML格式 | 不支持 | 支持 | 支持 |
除了上述之外,還有以下其他功能特性:
- 開發人員最習慣的就是在文件中修改配置,管理頁面上提供「舒適」的富文本編輯框;
- 全局配置約定,比如多個項目共享的配置,比如短信地址等采取約定大於配置。全局配置<應用配置;
- 配置校驗,文本修改高亮對比修改內容,防止低級錯誤等;
架構設計
有史以來最簡單的配置中心。使用數據庫保存配置是因為微服務拆分粒度相對比較細,使用的配置也會相對比較少,所以使用數據庫表就夠保存,流程如下:
- 用戶先去配置中心 添加、修改配置;
- 應用啟動時:(Spring boot應用向配置中心客戶端獲取配置、然后緩存配置到本地內存及本地文件緩存、應用根據配置進行啟動;)
- 不停機更新配置(調用Spring Cloud的RefreshEndpoint、通過RefreshEndpoint刷新配置)
- 使用前后端分離架構,如果需要重新設計管理界面,也可以使用自己習慣的技術實現
設計的初衷
通過講解管理后台功能,理解我們當初出於什么原因為什么要這么設計?能解決哪些問題?設計時的考慮點有哪些?通過前面的閱閱讀,已知ConfigKeeper有以下核心功能:
權限管理
為什么要有權限管理?
- 1.對於企業級應用來說,權限管理是必不可以一個需求;
- 2.通過權限管理隔離數據,保證數據的安全性,避免誤操作;
- 3.在微服務比較多情況下,也可以通過權限自動過濾出我們所關心的服務,不需要再自己手動過濾,減少不必要的操作,可以提高工作效率;
這個權限系統是我們最初設計的,我們內部現在使用了一個統一的權限系統。為了降低管理成本,我們也開發了微服務管理平台,將配置中心,注冊中心,網關管理后台等一系列基礎服務都接入到此平台來管理,並通過此平台統一進行權限管理;
我們使用開源系統越多,那么需要管理的賬號就會越多,如果團隊比較大的話,會增加非常大的管理成本。
多環境管理
配置中心的部署比較靈活,支持多環境集中式管理。但是隨行付內部,為了隔離生產環境,我們分開部署了兩套配置中心,一套負責開發環境、測試環境、准生產環境的配置管理,另一套負責生產環境的配置管理。當然開發工程師可以選擇使用本地配置,不強制開發者環境與配置中心強關聯。(只要考慮開發人員眾多,需求同步進行)
配置設計
先回想一下:你有使用jar將配置共享給別人,或別人將提供給你帶配置的jar?答案是肯定的,這應該是開發中必須面對的問題,那么使用jar共享配置會帶來哪些問題呢?
容易造成沖突
之前為了統一日志的輸出格式,將logback.xml打成一個jar里,讓大家使用;而我去年在推新的logback配置規范時,發現與它發生沖突了。為了解決這個沖突,我們在每個項目中增加了個空的logbak.xml文件。
不方便修改。
需要與jar包提供方進行協調,還要確認修改是否對其它應用產生影響。
不能做差異化配置
比如有些項目為了復用數據庫操作部分代碼,將數據庫操作以及配置都放到單獨的模塊,以jar的形式進行復用,如果從復用的角度來看,是非常不錯的方法。
但是當系統發展到一定程度后,有些應用的並發量上來了,其數據庫連接池的配置就要與其它應用有差別,這時我們還是需要將配置從此模塊中拆出來。
通過上面的例子,可以發現配置之所以從代碼中提取出,其核心作用就是為了更好適應變化。因為共享配置存在以這些問題,而且微服務架構下,盡量還是以服務的方式來復用業務功能。再者我們一直要將代碼進行解偶,那么配置更需要進行解偶。
出於以上種種原因考慮,我們在設計配置中心時,也就沒有考慮設計以“組”的形式來共享配置。這也是我們設計時爭議比較大的地方。
配置內容
分為應用配置和全局配置:
- 全局配置:是某一環境下所有應用共享的配置,比如公司的郵件服務配置;注冊中心地址、公司名稱、公司地址等,可能會變化,但普遍性非常高的配置。
- 應用配置:每個應用個性化的配置;
為什么還要全局配置?這遇前面講的組共享配置不是沖突了嗎?
全局配置只是用於適應運行環境的變化而設計的,不設計到業務配置。“組”的界限不是很清楚,很容易亂,而全局配置不存在這方面的問題。
為什么單個應用只支持單個配置?
微服務已經拆得比較小了,其配置內容也不會非常多,所以只設計為一個應用只有一個配置。而且經過我們的實踐呢,一個配置是可以滿足實際需要的。
支持版本控制
我們的版本設計相比Git的,要比較簡單,但是相應的功能也還有的。主要職責如下:
- 配置每被修改一次,會將舊數據及版本號保存到日志表中,更新配置內容的同量,將版本號加一
- 支持版本比較功能:方便查看與最新版本的差異;檢查在哪天做了什么調整;
- 支持回退功能:如果配置出現問題,可以快速回退;
修改配置
不管是在內部推廣時,還是開源后,都有人問能支持properties嗎?其時最初版本是支持的,但我們在前端頁面把這個功能屏蔽了,因為我們決定只支持yaml格式。
- 1.properties 對中文支持不是好,而yml卻沒有這個問題;
- 2.yaml能很好管理同類項配置,避免配置重復key。看過不少properties文件,配置雜亂以及同一個文件出重相同的key,不同value的情況;不是所有的開發都是有強迫症;
- 3.統一大家的習慣;
當Yml也不是完全沒有問題的,在實踐過程中,偶爾也出現有人把縮進搞錯的情況。
使用Yml在線編輯器,可以非常方便編輯,比如:復制粘貼內容,就像在修改配置文件一樣,尤其是批量修改時更為方便。不像其它通過key value方便管理的配置中心,每次修改都需要先找到相應的key才能進行一個個修改,非常費時費力;
Yml的JSON預覽功能。當用戶編輯內容時,會實時檢查格式是否符合yaml格式時,如果格式是正確的,右則會正確顯示其對應的json內容,如果格式不正確則,右則會提示相應的錯誤信息,能及時發現錯誤。
實例基本信息及批量刷新
不停機實時刷新配置是配置中心的核心需求之一。比如在生產中運行的應用,突然因需求或性能等原因,需要調整配置,如果我們還需要經過修改代碼,重新打包,測試並部署等一系列的操作步驟的話,那效率可想可知,因此帶來的損失也可能會非常之大。ConfigKeeper使用Spring Cloud提供的RefreshEndpoint刷新配置,在最初的版本中,我們是通過curl或Postman等工具實現此功能,但這樣操作效率比較差,為此在最新版本中增加了如下功能:
在此頁面,我們實現如下功能:
- 1.列出所有應用實例的IP、管理端口等信息
- 2.查看應用中配置的版本是否是最新的;(非常方便核對應用版本是否是最新的;避免漏操作等問題;)
- 3.實現灰度發布;(可以手動刷新選中的一個或多個實例的配置;)
客戶端實現
因為隨行付從Spring boot 1.2.2版本就開始使用Spring boot,到現在已經實現所有應用boot化,所以我們在設計配置中心時,其客戶端必須要無縫兼容Spring boot、Spring cloud應用,所以我們就參考Spring cloud config的實現。
無縫兼容Spring boot、Spring cloud應用
為什么ConfigKeeper能實現無縫兼容Spring boot、Spring cloud應用?其原因非常簡單,因為核心實現還是由Spring cloud提供的,我們只是在對Spring cloud進行擴展,而不是在其基礎上重新造輪子。
- 只依賴 spring-cloud-context 和 spring-cloud-commons 兩個jar;
- Spring cloud 提供PropertySourceLocator接口,方便我們去加載外部配置,ConfigKeeper的客戶端核心代碼就是實現此接口;
客戶端源碼解析
要想學習客戶端的源碼的話,可能以/META-INF/spring.factories文件為入口,此文件中有如下配置:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.suixingpay.config.client.SxfConfigServiceBootstrapConfiguration
而SxfConfigServiceBootstrapConfiguration存在如下代碼:
@Bean
@ConditionalOnMissingBean(SxfConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "suixingpay.config.enabled", matchIfMissing = true)
public SxfConfigServicePropertySourceLocator sxfConfigServicePropertySource(ApplicationContext context) {
SxfConfigClientProperties configClientProperties = sxfConfigClientProperties(context);
ConfigDAO configDAO = sxfConfigDAO(configClientProperties);
return new SxfConfigServicePropertySourceLocator(configDAO, configClientProperties);
}
而SxfConfigServicePropertySourceLocator其實就是PropertySourceLocator的實現類,其具體實現請大家查看源碼文件。
客戶端特性
- 支持客戶端負載:如果有多個配置中心服務器實例,可以通過簡單的輪詢實現客戶端負載,達到高可能的效果。當然也可以使用nginx 反向代理實現服務端負載。
- 支持失敗后重試功能;
- 支持本地緩存
- 客戶端從配置中心拉取最新配置后,會緩存到本地磁盤。每次去拉取配置之前,會加載本地緩存配置的版本信息,前傳到服務端,如果服務端與客戶端的版本一致時,接口會返回304狀態,並使用本地緩存進行啟動應用,當服務端與客戶端的版本不一致時,會返回最新版本,並緩存到本地磁盤中。通過此緩存機制,一方面可以降低網絡帶寬,二是即使配置中心不可用,也不會影響應用的啟動。
- 上報應用實例信息
使用建議
配置治理
在我們實踐后發現,使用配置中心,還可以很好地對配置進行治理,比如統一使用YAML格式配置,使用配置內容更加清晰;避免了使用jar來共享配置帶來的一系列問題等等。但Spring boot、Spring cloud應用可加載的配置源非常之多,還需要注意一些問題。
- Command line arguments.
- Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property)
- ServletConfig init parameters.
- ServletContext init parameters.
- JNDI attributes from java:comp/env.
- Java System properties (System.getProperties()).
- OS environment variables.
- A RandomValuePropertySource that only has properties in random.*.
- Profile-specific bootstrap properties outside of your packaged jar (bootstrap-{profile}.properties and YAML variants)
- Profile-specific bootstrap properties packaged inside your jar (bootstrap-{profile}.properties and YAML variants)
- Bootstrap properties outside of your packaged jar (bootstrap.properties and YAML variants).
- Bootstrap properties packaged inside your jar (bootstrap.properties and YAML variants).
- Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants)
- Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants)
- Application properties outside of your packaged jar (application.properties and YAML variants).
- Application properties packaged inside your jar (application.properties and YAML variants).
- 通過 PropertySourceLocator 加載配置(應用配置優先級要高於全局配置)
- @PropertySource annotations on your @Configuration classes.
- Default properties (specified using SpringApplication.setDefaultProperties).
從上面內容可見,Spring boot是支持非常多種方式加載配置的,而且支持重復配置以及支持覆蓋,即相同key的配置,先加載的內容會被后加載的覆蓋,為了方便后期維護,盡量遵守以下原則:
- 盡量避免同一key在多個地方配置的情況;
- 如果第1種情況不可避免,那么要注意各個配置中的優化級,比如ConfigKeeper中全局配置的優先級要低於應用配置;
- 約定配置位置
可配置的比較那么多,在團隊中每個人使用的方法不一樣,拋必造成混亂,所以需要大家提前做好約定,比如:哪些配置通過命令行來配置,那些配置放到bootstrap 文件中,那些放到application 文件中。 - 拒絕使用jar共享配置
是不是所有的配置都可以通過配置中心來實時刷新?
相信很多人都會有這樣的誤區:所有的配置都是可以通過配置中心來實時刷新,不然配置中心的就沒有多大意義了。為了解答這個問題,我先來看RefreshEndpoint都做了哪些事情:
public synchronized Set<String> refresh() {
Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
// 加載最新配置到Environment
addConfigFilesToEnvironment();
Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
// 發送EnvironmentChangeEvent
this.context.publishEvent(new EnvironmentChangeEvent(context, keys));
// 清空RefreshScope緩存
this.scope.refreshAll();
return keys;
}
通過上面的源碼,我們可以看出其RefreshEndpoint主要做了三件事情:
- 加載最新配置到Environment
- 發送EnvironmentChangeEvent
- 清空RefreshScope緩存
所以我們要想獲取最新配置配置,可以通過以下途徑:
-
直接通過Environment獲取,比如:
String applicationName = environment.getProperty("spring.application.name");
-
處理EnvironmentChangeEvent,比如對於線程池大小的調整,我們可以監聽EnvironmentChangeEvent,當接收到EnvironmentChangeEvent時,關閉原來的線程池,前重新實例化新的線程池;
Spring boot官方建議我們盡量我們使用@ConfigurationProperties管理配置,那么它是否能自動刷新配置呢?其實它是可以的,因為在ConfigurationPropertiesRebinder中會監聽EnvironmentChangeEvent,詳細內容請查看org.springframework.cloud.context.properties. ConfigurationPropertiesRebinder。
-
在實例化bean時增加@RefreshScope, 比如:
@Autowired private DefaultUserProperties userProperties; @RefreshScope // 支持動態刷新 @Bean(name="defaultUser") public UserDO defaultUser() { UserDO userDO=new UserDO(); userDO.setId(userProperties.getId()); userDO.setName(userProperties.getName()); return userDO; }
Spring cloud 為了實現運行時動態刷新,增加了RefreshScope(org.springframework.cloud.context.scope.refresh.RefreshScope類),會將加了@RefreshScope的bean放入RefreshScope中,當刷新RefreshScope時,會清空緩存,當下次使用這些bean時會重新實例些這些bean。
安全提示
通過RefreshEndpoint 刷新的話,就需要開啟Spring boot Endpoint相關功能,而Spring boot Endpoint如果不做特殊處理的話,很容易被探測到,引發一些安全問題。比如:
server:
port: 8080
management:
security:
enabled: false
那么很容易去調用Spring boot Endpoint。生產環境的應用,安全問題不可忽視,所以建議做如下處理:
- management.port 與 server.port 設置不同的值,並且此端口不允許外網訪問;
- 增加安全驗證;
- 修改management.context-path
- 生產環境的management相關配置,盡量與其它環境的配置要有差異,不能完全一樣。
調整后的配置實例如下:
server:
port: 8080
management:
security:
enabled: true
context-path: /_ops
port: 9098
security:
basic:
enabled: true
path: ${management.context-path}/**, /swagger-ui.html, /v2/api-docs, /druid/**
user:
name: ma
password: xxxxxx
開源地址
Spring 生態功能非常豐富,為我們解決了非常多棘手問題,但很多東西要進行本地化開發后才能更好的使用。配置中心使用了不少開源技術,給我們帶來了不少便利,希望通過此開源項目回饋社區,為開源社區貢獻綿薄之力。