在微服務架構中,配置中心是必不可少的基礎服務。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 生態功能非常豐富,爲我們解決了非常多棘手問題,但很多東西要進行本地化開發後才能更好的使用。配置中心使用了不少開源技術,給我們帶來了不少便利,希望通過此開源項目回饋社區,爲開源社區貢獻綿薄之力。