在微服務架構中,配置中心是必不可少的基礎服務。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的功能差異:
除了上述之外,還有以下其他功能特性:
- 開發人員最習慣的就是在檔案中修改配置,管理頁面上提供「舒適」的富文字編輯框;
- 全域性配置約定,比如多個專案共享的配置,比如簡訊地址等採取約定大於配置。全域性配置<應用配置;
- 配置校驗,文字修改高亮對比修改內容,防止低階錯誤等;
架構設計
有史以來最簡單的配置中心。使用資料庫儲存配置是因為微服務拆分粒度相對比較細,使用的配置也會相對比較少,所以使用資料庫表就夠儲存,流程如下:
-
- 使用者先去配置中心 新增、修改配置;
-
- 應用啟動時:(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](https://my.oschina.net/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應用可載入的配置源非常之多,還需要注意一些問題。
下面是擷取docs.spring.io/spring-boot…中的內容:
- 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 生態功能非常豐富,為我們解決了非常多棘手問題,但很多東西要進行本地化開發後才能更好的使用。配置中心使用了不少開源技術,給我們帶來了不少便利,希望通過此開源專案回饋社群,為開源社群貢獻綿薄之力。
ConfigKeeper QQ群:478814745
本分類文章,與「隨行付研究院」微訊號文章同步,第一時間接收公眾號推送,請關注「隨行付研究院」公眾號。
複製程式碼