簡單的整理了一下配置中心的基本概念,主要是為了方便記住,沒啥營養。
什么是配置中心
配置中心將配置從應用中剝離出來,統一管理,優雅的解決了配置的動態變更、持久化、運維成本等問題。應用自身既不需要去添加管理配置接口,也不需要自己去實現配置的持久化,更不需要引入“定時任務”以便降低運維成本。總得來說,配置中心就是一種統一管理各種應用配置的基礎服務組件。
- 配置項容易讀取和修改
- 添加新配置簡單直接
- 支持對配置的修改的檢視以把控風險
- 可以查看配置修改的歷史
- 不同部署環境支持隔離
從配置中心角度來看,性能方面Nacos的讀寫性能最高,Apollo次之,Spring Cloud Config依賴Git場景不適合開放的大規模自動化運維API。功能方面Apollo最為完善,nacos具有Apollo大部分配置管理功能,而Spring CloudConfig不帶運維管理界面,需要自行開發。Nacos的一大優勢是整合了注冊中心、配置中心功能,部署和操作相比Apollo都要直觀簡單,因此它簡化了架構復雜度,並減輕運維及部署工作。
Apollo
Apollo(阿波羅)是攜程框架部門研發的分布式配置中心,能夠集中化管理應用不同環境、不同集群的配置,配置修改后能夠實時推送到應用端,並且具備規范的權限、流程治理等特性,適用於微服務配置管理場景。
- 統一管理不同環境、不同集群的配置
- 配置修改實時生效(熱發布)
- 版本發布管理
- 灰度發布
- 權限管理、發布審核、操作審計
- 客戶端配置信息監控
- 提供 Java和.Net原生客戶端
- 提供開放平台 API

上圖簡要描述了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進程中
各模塊概要介紹
Config Service
- 提供配置獲取接口
- 提供配置更新推送接口(基於Http long polling)
- 服務端使用Spring DeferredResult實現異步化,從而大大增加長連接數量
- 目前使用的tomcat embed默認配置是最多10000個連接(可以調整),使用了4C8G的虛擬機實測可以支撐10000個連接,所以滿足需求(一個應用實例只會發起一個長連接)。
- 接口服務對象為Apollo客戶端
Admin Service
- 提供配置管理接口
- 提供配置修改、發布等接口
- 接口服務對象為Portal
Meta Server
- Portal通過域名訪問Meta Server獲取Admin Service服務列表(IP+Port)
- Client通過域名訪問Meta Server獲取Config Service服務列表(IP+Port)
- Meta Server從Eureka獲取Config Service和Admin Service的服務信息,相當於是一個Eureka Client
- 增設一個Meta Server的角色主要是為了封裝服務發現的細節,對Portal和Client而言,永遠通過一個Http接口獲取Admin Service和Config Service的服務信息,而不需要關心背后實際的服務注冊和發現組件
- Meta Server只是一個邏輯角色,在部署時和Config Service是在一個JVM進程中的,所以IP、端口和Config Service一致
Eureka
- 基於Eureka和Spring Cloud Netflix提供服務注冊和發現
- Config Service和Admin Service會向Eureka注冊服務,並保持心跳
- 為了簡單起見,目前Eureka在部署時和Config Service是在一個JVM進程中的(通過Spring Cloud Netflix)
Portal
- 提供Web界面供用戶管理配置
- 通過Meta Server獲取Admin Service服務列表(IP+Port),通過IP+Port訪問服務
- 在Portal側做load balance、錯誤重試
Client
-
Apollo提供的客戶端程序,為應用提供配置獲取、實時更新等功能
-
通過Meta Server獲取Config Service服務列表(IP+Port),通過IP+Port訪問服務
-
在Client側做load balance、錯誤重試
為什么我們采用Eureka作為服務注冊中心,而不是使用傳統的zk、etcd呢?我大致總結了一下,有以下幾方面的原因:
- 它提供了完整的Service Registry和Service Discovery實現
- 首先是提供了完整的實現,並且也經受住了Netflix自己的生產環境考驗,相對使用起來會比較省心。
- 和Spring Cloud無縫集成
- 我們的項目本身就使用了Spring Cloud和Spring Boot,同時Spring Cloud還有一套非常完善的開源代碼來整合Eureka,所以使用起來非常方便。
- 另外,Eureka還支持在我們應用自身的容器中啟動,也就是說我們的應用啟動完之后,既充當了Eureka的角色,同時也是服務的提供者。這樣就極大的提高了服務的可用性。
- 這一點是我們選擇Eureka而不是zk、etcd等的主要原因,為了提高配置中心的可用性和降低部署復雜度,我們需要盡可能地減少外部依賴。
- Open Source
- 最后一點是開源,由於代碼是開源的,所以非常便於我們了解它的實現原理和排查問題。
服務端設計
配置發布后的實時推送設計
在配置中心中,一個重要的功能就是配置發布后實時推送到客戶端。下面我們簡要看一下這塊是怎么設計實現的。
上圖簡要描述了配置發布的大致過程:
- 用戶在Portal操作配置發布
- Portal調用Admin Service的接口操作發布
- Admin Service發布配置后,發送ReleaseMessage給各個Config Service
- Config Service收到ReleaseMessage后,通知對應的客戶端
發送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。
- Config Service有一個線程會每秒掃描一次ReleaseMessage表,看看是否有新的消息記錄,參見ReleaseMessageScanner
- Config Service如果發現有新的消息記錄,那么就會通知到所有的消息監聽器(ReleaseMessageListener),如NotificationControllerV2,消息監聽器的注冊過程參見ConfigServiceAutoConfiguration
- NotificationControllerV2得到配置發布的AppId+Cluster+Namespace后,會通知對應的客戶端
Config Service通知客戶端的實現方式
上一節中簡要描述了NotificationControllerV2是如何得知有配置發布的,那NotificationControllerV2在得知有配置發布后是如何通知到客戶端的呢?
實現方式如下:
- 客戶端會發起一個Http請求到Config Service的
notifications/v2接口,也就是NotificationControllerV2,參見RemoteConfigLongPollService - NotificationControllerV2不會立即返回結果,而是通過Spring DeferredResult把請求掛起
- 如果在60秒內沒有該客戶端關心的配置發布,那么會返回Http狀態碼304給客戶端
- 如果有該客戶端關心的配置發布,NotificationControllerV2會調用DeferredResult的setResult方法,傳入有配置變化的namespace信息,同時該請求會立即返回。客戶端從返回的結果中獲取到配置變化的namespace后,會立即請求Config Service獲取該namespace的最新配置。
客戶端設計

Apollo客戶端的實現原理:
- 客戶端和服務端保持了一個長連接,從而能第一時間獲得配置更新的推送。(通過Http Long Polling實現)
- 客戶端還會定時從Apollo配置中心服務端拉取應用的最新配置。
- 這是一個fallback機制,為了防止推送機制失效導致配置不更新
- 客戶端定時拉取會上報本地版本,所以一般情況下,對於定時拉取的操作,服務端都會返回304 - Not Modified
- 定時頻率默認為每5分鍾拉取一次,客戶端也可以通過在運行時指定System Property:
apollo.refreshInterval來覆蓋,單位為分鍾。
- 客戶端從Apollo配置中心服務端獲取到應用的最新配置后,會保存在內存中
- 客戶端會把從服務端獲取到的配置在本地文件系統緩存一份
- 在遇到服務不可用,或網絡不通的時候,依然能從本地恢復配置
- 應用程序可以從Apollo客戶端獲取最新的配置、訂閱配置更新通知
Nacos
Nacos 的關鍵特性包括:
-
服務發現和服務健康監測
Nacos 支持基於 DNS 和基於 RPC 的服務發現。服務提供者使用 原生SDK、OpenAPI、或一個獨立的Agent TODO注冊 Service 后,服務消費者可以使用DNS TODO 或HTTP&API查找和發現服務。
Nacos 提供對服務的實時的健康檢查,阻止向不健康的主機或服務實例發送請求。
-
動態配置服務
動態配置服務可以讓您以中心化、外部化和動態化的方式管理所有環境的應用配置和服務配置。
動態配置消除了配置變更時重新部署應用和服務的需要,讓配置管理變得更加高效和敏捷。
-
動態 DNS 服務
動態 DNS 服務支持權重路由,讓您更容易地實現中間層負載均衡、更靈活的路由策略、流量控制以及數據中心內網的簡單DNS解析服務。
-
服務及其元數據管理
Nacos 能讓您從微服務平台建設的視角管理數據中心的所有服務及元數據,包括管理服務的描述、生命周期、服務的靜態依賴分析、服務的健康狀態、服務的流量管理、路由及安全策略、服務的 SLA 以及最首要的 metrics 統計數據。
Nacos的配置中心可以分為獲取配置,監聽配置,發布配置,刪除配置,其中配置的獲取刪除發布相對簡單,堆服務端來說,無非就是怎么存儲配置。是否要持久化,對客戶端來說,就是通過接口從服務器端查詢相應的數據,然后返回即可。
而監聽的高壓,常見的交互方式就是pull和push。Nacos采用的就是pull模式,是一種長輪詢機制,它結合Push和pull兩者的優點。客戶端采用長輪詢的方式定時發起pull請求,去檢查服務端配置信息是否發生了變化,如果變化,就會獲取最新的配置。如果沒有變化,服務端就會“Hold”住這個請求,直到這段時間內配置發生變化。其實就是設置一個定時任務,延遲29.5s執行,並且把當前的客戶端長輪詢連接加入allSubs隊列,這樣降低了服務端的壓力。
推還是拉
現在我們了解了 Nacos 的配置管理的功能了,但是有一個問題我們需要弄明白,那就是 Nacos 客戶端是怎么實時獲取到 Nacos 服務端的最新數據的。
其實客戶端和服務端之間的數據交互,無外乎兩種情況:
- 服務端推數據給客戶端
- 客戶端從服務端拉數據
那到底是推還是拉呢,從 Nacos 客戶端通過 Listener 來接收最新數據的這個做法來看,感覺像是服務端推的數據,但是不能想當然,要想知道答案,最快最准確的方法就是從源碼中去尋找。
創建 ConfigService
從我們的 demo 中可以知道,首先是創建了一個 ConfigService。而 ConfigService 是通過 ConfigFactory 類創建的,如下圖所示:
可以看到實際是通過反射調用了 NacosConfigService 的構造方法來創建 ConfigService 的,而且是有一個 Properties 參數的構造方法。
需要注意的是,這里並沒有通過單例或者緩存技術,也就是說每次調用都會重新創建一個 ConfigService的實例。
實例化 ConfigService
現在我們來看下 NacosConfigService 的構造方法,看看 ConfigService 是怎么實例化的,如下圖所示:
實例化時主要是初始化了兩個對象,他們分別是:
- HttpAgent
- ClientWorker
HttpAgent
其中 agent 是通過裝飾着模式實現的,ServerHttpAgent 是實際工作的類,MetricsHttpAgent 在內部也是調用了 ServerHttpAgent 的方法,另外加上了一些統計操作,所以我們只需要關心 ServerHttpAgent 的功能就可以了。
agent 實際是在 ClientWorker 中發揮能力的,下面我們來看下 ClientWorker 類。
ClientWorker
以下是 ClientWorker 的構造方法,如下圖所示:
可以看到 ClientWorker 除了將 HttpAgent 維持在自己內部,還創建了兩個線程池:
第一個線程池是只擁有一個線程用來執行定時任務的 executor,executor 每隔 10ms 就會執行一次 checkConfigInfo() 方法,從方法名上可以知道是每 10 ms 檢查一次配置信息。
第二個線程池是一個普通的線程池,從 ThreadFactory 的名稱可以看到這個線程池是做長輪詢的。
現在讓我們來看下 executor 每 10ms 執行的方法到底是干什么的,如下圖所示:

可以看到,checkConfigInfo 方法是取出了一批任務,然后提交給 executorService 線程池去執行,執行的任務就是 LongPollingRunnable,每個任務都有一個 taskId。
現在我們來看看 LongPollingRunnable 做了什么,主要分為兩部分,第一部分是檢查本地的配置信息,第二部分是獲取服務端的配置信息然后更新到本地。
1.本地檢查
首先取出與該 taskId 相關的 CacheData,然后對 CacheData 進行檢查,包括本地配置檢查和監聽器的 md5 檢查,本地檢查主要是做一個故障容錯,當服務端掛掉后,Nacos 客戶端可以從本地的文件系統中獲取相關的配置信息,如下圖所示:

通過跟蹤 checkLocalConfig 方法,可以看到 Nacos 將配置信息保存在了~/nacos/config/fixed-{address}_8848_nacos/snapshot/DEFAULT_GROUP/{dataId}
這個文件中,我們看下這個文件中保存的內容,如下圖所示:

2.服務端檢查
然后通過 checkUpdateDataIds() 方法從服務端獲取那些值發生了變化的 dataId 列表,
通過 getServerConfig 方法,根據 dataId 到服務端獲取最新的配置信息,接着將最新的配置信息保存到 CacheData 中。
最后調用 CacheData 的 checkListenerMd5 方法,可以看到該方法在第一部分也被調用過,我們需要重點關注一下。
可以看到,在該任務的最后,也就是在 finally 中又重新通過 executorService 提交了本任務。
添加 Listener
好了現在我們可以為 ConfigService 來添加一個 Listener 了,最終是調用了 ClientWorker 的 addTenantListeners 方法,如下圖所示:

該方法分為兩個部分,首先根據 dataId,group 和當前的場景獲取一個 CacheData 對象,然后將當前要添加的 listener 對象添加到 CacheData 中去。
也就是說 listener 最終是被這里的 CacheData 所持有了,那 listener 的回調方法 receiveConfigInfo 就應該是在 CacheData 中觸發的。
我們發現 CacheData 是出現頻率非常高的一個類,在 LongPollingRunnable 的任務中,幾乎所有的方法都圍繞着 CacheData 類,現在添加 Listener 的時候,實際上該 Listener 也被委托給了 CacheData,那我們要重點關注下 CacheData 類了。
CacheData
首先讓我們來看一下 CacheData 中的成員變量,如下圖所示:
可以看到除了 dataId,group,content,taskId 這些跟配置相關的屬性,還有兩個比較重要的屬性:listeners、md5。
listeners 是該 CacheData 所關聯的所有 listener,不過不是保存的原始的 Listener 對象,而是包裝后的 ManagerListenerWrap 對象,該對象除了持有 Listener 對象,還持有了一個 lastCallMd5 屬性。
另外一個屬性 md5 就是根據當前對象的 content 計算出來的 md5 值。
觸發回調
現在我們對 ConfigService 有了大致的了解了,現在剩下最后一個重要的問題還沒有答案,那就是 ConfigService 的 Listener 是在什么時候觸發回調方法 receiveConfigInfo 的。
現在讓我們回過頭來想一下,在 ClientWorker 中的定時任務中,啟動了一個長輪詢的任務:LongPollingRunnable,該任務多次執行了 cacheData.checkListenerMd5() 方法,那現在就讓我們來看下這個方法到底做了些什么,如下圖所示:

到這里應該就比較清晰了,該方法會檢查 CacheData 當前的 md5 與 CacheData 持有的所有 Listener 中保存的 md5 的值是否一致,如果不一致,就執行一個安全的監聽器的通知方法:safeNotifyListener,通知什么呢?我們可以大膽的猜一下,應該是通知 Listener 的使用者,該 Listener 所關注的配置信息已經發生改變了。現在讓我們來看一下 safeNotifyListener 方法,如下圖所示:

可以看到在 safeNotifyListener 方法中,重點關注下紅框中的三行代碼:獲取最新的配置信息,調用 Listener 的回調方法,將最新的配置信息作為參數傳入,這樣 Listener 的使用者就能接收到變更后的配置信息了,最后更新 ListenerWrap 的 md5 值。和我們猜測的一樣, Listener 的回調方法就是在該方法中觸發的。
Md5何時變更
那 CacheData 的 md5 值是何時發生改變的呢?我們可以回想一下,在上面的 LongPollingRunnable 所執行的任務中,在獲取服務端發生變更的配置信息時,將最新的 content 數據寫入了 CacheData 中,我們可以看下該方法如下:

可以看到是在長輪詢的任務中,當服務端配置信息發生變更時,客戶端將最新的數據獲取下來之后,保存在了 CacheData 中,同時更新了該 CacheData 的 md5 值,所以當下次執行 checkListenerMd5 方法時,就會發現當前 listener 所持有的 md5 值已經和 CacheData 的 md5 值不一樣了,也就意味着服務端的配置信息發生改變了,這時就需要將最新的數據通知給 Listener 的持有者。
至此配置中心的完整流程已經分析完畢了,可以發現,Nacos 並不是通過推的方式將服務端最新的配置信息發送給客戶端的,而是客戶端維護了一個長輪詢的任務,定時去拉取發生變更的配置信息,然后將最新的數據推送給 Listener 的持有者。
拉的優勢
客戶端拉取服務端的數據與服務端推送數據給客戶端相比,優勢在哪呢,為什么 Nacos 不設計成主動推送數據,而是要客戶端去拉取呢?如果用推的方式,服務端需要維持與客戶端的長連接,這樣的話需要耗費大量的資源,並且還需要考慮連接的有效性,例如需要通過心跳來維持兩者之間的連接。而用拉的方式,客戶端只需要通過一個無狀態的 http 請求即可獲取到服務端的數據。
總結
Nacos 服務端創建了相關的配置項后,客戶端就可以進行監聽了。
客戶端是通過一個定時任務來檢查自己監聽的配置項的數據的,一旦服務端的數據發生變化時,客戶端將會獲取到最新的數據,並將最新的數據保存在一個 CacheData 對象中,然后會重新計算 CacheData 的 md5 屬性的值,此時就會對該 CacheData 所綁定的 Listener 觸發 receiveConfigInfo 回調。
考慮到服務端故障的問題,客戶端將最新數據獲取后會保存在本地的 snapshot 文件中,以后會優先從文件中獲取配置信息的值。
這邊參考:https://www.jianshu.com/p/38b5452c9fec,大部分和Spring Cloud Alibaba中一致。
Spring Cloud Config
Spring Cloud Config項目是一個解決分布式系統的配置管理方案。它包含了Client和Server兩個部分,server提供配置文件的存儲、以接口的形式將配置文件的內容提供出去,client通過接口獲取數據、並依據此數據初始化自己的應用。在微服務架構中,通常會使用輕量級的消息代理來構建一個共用的消息主題來連接各個微服務實例,它廣播的消息會被所有在注冊中心的微服務實例監聽和消費,也稱消息總線。SpringCloud中也有對應的解決方案,SpringCloud Bus 將分布式的節點用輕量的消息代理連接起來,可以很容易搭建消息總線,配合SpringCloud config 實現微服務應用配置信息的動態更新。
Spring Cloud Config為分布式系統中的外部配置提供服務器和客戶端支持。使用Config Server,您可以為所有環境中的應用程序管理其外部屬性。它非常適合spring應用,也可以使用在其他語言的應用上。隨着應用程序通過從開發到測試和生產的部署流程,您可以管理這些環境之間的配置,並確定應用程序具有遷移時需要運行的一切。服務器存儲后端的默認實現使用git,因此它輕松支持標簽版本的配置環境,以及可以訪問用於管理內容的各種工具。
Spring Cloud Config服務端特性:
- HTTP,為外部配置提供基於資源的API(鍵值對,或者等價的YAML內容)
- 屬性值的加密和解密(對稱加密和非對稱加密)
- 通過使用@EnableConfigServer在Spring boot應用中非常簡單的嵌入。
- 綁定Config服務端,並使用遠程的屬性源初始化Spring環境。
- 屬性值的加密和解密(對稱加密和非對稱加密)
