動(dòng)態(tài)配置管理是 Nacos 的三大功能之一,通過(guò)動(dòng)態(tài)配置服務(wù),我們可以在所有環(huán)境中以集中和動(dòng)態(tài)的方式管理所有應(yīng)用程序或服務(wù)的配置信息。
動(dòng)態(tài)配置中心可以實(shí)現(xiàn)配置更新時(shí)無(wú)需重新部署應(yīng)用程序和服務(wù)即可使相應(yīng)的配置信息生效,這極大了增加了系統(tǒng)的運(yùn)維能力。
動(dòng)態(tài)配置
下面我將來(lái)和大家一起來(lái)了解下 Nacos 的動(dòng)態(tài)配置的能力,看看 Nacos 是如何以簡(jiǎn)單、優(yōu)雅、高效的方式管理配置,實(shí)現(xiàn)配置的動(dòng)態(tài)變更的。
我們用一個(gè)簡(jiǎn)單的例子來(lái)了解下 Nacos 的動(dòng)態(tài)配置的功能。
環(huán)境準(zhǔn)備
首先我們要準(zhǔn)備一個(gè) Nacos 的服務(wù)端,現(xiàn)在有兩種方式獲取 Nacos 的服務(wù)端:
- 1.通過(guò)源碼編譯
- 2.下載 Release 包
兩種方法可以獲得 Nacos 的可執(zhí)行程序,下面我用第一種方式通過(guò)源碼編譯一個(gè)可執(zhí)行程序,可能有人會(huì)問(wèn)為啥不直接下載 Release 包,還要自己去編譯呢?首先 Release 包也是通過(guò)源碼編譯得到的,其次我們通過(guò)自己編譯可以了解一些過(guò)程也有可能會(huì)碰到一些問(wèn)題,這些都是很重要的經(jīng)驗(yàn),好了那我們直接源碼編譯吧。
首先 fork 一份 nacos 的代碼到自己的 github 庫(kù),然后把代碼 clone 到本地。
然后在項(xiàng)目的根目錄下執(zhí)行以下命令(假設(shè)我們已經(jīng)配置好了 java 和 maven 環(huán)境):
mvn -Prelease-nacos clean install -U
執(zhí)行成功之后你將會(huì)看到如下圖所示的結(jié)果:
然后在項(xiàng)目的 distribution 目錄下我們就可以找到可執(zhí)行程序了,包括兩個(gè)壓縮包,這兩個(gè)壓縮包就是nacos 的 github 官網(wǎng)上發(fā)布的 Release 包。
接下來(lái)我們把編譯好的兩個(gè)壓縮包拷貝出來(lái),然后解壓出來(lái)直接使用,這樣就相當(dāng)于我們下載了 Release 包了。解壓后文件結(jié)構(gòu)和 nacos-server-0.8.0 一樣,我們直接執(zhí)行 startup.sh 即可啟動(dòng)一個(gè)單機(jī)的 Nacos 服務(wù)端了。
啟動(dòng)服務(wù)端
執(zhí)行下列命令來(lái)啟動(dòng)一個(gè) Nacos 服務(wù)端:
sh startup.sh -m standalone
啟動(dòng)完你將會(huì)看到如下圖所示的結(jié)果:
啟動(dòng)成功后,我們就可以訪問(wèn) Nacos 的控制臺(tái)了,如下圖所示:
控制臺(tái)做了簡(jiǎn)單的權(quán)限控制,默認(rèn)的賬號(hào)和密碼都是 nacos。
登錄進(jìn)去之后,是這樣的:
新建配置
接下來(lái)我們?cè)诳刂婆_(tái)上創(chuàng)建一個(gè)簡(jiǎn)單的配置項(xiàng),如下圖所示:
啟動(dòng)客戶端
當(dāng)服務(wù)端以及配置項(xiàng)都準(zhǔn)備好之后,就可以創(chuàng)建客戶端了,如下圖所示新建一個(gè) Nacos 的 ConfigService 來(lái)接收數(shù)據(jù):
執(zhí)行后將打印如下信息:
這里我用了一個(gè) System.in.read() 方法來(lái)監(jiān)聽(tīng)輸入的信息,主要是為了防止主線程退出,看不到后續(xù)的結(jié)果。
修改配置信息
接下來(lái)我們?cè)?Nacos 的控制臺(tái)上將我們的配置信息改為如下圖所示:
修改完配置,點(diǎn)擊 “發(fā)布” 按鈕后,客戶端將會(huì)收到最新的數(shù)據(jù),如下圖所示:
至此一個(gè)簡(jiǎn)單的動(dòng)態(tài)配置管理功能已經(jīng)講完了,刪除配置和更新配置操作類似,這里不再贅述。
適用場(chǎng)景
了解了動(dòng)態(tài)配置管理的效果之后,我們知道了大概的原理了,Nacos 服務(wù)端保存了配置信息,客戶端連接到服務(wù)端之后,根據(jù) dataID,group可以獲取到具體的配置信息,當(dāng)服務(wù)端的配置發(fā)生變更時(shí),客戶端會(huì)收到通知。當(dāng)客戶端拿到變更后的最新配置信息后,就可以做自己的處理了,這非常有用,所有需要使用配置的場(chǎng)景都可以通過(guò) Nacos 來(lái)進(jìn)行管理。
可以說(shuō) Nacos 有很多的適用場(chǎng)景,包括但不限于以下這些情況:
- 數(shù)據(jù)庫(kù)連接信息
- 限流規(guī)則和降級(jí)開(kāi)關(guān)
- 流量的動(dòng)態(tài)調(diào)度
看過(guò)我的 Sentinel 系列文章的同學(xué)可能知道,其中有一篇專門(mén)介紹集群限流環(huán)境搭建的文章,就是通過(guò) Nacos 來(lái)創(chuàng)建動(dòng)態(tài)規(guī)則的。
推還是拉
現(xiàn)在我們了解了 Nacos 的配置管理的功能了,但是有一個(gè)問(wèn)題我們需要弄明白,那就是 Nacos 客戶端是怎么實(shí)時(shí)獲取到 Nacos 服務(wù)端的最新數(shù)據(jù)的。
其實(shí)客戶端和服務(wù)端之間的數(shù)據(jù)交互,無(wú)外乎兩種情況:
- 服務(wù)端推數(shù)據(jù)給客戶端
- 客戶端從服務(wù)端拉數(shù)據(jù)
那到底是推還是拉呢,從 Nacos 客戶端通過(guò) Listener 來(lái)接收最新數(shù)據(jù)的這個(gè)做法來(lái)看,感覺(jué)像是服務(wù)端推的數(shù)據(jù),但是不能想當(dāng)然,要想知道答案,最快最準(zhǔn)確的方法就是從源碼中去尋找。
創(chuàng)建 ConfigService
從我們的 demo 中可以知道,首先是創(chuàng)建了一個(gè) ConfigService。而 ConfigService 是通過(guò) ConfigFactory 類創(chuàng)建的,如下圖所示:
可以看到實(shí)際是通過(guò)反射調(diào)用了 NacosConfigService 的構(gòu)造方法來(lái)創(chuàng)建 ConfigService 的,而且是有一個(gè) Properties 參數(shù)的構(gòu)造方法。
需要注意的是,這里并沒(méi)有通過(guò)單例或者緩存技術(shù),也就是說(shuō)每次調(diào)用都會(huì)重新創(chuàng)建一個(gè) ConfigService的實(shí)例。
實(shí)例化 ConfigService
現(xiàn)在我們來(lái)看下 NacosConfigService 的構(gòu)造方法,看看 ConfigService 是怎么實(shí)例化的,如下圖所示:
實(shí)例化時(shí)主要是初始化了兩個(gè)對(duì)象,他們分別是:
- HttpAgent
- ClientWorker
HttpAgent
其中 agent 是通過(guò)裝飾著模式實(shí)現(xiàn)的,ServerHttpAgent 是實(shí)際工作的類,MetricsHttpAgent 在內(nèi)部也是調(diào)用了 ServerHttpAgent 的方法,另外加上了一些統(tǒng)計(jì)操作,所以我們只需要關(guān)心 ServerHttpAgent 的功能就可以了。
agent 實(shí)際是在 ClientWorker 中發(fā)揮能力的,下面我們來(lái)看下 ClientWorker 類。
ClientWorker
以下是 ClientWorker 的構(gòu)造方法,如下圖所示:
可以看到 ClientWorker 除了將 HttpAgent 維持在自己內(nèi)部,還創(chuàng)建了兩個(gè)線程池:
第一個(gè)線程池是只擁有一個(gè)線程用來(lái)執(zhí)行定時(shí)任務(wù)的 executor,executor 每隔 10ms 就會(huì)執(zhí)行一次 checkConfigInfo() 方法,從方法名上可以知道是每 10 ms 檢查一次配置信息。
第二個(gè)線程池是一個(gè)普通的線程池,從 ThreadFactory 的名稱可以看到這個(gè)線程池是做長(zhǎng)輪詢的。
現(xiàn)在讓我們來(lái)看下 executor 每 10ms 執(zhí)行的方法到底是干什么的,如下圖所示:
可以看到,checkConfigInfo 方法是取出了一批任務(wù),然后提交給 executorService 線程池去執(zhí)行,執(zhí)行的任務(wù)就是 LongPollingRunnable,每個(gè)任務(wù)都有一個(gè) taskId。
現(xiàn)在我們來(lái)看看 LongPollingRunnable 做了什么,主要分為兩部分,第一部分是檢查本地的配置信息,第二部分是獲取服務(wù)端的配置信息然后更新到本地。
1.本地檢查
首先取出與該 taskId 相關(guān)的 CacheData,然后對(duì) CacheData 進(jìn)行檢查,包括本地配置檢查和監(jiān)聽(tīng)器的 md5 檢查,本地檢查主要是做一個(gè)故障容錯(cuò),當(dāng)服務(wù)端掛掉后,Nacos 客戶端可以從本地的文件系統(tǒng)中獲取相關(guān)的配置信息,如下圖所示:
通過(guò)跟蹤 checkLocalConfig 方法,可以看到 Nacos 將配置信息保存在了
~/nacos/config/fixed-{address}_8848_nacos/snapshot/DEFAULT_GROUP/{dataId}
這個(gè)文件中,我們看下這個(gè)文件中保存的內(nèi)容,如下圖所示:
2.服務(wù)端檢查
然后通過(guò) checkUpdateDataIds() 方法從服務(wù)端獲取那些值發(fā)生了變化的 dataId 列表,
通過(guò) getServerConfig 方法,根據(jù) dataId 到服務(wù)端獲取最新的配置信息,接著將最新的配置信息保存到 CacheData 中。
最后調(diào)用 CacheData 的 checkListenerMd5 方法,可以看到該方法在第一部分也被調(diào)用過(guò),我們需要重點(diǎn)關(guān)注一下。
可以看到,在該任務(wù)的最后,也就是在 finally 中又重新通過(guò) executorService 提交了本任務(wù)。
添加 Listener
好了現(xiàn)在我們可以為 ConfigService 來(lái)添加一個(gè) Listener 了,最終是調(diào)用了 ClientWorker 的 addTenantListeners 方法,如下圖所示:
該方法分為兩個(gè)部分,首先根據(jù) dataId,group 和當(dāng)前的場(chǎng)景獲取一個(gè) CacheData 對(duì)象,然后將當(dāng)前要添加的 listener 對(duì)象添加到 CacheData 中去。
也就是說(shuō) listener 最終是被這里的 CacheData 所持有了,那 listener 的回調(diào)方法 receiveConfigInfo 就應(yīng)該是在 CacheData 中觸發(fā)的。
我們發(fā)現(xiàn) CacheData 是出現(xiàn)頻率非常高的一個(gè)類,在 LongPollingRunnable 的任務(wù)中,幾乎所有的方法都圍繞著 CacheData 類,現(xiàn)在添加 Listener 的時(shí)候,實(shí)際上該 Listener 也被委托給了 CacheData,那我們要重點(diǎn)關(guān)注下 CacheData 類了。
CacheData
首先讓我們來(lái)看一下 CacheData 中的成員變量,如下圖所示:
可以看到除了 dataId,group,content,taskId 這些跟配置相關(guān)的屬性,還有兩個(gè)比較重要的屬性:listeners、md5。
listeners 是該 CacheData 所關(guān)聯(lián)的所有 listener,不過(guò)不是保存的原始的 Listener 對(duì)象,而是包裝后的 ManagerListenerWrap 對(duì)象,該對(duì)象除了持有 Listener 對(duì)象,還持有了一個(gè) lastCallMd5 屬性。
另外一個(gè)屬性 md5 就是根據(jù)當(dāng)前對(duì)象的 content 計(jì)算出來(lái)的 md5 值。
觸發(fā)回調(diào)
現(xiàn)在我們對(duì) ConfigService 有了大致的了解了,現(xiàn)在剩下最后一個(gè)重要的問(wèn)題還沒(méi)有答案,那就是 ConfigService 的 Listener 是在什么時(shí)候觸發(fā)回調(diào)方法 receiveConfigInfo 的。
現(xiàn)在讓我們回過(guò)頭來(lái)想一下,在 ClientWorker 中的定時(shí)任務(wù)中,啟動(dòng)了一個(gè)長(zhǎng)輪詢的任務(wù):LongPollingRunnable,該任務(wù)多次執(zhí)行了 cacheData.checkListenerMd5() 方法,那現(xiàn)在就讓我們來(lái)看下這個(gè)方法到底做了些什么,如下圖所示:
到這里應(yīng)該就比較清晰了,該方法會(huì)檢查 CacheData 當(dāng)前的 md5 與 CacheData 持有的所有 Listener 中保存的 md5 的值是否一致,如果不一致,就執(zhí)行一個(gè)安全的監(jiān)聽(tīng)器的通知方法:safeNotifyListener,通知什么呢?我們可以大膽的猜一下,應(yīng)該是通知 Listener 的使用者,該 Listener 所關(guān)注的配置信息已經(jīng)發(fā)生改變了?,F(xiàn)在讓我們來(lái)看一下 safeNotifyListener 方法,如下圖所示:
可以看到在 safeNotifyListener 方法中,重點(diǎn)關(guān)注下紅框中的三行代碼:獲取最新的配置信息,調(diào)用 Listener 的回調(diào)方法,將最新的配置信息作為參數(shù)傳入,這樣 Listener 的使用者就能接收到變更后的配置信息了,最后更新 ListenerWrap 的 md5 值。和我們猜測(cè)的一樣, Listener 的回調(diào)方法就是在該方法中觸發(fā)的。
Md5何時(shí)變更
那 CacheData 的 md5 值是何時(shí)發(fā)生改變的呢?我們可以回想一下,在上面的 LongPollingRunnable 所執(zhí)行的任務(wù)中,在獲取服務(wù)端發(fā)生變更的配置信息時(shí),將最新的 content 數(shù)據(jù)寫(xiě)入了 CacheData 中,我們可以看下該方法如下:
可以看到是在長(zhǎng)輪詢的任務(wù)中,當(dāng)服務(wù)端配置信息發(fā)生變更時(shí),客戶端將最新的數(shù)據(jù)獲取下來(lái)之后,保存在了 CacheData 中,同時(shí)更新了該 CacheData 的 md5 值,所以當(dāng)下次執(zhí)行 checkListenerMd5 方法時(shí),就會(huì)發(fā)現(xiàn)當(dāng)前 listener 所持有的 md5 值已經(jīng)和 CacheData 的 md5 值不一樣了,也就意味著服務(wù)端的配置信息發(fā)生改變了,這時(shí)就需要將最新的數(shù)據(jù)通知給 Listener 的持有者。
至此配置中心的完整流程已經(jīng)分析完畢了,可以發(fā)現(xiàn),Nacos 并不是通過(guò)推的方式將服務(wù)端最新的配置信息發(fā)送給客戶端的,而是客戶端維護(hù)了一個(gè)長(zhǎng)輪詢的任務(wù),定時(shí)去拉取發(fā)生變更的配置信息,然后將最新的數(shù)據(jù)推送給 Listener 的持有者。
拉的優(yōu)勢(shì)
客戶端拉取服務(wù)端的數(shù)據(jù)與服務(wù)端推送數(shù)據(jù)給客戶端相比,優(yōu)勢(shì)在哪呢,為什么 Nacos 不設(shè)計(jì)成主動(dòng)推送數(shù)據(jù),而是要客戶端去拉取呢?如果用推的方式,服務(wù)端需要維持與客戶端的長(zhǎng)連接,這樣的話需要耗費(fèi)大量的資源,并且還需要考慮連接的有效性,例如需要通過(guò)心跳來(lái)維持兩者之間的連接。而用拉的方式,客戶端只需要通過(guò)一個(gè)無(wú)狀態(tài)的 http 請(qǐng)求即可獲取到服務(wù)端的數(shù)據(jù)。
總結(jié)
Nacos 服務(wù)端創(chuàng)建了相關(guān)的配置項(xiàng)后,客戶端就可以進(jìn)行監(jiān)聽(tīng)了。
客戶端是通過(guò)一個(gè)定時(shí)任務(wù)來(lái)檢查自己監(jiān)聽(tīng)的配置項(xiàng)的數(shù)據(jù)的,一旦服務(wù)端的數(shù)據(jù)發(fā)生變化時(shí),客戶端將會(huì)獲取到最新的數(shù)據(jù),并將最新的數(shù)據(jù)保存在一個(gè) CacheData 對(duì)象中,然后會(huì)重新計(jì)算 CacheData 的 md5 屬性的值,此時(shí)就會(huì)對(duì)該 CacheData 所綁定的 Listener 觸發(fā) receiveConfigInfo 回調(diào)。
考慮到服務(wù)端故障的問(wèn)題,客戶端將最新數(shù)據(jù)獲取后會(huì)保存在本地的 snapshot 文件中,以后會(huì)優(yōu)先從文件中獲取配置信息的值。