上篇文章《Nacos 配置中心原理分析》我和大家分析了 Nacos 的配置中心原理眶明,主要分析了 Nacos 客戶端是如何感知到服務(wù)端的配置變更的泛烙,但是只是從客戶端的角度進(jìn)行了分析,并沒有從服務(wù)端的角度進(jìn)行分析,本篇文章我將結(jié)合服務(wù)端從兩個角度來分析配置變更是如何通知到客戶端的。
PS:文章有點長鞍历,因為涉及到多個細(xì)節(jié)需要闡述,如果看不下去的話肪虎,可以直接轉(zhuǎn)到文末看結(jié)論即可劣砍。
一、客戶端
從上篇文章中我們已經(jīng)知道了 Nacos 的客戶端維護(hù)了一個長輪詢的任務(wù)扇救,去檢查服務(wù)端的配置信息是否發(fā)生變更刑枝,如果發(fā)生了變更香嗓,那么客戶端會拿到變更的 groupKey 再根據(jù) groupKey 去獲取配置項的最新值即可。
每次都靠客戶端去發(fā)請求装畅,詢問服務(wù)端我所關(guān)注的配置項有沒有發(fā)生變更靠娱,那請求的間隔改設(shè)置為多少才合適呢?
如果間隔時間設(shè)置的太長的話有可能無法及時獲取服務(wù)端的變更洁灵,如果間隔時間設(shè)置的太短的話,那么頻繁的請求對于服務(wù)端來說無疑也是一種負(fù)擔(dān)掺出。
所以最好的方式是客戶端每隔一段長度適中的時間去服務(wù)端請求徽千,而在這期間如果配置發(fā)生變更,服務(wù)端能夠主動將變更后的結(jié)果推送給客戶端汤锨,這樣既能保證客戶端能夠?qū)崟r感知到配置的變化双抽,也降低了服務(wù)端的壓力。
客戶端長輪詢
現(xiàn)在讓我們再次回到客戶端長輪詢的部分闲礼,也就是 LongPollingRunnable 中的 checkUpdateDataIds 方法牍汹,該方法就是用來訪問服務(wù)端的配置是否發(fā)生變更的,該方法最終會調(diào)用如下圖所示的方法:
check-update-config.jpg
請注意圖中紅框部分的內(nèi)容柬泽,客戶端是通過一個 http 的 post 請求去獲取服務(wù)端的結(jié)果的慎菲,并且設(shè)置了一個超時時間:30s。
這個信息很關(guān)鍵锨并,為什么客戶端要等待 30s 才超時呢露该?不應(yīng)該越快得到結(jié)果越好嗎,我們來驗證下該方法是不是真的等待了 30s第煮。
在 LongPollingRunnable 中的 checkUpdateDataIds 方法前后加上時間計算解幼,然后將所消耗的時間打印出來,如下圖所示:
print-cost-check-update-config.jpg
然后我們啟動客戶端包警,觀察打印的日志撵摆,如下圖所示:
long-polling-cost-result.jpg
從打印出來的日志可以看出來,客戶端足足等了29.5+s害晦,才請求到服務(wù)端的結(jié)果特铝。然后客戶端得到服務(wù)端的結(jié)果之后,再做一些后續(xù)的操作壹瘟,全部都執(zhí)行完畢之后苟呐,在 finally 中又重新調(diào)用了自身,也就是說這個過程是一直循環(huán)下去的俐筋。
長輪詢時修改配置
現(xiàn)在我們可以確定的是牵素,客戶端向服務(wù)端發(fā)起一次請求,最少要29.5s才能得到結(jié)果澄者,當(dāng)然啦笆呆,這是在配置沒有發(fā)生變化的情況下请琳。
如果客戶端在長輪詢時配置發(fā)生變更的話,該請求需要多長時間才會返回呢赠幕,我們繼續(xù)做一個實驗俄精,在客戶端長輪詢時修改配置,結(jié)果如下圖所示:
long-polling-cost-result-2.jpg
上圖中紅框中就是我在客戶端一發(fā)起請求時就更新配置后打印的結(jié)果榕堰,從結(jié)果可以看出來該請求并沒有等到 29.5s+ 才返回竖慧,而是一個很短的時間就返回了,具體多久需要從服務(wù)端的實現(xiàn)中查詢答案逆屡。
到目前為止我們已經(jīng)知道了客戶端執(zhí)行長輪詢的邏輯圾旨,以及每次請求的響應(yīng)時間會隨著服務(wù)端配置是否變更而發(fā)生變化,具體可以用下圖描述:
nacos-client-request.jpg
二魏蔗、服務(wù)端
分析完客戶端的情況砍的,接下來要重點分析服務(wù)端是如何實現(xiàn)的,并且要帶著幾個問題去尋找答案:
客戶端長輪詢的響應(yīng)時間會受什么影響
為什么更改了配置信息后客戶端會立即得到響應(yīng)
客戶端的超時時間為什么要設(shè)置為30s
帶著以上這些問題我們從服務(wù)端的代碼中去探尋結(jié)論莺治。
首先我們從客戶端發(fā)送的 http 請求中可以知道廓鞠,請求的是服務(wù)端的 /v1/cs/configs/listener 這個接口。
我們找到該接口對應(yīng)的方法谣旁,在 ConfigController 類中床佳,如下圖所示:
com.alibaba.nacos.config.server.controller.ConfigController.java
config-controller-listener.jpg
Nacos 的服務(wù)端是通過 spring 對外提供的 http 服務(wù),對 HttpServletRequest 中的參數(shù)進(jìn)行轉(zhuǎn)換后榄审,然后交給一個叫 inner 的對象去執(zhí)行夕土。
下面我們進(jìn)入這個叫 inner 的對象中去,該 inner 對象是 ConfigServletInner 類的實例瘟判,具體的方法如下所示:
com.alibaba.nacos.config.server.controller.ConfigServletInner.java
do-polling-config.jpg
可以看到該方法是一個輪詢的接口怨绣,除了支持長輪詢外還支持短輪詢的邏輯,這里我們只關(guān)心長輪詢的部分拷获,也就是圖中紅框中的部分篮撑。
再次進(jìn)入 longPollingService 的 addLongPollingClient 方法,如下圖所示:
com.alibaba.nacos.config.server.service.LongPollingService.java
add-long-polling-client.jpg
從該方法的名字我們可以知道匆瓜,該方法主要是將客戶端的長輪詢請求添加到某個東西中去赢笨,在方法的最后一行我們得到了答案:服務(wù)端將客戶端的長輪詢請求封裝成一個叫 ClientLongPolling 的任務(wù),交給 scheduler 去執(zhí)行驮吱。
但是請注意我用紅框圈出來的代碼茧妒,服務(wù)端拿到客戶端提交的超時時間后,又減去了 500ms 也就是說服務(wù)端在這里使用了一個比客戶端提交的時間少 500ms 的超時時間左冬,也就是 29.5s桐筏,看到這個 29.5s 我們應(yīng)該有點興奮了。
PS:這里的 timeout 不一定一直是 29.5拇砰,當(dāng) isFixedPolling() 方法為 true 時梅忌,timeout 將會是一個固定的間隔時間狰腌,這里為了描述簡單就直接用 29.5 來進(jìn)行說明。
接下來我們來看服務(wù)端封裝的 ClientLongPolling 的任務(wù)到底執(zhí)行的什么操作牧氮,如下圖所示:
com.alibaba.nacos.config.server.service.LongPollingService.ClientLongPolling.java
client-long-polling.jpg
ClientLongPolling 被提交給 scheduler 執(zhí)行之后琼腔,實際執(zhí)行的內(nèi)容可以拆分成以下四個步驟:
1.創(chuàng)建一個調(diào)度的任務(wù),調(diào)度的延時時間為 29.5s
2.將該 ClientLongPolling 自身的實例添加到一個 allSubs 中去
3.延時時間到了之后踱葛,首先將該 ClientLongPolling 自身的實例從 allSubs 中移除
4.獲取服務(wù)端中保存的對應(yīng)客戶端請求的 groupKeys 是否發(fā)生變更丹莲,將結(jié)果寫入 response 返回給客戶端
整個過程可以用下面的圖進(jìn)行描述:
client-long-polling-process.jpg
這里出現(xiàn)了一個很關(guān)鍵的 allSubs 對象,該對象是一個 ConcurrentLinkedQueue 隊列尸诽,ClientLongPolling 將自身添加到隊列中去肯定是有原因的甥材,這里需要對 allSubs 留個心眼。
調(diào)度任務(wù)
我們先不管 allSubs 隊列具體做了什么事逊谋,先來看下服務(wù)端過了 29.5s 的延時時間后擂达,執(zhí)行調(diào)度任務(wù)時做了什么土铺,也就是上圖中對應(yīng)的第三胶滋、第四步。
首先將自身從 allSubs 隊列中刪除掉悲敷,也就是如注釋中說的:刪除訂閱關(guān)系究恤,從這里我們可以知道 allSubs 和 ClientLongPolling 之間維持了一種訂閱關(guān)系,而 ClientLongPolling 是被訂閱的后德。
PS:刪除掉訂閱關(guān)系之后部宿,訂閱方就無法對被訂閱方進(jìn)行通知了。
然后服務(wù)端對客戶端提交上來的 groupKey 進(jìn)行檢查瓢湃,如果發(fā)現(xiàn)某一個 groupKey 的 md5 值還不是最新的理张,則說明客戶端的配置項還沒發(fā)生變更,所以將該 groupKey 放到一個 changedGroupKeys 列表中绵患,最后將該 changedGroupKeys 返回給客戶端雾叭。
對于客戶端來說,只要拿到 changedGroupKeys 即可落蝙,后續(xù)的操作我在上一篇文章中已經(jīng)分析過了织狐。
服務(wù)端數(shù)據(jù)變更
服務(wù)端直到調(diào)度任務(wù)的延時時間到了之前,ClientLongPolling 都不會有其他的任務(wù)可做筏勒,所以在這段時間內(nèi)移迫,該 allSubs 隊列肯定有事情需要進(jìn)行處理。
回想到我們在客戶端長輪詢期間管行,更改了配置之后厨埋,客戶端能夠立即得到響應(yīng),所以我們有理由相信捐顷,這個隊列可能會跟配置變更有關(guān)系揽咕。
現(xiàn)在我們找一下在 dashboard 上修改配置后悲酷,調(diào)用的請求,可以很容易的找到該請求對應(yīng)的 url為:/v1/cs/configs 并且是一個 POST 請求亲善,具體的方法是 ConfigController 中的 publishConfig 方法设易,如下圖所示:
publish-config.jpg
我只截取了重要的部分,從紅框中的代碼可以看出蛹头,修改配置后顿肺,服務(wù)端首先將配置的值進(jìn)行了持久化層的更新,然后觸發(fā)了一個 ConfigDataChangeEvent 的事件渣蜗。
具體的 fireEvent 的方法如下圖所示:
com.alibaba.nacos.config.server.utils.event.EventDispatcher.java
fire-event.jpg
fireEvent 方法實際上是觸發(fā)的 AbstractEventListener 的 onEvent 方法屠尊,而所有的 listener 是保存在一個叫 listeners 對象中的。
被觸發(fā)的 AbstractEventListener 對象則是通過 addEventListener 方法添加到 listeners 中的耕拷,所以我們只需要找到 addEventListener 方法在何處被調(diào)用的讼昆,就知道有哪些 AbstractEventListener 需要被觸發(fā) onEvent 回調(diào)方法了。
可以找到是在 AbstractEventListener 類的構(gòu)造方法中骚烧,將自身注冊進(jìn)去了浸赫,如下圖所示:
com.alibaba.nacos.config.server.utils.event.EventDispatcher.AbstractEventListener.java
abstract-event-listener.jpg
而 AbstractEventListener 是一個抽象類,所以實際注冊的應(yīng)該是 AbstractEventListener 的子類,所以我們需要找到所以繼承自 AbstractEventListener 的類,如下圖所示:
abstract-event-listener-subclass.jpg
可以看到 AbstractEventListener 所有的子類中驹尼,有一個我們熟悉的身影筝家,他就是我們剛剛一直在研究的 LongPollingService。
所以到這里我們就知道了,當(dāng)我們從 dashboard 中更新了配置項之后,實際會調(diào)用到 LongPollingService 的 onEvent 方法。
現(xiàn)在我們繼續(xù)回到 LongPollingService 中传惠,查看一下 onEvent 方法,如下圖所示:
on-event.jpg
com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask.java
發(fā)現(xiàn)當(dāng)觸發(fā)了 LongPollingService 的 onEvent 方法時稻扬,實際是執(zhí)行了一個叫 DataChangeTask 的任務(wù)卦方,應(yīng)該是通過該任務(wù)來通知客戶端服務(wù)端的數(shù)據(jù)已經(jīng)發(fā)生了變更,我們進(jìn)入 DataChangeTask 中看下具體的代碼腐螟,如下圖所示:
data-change-task.jpg
代碼很簡單愿汰,可以總結(jié)為兩個步驟:
1.遍歷 allSubs 的隊列
首先遍歷 allSubs 的隊列,該隊列中維持的是所有客戶端的請求任務(wù)乐纸,需要找到與當(dāng)前發(fā)生變更的配置項的 groupKey 相等的 ClientLongPolling 任務(wù)
2.往客戶端寫響應(yīng)數(shù)據(jù)
在第一步找到具體的 ClientLongPolling 任務(wù)后衬廷,只需要將發(fā)生變更的 groupKey 通過該 ClientLongPolling 寫入到響應(yīng)對象中,就完成了一次數(shù)據(jù)變更的 “推送” 操作了
如果 DataChangeTask 任務(wù)完成了數(shù)據(jù)的 “推送” 之后汽绢,ClientLongPolling 中的調(diào)度任務(wù)又開始執(zhí)行了怎么辦呢吗跋?
很簡單,只要在進(jìn)行 “推送” 操作之前,先將原來等待執(zhí)行的調(diào)度任務(wù)取消掉就可以了跌宛,這樣就防止了推送操作寫完響應(yīng)數(shù)據(jù)之后酗宋,調(diào)度任務(wù)又去寫響應(yīng)數(shù)據(jù),這時肯定會報錯的疆拘。
可以從 sendResponse 方法中看到蜕猫,確實是這樣做的:
send-response.jpg
問題解答
現(xiàn)在讓我們回到剛開始的時候提的幾個問題,相信大家已經(jīng)有了答案了哎迄。
客戶端長輪詢的響應(yīng)時間會受什么影響
客戶端長輪詢的響應(yīng)時間回右,設(shè)置的是30s,但是有時響應(yīng)很快漱挚,有時響應(yīng)很慢翔烁,這取決于服務(wù)端的配置有沒有發(fā)生變化。當(dāng)配置發(fā)生變化時旨涝,響應(yīng)很快就會返回蹬屹,當(dāng)配置一直沒有發(fā)生變化時,會等到 29.5s 之后再進(jìn)行響應(yīng)白华。
為什么更改了配置信息后客戶端會立即得到響應(yīng)
因為服務(wù)端會在更改了配置信息后慨默,找到具體的客戶端請求中的 response,然后直接將結(jié)果寫入 response 中衬鱼,就像服務(wù)端對客戶端進(jìn)行的數(shù)據(jù) “推送” 一樣业筏,所以客戶端會很快得到響應(yīng)憔杨。
客戶端的超時時間為什么要設(shè)置為30s
這應(yīng)該是一個經(jīng)驗值鸟赫,該超時時間關(guān)系到服務(wù)端調(diào)度任務(wù)的等待時間,服務(wù)端在前29.5s 只需要進(jìn)行等待消别,最后的 0.5s 才進(jìn)行配置變更檢查抛蚤。
如果設(shè)置的太短,那服務(wù)端等待的時間就太短寻狂,如果這時配置變更的比較頻繁岁经,那很可能無法在等待期對客戶端做推送,而是滑動到檢查期對數(shù)據(jù)進(jìn)行檢查后才能將數(shù)據(jù)變更發(fā)回給客戶端蛇券,檢查期相比等待期需要進(jìn)行數(shù)據(jù)的檢查缀壤,涉及到 IO 操作,而 IO 操作是比較昂貴的纠亚,我們應(yīng)該盡量在等待期就將數(shù)據(jù)變更發(fā)送給客戶端塘慕。
http 請求本來就是無狀態(tài)的,所以沒必要也不能將超時時間設(shè)置的太長蒂胞,這樣是對資源的一種浪費(fèi)图呢。
結(jié)論
1、客戶端的請求到達(dá)服務(wù)端后,服務(wù)端將該請求加入到一個叫 allSubs 的隊列中蛤织,等待配置發(fā)生變更時 DataChangeTask 主動去觸發(fā)赴叹,并將變更后的數(shù)據(jù)寫入響應(yīng)對象,如下圖所示:
nacos-config-update-1.jpg
2指蚜、與此同時服務(wù)端也將該請求封裝成一個調(diào)度任務(wù)去執(zhí)行乞巧,等待調(diào)度的期間就是等待 DataChangeTask 主動觸發(fā)的,如果延遲時間到了 DataChangeTask 還未觸發(fā)的話摊鸡,則調(diào)度任務(wù)開始執(zhí)行數(shù)據(jù)變更的檢查摊欠,然后將檢查的結(jié)果寫入響應(yīng)對象,如下圖所示:
nacos-config-update-2.jpg
基于上述的分析柱宦,最終總結(jié)了以下結(jié)論:
1.Nacos 客戶端會循環(huán)請求服務(wù)端變更的數(shù)據(jù)些椒,并且超時時間設(shè)置為30s,當(dāng)配置發(fā)生變化時掸刊,請求的響應(yīng)會立即返回免糕,否則會一直等到 29.5s+ 之后再返回響應(yīng)
2.Nacos 客戶端能夠?qū)崟r感知到服務(wù)端配置發(fā)生了變化。
3.實時感知是建立在客戶端拉和服務(wù)端“推”的基礎(chǔ)上忧侧,但是這里的服務(wù)端“推”需要打上引號石窑,因為服務(wù)端和客戶端直接本質(zhì)上還是通過 http 進(jìn)行數(shù)據(jù)通訊的,之所以有“推”的感覺蚓炬,是因為服務(wù)端主動將變更后的數(shù)據(jù)通過 http 的 response 對象提前寫入了松逊。
至此,正如標(biāo)題所說的肯夏,推+拉打造 Nacos 配置信息的實時更新的原理已經(jīng)分析清楚了经宏。
逅弈逐碼,專注于原創(chuàng)分享驯击,用通俗易懂的圖文描述源碼及原理
作者:逅弈
鏈接:http://www.reibang.com/p/acb9b1093a54
來源:簡書
著作權(quán)歸作者所有烁兰。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處徊都。