1. 背景介紹
隨着微服務的流行,應用和機器數量急劇增長,程序配置也愈加繁雜:各種功能的開關、參數的配置、服務器的地址等等。 同時,我們對程序配置的期望值也越來越高:配置修改後實時生效,灰度發佈,分環境、分集羣管理,完善的權限、審覈機制等等。
在這樣的大環境下,傳統的通過配置文件、數據庫等方式已經越來越無法滿足我們對配置管理的需求。
配置中心,應運而生!
通過配置中心,我們可以方便地管理微服務在不同環境中的配置,從而可以在運行時動態調整服務行爲,真正實現配置即『控制』的目標。 所以,在一定程度上,配置中心就成爲了微服務的大腦,如何用好這個大腦,讓微服務更『智能』,也就成爲了一項比較重要的議題。
2. 爲什麼需要配置中心?
2.1 配置即『控制』
程序的發佈其實和衛星的發射有一些相似之處。
當衛星發射昇天後,基本就處於自主駕駛狀態了,一般情況下就是按照預設的軌道運行,間歇可能會收到一些來自地面的『控制』信號對運行姿態進行一定的調整。
圖片來源
程序發佈其實也是這樣,當程序發佈到生產環境後,一般就是按照預設的邏輯運行,我們無法直接去幹預程序的行爲,不過可以通過調整配置參數來動態調整程序的行爲。這些配置參數就代表着我們對程序的『控制』信號。
由此可見,配置對程序的運行非常重要,我們需要一種可靠性高、實時性好的手段,從而可以隨時對程序『發號施令』。
2.2 配置需要治理
鑑於配置對程序正確運行的重要性,所以配置的治理就顯得尤爲重要了,比如:
- 權限控制、審計日誌
- 由於配置能改變程序的行爲,不正確的配置甚至能引起災難,所以對配置的修改必須有比較完善的權限控制。同時也需要有一套完善的審計機制,能夠方便地追溯是誰改的配置、改了什麼、什麼時候改的等等。
- 灰度發佈、配置回滾
- 對於一些比較重要的配置變更,我們一般會傾向於先在少量機器上修改看看效果,如果沒問題再推給所有機器。同時如果發現配置改得有問題的話,需要能夠方便地回滾配置。
- 不同環境、集羣管理
- 同一份程序在不同的環境(開發,測試,生產)、不同的集羣(如不同的數據中心)經常需要有不同的配置,所以需要有完善的環境、集羣配置管理。
2.3 微服務的複雜性
單體應用時代,應用數量比較少,配置也相對比較簡單,還有可能讓運維登上機器一臺一臺修改程序的配置文件。
隨着微服務的流行,大應用拆成小應用,小應用拆成多個獨立的服務,導致微服務的節點數量非常多,配置也隨着服務數量增加而急劇增長,再讓運維登上機器一臺一臺手工修改配置不僅效率低,而且還容易出錯。如果碰到了緊急事件需要大規模迅速修改配置,估計運維人員也只能兩手一攤了。
圖片來源
所以,綜合以上幾個要素,我們需要一個統一的配置中心來管理微服務的配置。
3. 配置中心的一般模樣
那麼,我們應該需要一個什麼樣的配置中心呢?
接下來就以開源配置中心Apollo爲例,來看一下配置中心的一般模樣。
3.1 治理能力
如前面所論述的:配置需要治理,所以配置中心需要具備完善的治理能力,比如:
- 統一管理不同環境、不同集羣的配置
- 支持灰度發佈
- 支持已發佈的配置回滾
- 完善的權限管理、操作審計日誌
Apollo配置中心的管理界面如下圖所示,可以發現相應的治理功能還是非常齊全的。
3.2 可用性
配置即『控制』,所以在一定程度上,配置中心已經成爲了微服務的大腦,作爲大腦,可用性顯然是要求非常高的。
接下來我們一起看一下Apollo是怎麼實現高可用的。
3.2.1 Apollo at a glance
如下即是Apollo的基礎模型:
- 用戶在配置中心對配置進行修改併發布
- 配置中心通知Apollo客戶端有配置更新
- Apollo客戶端從配置中心拉取最新的配置、更新本地配置並通知到應用
3.2.2 服務端高可用
上圖簡要描述了Apollo的服務端設計,我們可以從下往上看:
- 首先最下面是一個DB,我們的配置是放在DB裏的,然後在DB之上有兩個服務:Config Service和Admin Service
- Config Service提供配置的讀取、推送等功能,服務對象是Apollo客戶端
- Admin Service提供配置的修改、發佈等功能,服務對象是Apollo Portal(管理界面)
- Config Service和Admin Service都是多實例、無狀態部署,所以需要將自己註冊到Eureka中並保持心跳
- 在Eureka之上我們架了一層Meta Server用於封裝Eureka的服務發現接口,主要是爲了讓客戶端和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進程中
- 通過上述的設計,可以看到整個服務端是無單點,有效地保證了服務端的可用性
3.2.3 客戶端高可用
上圖簡要描述了Apollo客戶端的實現原理:
- 客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
- 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,爲了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率默認爲每5分鐘拉取一次,客戶端也可以通過在運行時指定System Property:
apollo.refreshInterval
來覆蓋,單位爲分鐘。
- 客戶端從Apollo配置中心服務端獲取到應用的最新配置後,會保存在內存中,所以我們的應用程序來獲取配置的時候其實始終是從內存中獲取的
- 客戶端還會把從服務端獲取到的配置在本地文件系統緩存一份
- 這主要是爲了容災,假設應用程序重啓的時候,恰好遠端服務全掛了,或者網絡有故障,應用程序依然能從本地恢復配置
- 通過這種推拉結合的機制,以及內存和本地文件雙緩存的方式,有效地保證了客戶端的可用性
3.2.4 可用性場景舉例
場景 | 影響 | 降級 | 原因 |
---|---|---|---|
某臺Config Service下線 | 無影響 | Config Service無狀態,客戶端重連其它Config Service | |
所有Config Service下線 | 客戶端無法讀取最新配置,Portal無影響 | 客戶端重啓時,可以讀取本地緩存配置文件。如果是新擴容的機器,可以從其它機器上獲取已緩存的配置文件 | |
某臺Admin Service下線 | 無影響 | Admin Service無狀態,Portal重連其它Admin Service | |
所有Admin Service下線 | 客戶端無影響,Portal無法更新配置 | ||
某臺Portal下線 | 無影響 | Portal域名通過SLB綁定多臺服務器,重試後指向可用的服務器 | |
全部Portal下線 | 客戶端無影響,Portal無法更新配置 | ||
某個數據中心下線 | 無影響 | 多數據中心部署,數據完全同步,Meta Server/Portal域名通過SLB自動切換到其它存活的數據中心 | |
數據庫全部宕機 | 客戶端無影響,Portal無法更新配置 | Config Service開啓配置緩存後,對配置的讀取不受數據庫宕機影響 |
3.3 實時性
配置即『控制』,所以我們希望我們的控制指令能迅速、準確地傳達到應用程序,我們來看看Apollo是如何實現實時性的。
上圖簡要描述了配置發佈的大致過程:
- 用戶在Portal操作配置發佈
- Portal調用Admin Service的接口操作發佈
- Admin Service發佈配置後,發送ReleaseMessage給各個Config Service
- Config Service收到ReleaseMessage後,通知對應的客戶端
3.3.1 發送ReleaseMessage的實現方式
Admin Service在配置發佈後,需要通知所有的Config Service有配置發佈,從而Config Service可以通知對應的客戶端來拉取最新的配置。
從概念上來看,這是一個典型的消息使用場景,Admin Service作爲producer發出消息,各個Config Service作爲consumer消費消息。通過一個消息組件(Message Queue)就能很好的實現Admin Service和Config Service的解耦。
在實現上,考慮到Apollo的實際使用場景,以及爲了儘可能減少外部依賴,我們沒有采用外部的消息中間件,而是通過數據庫實現了一個簡單的消息隊列。
實現方式如下:
- Admin Service在配置發佈後會往ReleaseMessage表插入一條消息記錄,消息內容就是配置發佈的AppId+Cluster+Namespace,參見DatabaseMessageSender
- Config Service有一個線程會每秒掃描一次ReleaseMessage表,看看是否有新的消息記錄,參見ReleaseMessageScanner
- Config Service如果發現有新的消息記錄,那麼就會通知到所有的消息監聽器(ReleaseMessageListener),如NotificationControllerV2,消息監聽器的註冊過程參見ConfigServiceAutoConfiguration
- NotificationControllerV2得到配置發佈的AppId+Cluster+Namespace後,會通知對應的客戶端
示意圖如下:
4. 如何讓微服務更『智能』?
接下來我們來看一下結合配置中心,我們能做哪些有趣的事情,讓微服務更智能。
4.1 開關
4.1.1 發佈開關
發佈開關一般用於發佈過程中,比如:
- 有些新功能依賴於其它系統的新接口,而其它系統的發佈週期未必和自己的系統一致,可以加個發佈開關,默認把該功能關閉,等依賴系統上線後再打開。
- 有些新功能有較大風險,可以加個發佈開關,上線後一旦有問題可以迅速關閉
需要注意的是,發佈開關應該是短暫存在的(1-2周),一旦功能穩定後需要及時清除開關代碼。
4.1.2 實驗開關
實驗開關通常用於對比測試或功能測試,比如:
- A/B測試
- 針對特定用戶應用新的推薦算法
- 針對特定百分比的用戶使用新的下單流程
圖片來源
- QA測試
- 有些重大功能已經對外宣稱在某年某日發佈
- 可以事先發到生產環境,只對內部用戶打開,測試沒問題後按時對全部用戶開放
圖片來源
實驗開關應該也是短暫存在的,一旦實驗結束了需要及時清除實驗開關代碼。
4.1.3 運維開關
運維開關通常用於提升系統穩定性,比如:
- 大促前可以把一些非關鍵功能關閉來提升系統容量
- 當系統出現問題時可以關閉非關鍵功能來保證核心功能正常工作
運維開關可能會長期存在,而且一般會涉及多個系統,所以需要提前規劃。
4.2 服務治理
4.2.1 限流
服務就像高速公路一樣,在正常情況下非常通暢,不過一旦流量突增(比如大促、遭受DDOS攻擊)時,如果沒有做好限流,就會導致系統整個被沖垮,所有用戶都無法訪問。
正常的高速公路
圖片來源
超出容量的高速公路
圖片來源
所以我們需要限流機制來應對此類問題,一般的做法是在網關或RPC框架層添加限流邏輯,結合配置中心的動態推送能力實現動態調整限流規則配置。
4.2.2 黑白名單
對於一些關鍵服務,哪怕是在內網環境中一般也會對調用方有所限制,比如:
- 有敏感信息的服務可以通過配置白名單來限制只有某些應用或IP才能調用
- 某個調用方代碼有問題導致超大量調用,對服務穩定性產生了影響,可以通過配置黑名單來暫時屏蔽這個調用方或IP
一般的做法是在RPC框架層添加校驗邏輯,結合配置中心的動態推送能力來實現動態調整黑白名單配置。
4.3 數據庫遷移
數據庫的遷移也是挺普遍的,比如:原來使用的SQL Server,現在需要遷移到MySQL,這種情況就可以結合配置中心來實現平滑遷移:
- 單寫SQL Server,100%讀SQL Server
- 初始化MySQL
- 雙寫SQL Server和MySQL,100%讀SQL Server
- 線下校驗、補齊MySQL數據
- 雙寫SQL Server和MySQL,90%讀SQL Server,10%讀MySQL
- 雙寫SQL Server和MySQL,100%讀MySQL
- 單寫MySQL,100%讀MySQL
- 切換完成
上述的讀寫開關和比例配置都可以通過配置中心實現動態調整。
4.4 動態日誌級別
服務運行過程中,經常會遇到需要通過日誌來排查定位問題的情況,然而這裏卻有個兩難:
- 如果日誌級別很高(如:ERROR),可能對排查問題也不會有太大幫助
- 如果日誌級別很低(如:DEBUG),日常運行會帶來非常大的日誌量,造成系統性能下降
爲了兼顧性能和排查問題,我們可以藉助於日誌組件和配置中心實現日誌級別動態調整。
以Spring Boot和Apollo結合爲例:
@ApolloConfigChangeListener private void onChange(ConfigChangeEvent changeEvent) { refreshLoggingLevels(changeEvent.changedKeys()); } private void refreshLoggingLevels(Set<String> changedKeys) { boolean loggingLevelChanged = false; for (String changedKey : changedKeys) { if (changedKey.startsWith("logging.level.")) { loggingLevelChanged = true; break; } } if (loggingLevelChanged) { // refresh logging levels this.applicationContext.publishEvent(new EnvironmentChangeEvent(changedKeys)); } }
詳細樣例代碼可以參考:https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-logger
4.5 動態網關路由
網關的核心功能之一就是路由轉發,而其中的路由信息也是經常會需要變化的,我們也可以結合配置中心實現動態更新路由信息。
以Spring Cloud Zuul和Apollo結合爲例:
@ApolloConfigChangeListener public void onChange(ConfigChangeEvent changeEvent) { boolean zuulPropertiesChanged = false; for (String changedKey : changeEvent.changedKeys()) { if (changedKey.startsWith("zuul.")) { zuulPropertiesChanged = true; break; } } if (zuulPropertiesChanged) { refreshZuulProperties(changeEvent); } } private void refreshZuulProperties(ConfigChangeEvent changeEvent) { // rebind configuration beans, e.g. ZuulProperties this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); // refresh routes this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator)); }
詳細樣例代碼可以參考:https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-cloud-zuul
4.6 動態數據源
數據庫是應用運行過程中的一個非常重要的資源,承擔了非常重要的角色。
在運行過程中,我們會遇到各種不同的場景需要讓應用程序切換數據庫連接,比如:數據庫維護、數據庫宕機主從切換等。
切換過程如下圖所示:
以Spring Boot和Apollo結合爲例:
@Configurationpublic class RefreshableDataSourceConfiguration { @Bean public DynamicDataSource dataSource(DataSourceManager dataSourceManager) { DataSource actualDataSource = dataSourceManager.createDataSource(); return new DynamicDataSource(actualDataSource); }}
public class DynamicDataSource implements DataSource { private final AtomicReference<DataSource> dataSourceAtomicReference; public DynamicDataSource(DataSource dataSource) { dataSourceAtomicReference = new AtomicReference<>(dataSource); } // set the new data source and return the previous one public DataSource setDataSource(DataSource newDataSource){ return dataSourceAtomicReference.getAndSet(newDataSource); } @Override public Connection getConnection() throws SQLException { return dataSourceAtomicReference.get().getConnection(); } ...}
@ApolloConfigChangeListener public void onChange(ConfigChangeEvent changeEvent) { boolean dataSourceConfigChanged = false; for (String changedKey : changeEvent.changedKeys()) { if (changedKey.startsWith("spring.datasource.")) { dataSourceConfigChanged = true; break; } } if (dataSourceConfigChanged) { refreshDataSource(changeEvent.changedKeys()); } } private synchronized void refreshDataSource(Set<String> changedKeys) { try { // rebind configuration beans, e.g. DataSourceProperties this.applicationContext.publishEvent(new EnvironmentChangeEvent(changedKeys)); DataSource newDataSource = dataSourceManager.createAndTestDataSource(); DataSource oldDataSource = dynamicDataSource.setDataSource(newDataSource); asyncTerminate(oldDataSource); } catch (Throwable ex) { logger.error("Refreshing data source failed", ex); } }
詳細樣例代碼可以參考:https://github.com/ctripcorp/apollo-use-cases/tree/master/dynamic-datasource
5. 最佳實踐
5.1 公共組件的配置
公共組件是指那些發佈給其它應用使用的客戶端代碼,比如RPC客戶端、DAL客戶端等。
這類組件一般是由單獨的團隊(如中間件團隊)開發、維護,但是運行時是在業務實際應用內的,所以本質上可以認爲是應用的一部分。
這類組件的特殊之處在於大部分的應用都會直接使用中間件團隊提供的默認值,少部分的應用需要根據自己的實際情況對默認值進行調整。
比如數據庫連接池的最小空閒連接數量(minimumIdle),出於對數據庫資源的保護,DBA要求將全公司默認的minimumIdle設爲1,對大部分的應用可能都適用,不過有些核心/高流量應用可能覺得太小,需要設爲10。
針對這種情況,可以藉助於Apollo提供的Namespace實現:
1.中間件團隊創建一個名爲dal
的公共Namespace,設置全公司的數據庫連接池默認配置
minimumIdle = 1maximumPoolSize = 20
2.dal組件的代碼會讀取dal
公共Namespace的配置
3.對大部分的應用由於默認配置已經適用,所以不用做任何事情
4.對於少量核心/高流量應用如果需要調整minimumIdle的值,只需要關聯dal
公共Namespace,然後對需要覆蓋的配置做調整即可,調整後的配置僅對該應用自己生效
minimumIdle = 10
通過這種方式的好處是不管是中間件團隊,還是應用開發,都可以靈活地動態調整公共組件的配置。
5.2 灰度發佈
對於重要的配置一定要做灰度發佈,先在一臺或多臺機器上生效後觀察效果,如果沒有問題再推給所有的機器。
對於公共組件的配置,建議先在一個或多個應用上生效後觀察效果,沒有問題再推給所有的應用。
圖片來源
5.3 發佈審覈
生產環境建議啓用發佈審覈功能,簡單而言就是如果某個人修改了配置,那麼必須由另一個人審覈後纔可以發佈,以避免由於頭腦不清醒、手一抖之類的造成生產事故。
圖片來源
6. 結語
本文主要介紹了以下幾方面:
- 爲什麼需要配置中心?
- 配置即『控制』
- 配置需要治理
- 微服務帶來的配置複雜性
- 配置中心的一般模樣
- 以Apollo爲例子,介紹了配置中心所具備的特徵
- 介紹了Apollo是如何實現高可用和實時性的
- 如何讓微服務更『智能』?
- 通過幾個案例,分享瞭如何藉助於配置中心使微服務更『智能』
- 配置中心的最佳實踐
- 公共組件的配置
- 灰度發佈
- 發佈審覈
最後,希望大家在平時工作中都能用好配置中心,更好地服務於業務場景,使微服務更『智能』,實現從青銅到王者的跨越!