nacos配置中心原理

轉載自https://mp.weixin.qq.com/s?__biz=MzAxODcyNjEzNQ==&mid=2247487083&idx=2&sn=ef637dcc3e9d708e281fd4a33668f75f&chksm=9bd0a3f3aca72ae5395739bef0dff2cc5cf4c1e9aa1375c43db2fc7887d04f7beab88c35ec1f&scene=27#wechat_redirect

 

Nacos 的客戶端維護了一個長輪詢的任務,去檢查服務端的配置信息是否發生變更,如果發生了變更,那麼客戶端會拿到變更的 groupKey 再根據 groupKey 去獲取配置項的最新值即可。

每次都靠客戶端去發請求,詢問服務端我所關注的配置項有沒有發生變更,那請求的間隔改設置爲多少才合適呢?

如果間隔時間設置的太長的話有可能無法及時獲取服務端的變更,如果間隔時間設置的太短的話,那麼頻繁的請求對於服務端來說無疑也是一種負擔。

所以最好的方式是客戶端每隔一段長度適中的時間去服務端請求,而在這期間如果配置發生變更,服務端能夠主動將變更後的結果推送給客戶端,這樣既能保證客戶端能夠實時感知到配置的變化,也降低了服務端的壓力。

客戶端長輪詢

現在讓我們再次回到客戶端長輪詢的部分,也就是 LongPollingRunnable 中的 checkUpdateDataIds 方法,該方法就是用來訪問服務端的配置是否發生變更的,該方法最終會調用如下圖所示的方法:

請注意圖中紅框部分的內容,客戶端是通過一個 http 的 post 請求去獲取服務端的結果的,並且設置了一個超時時間:30s。

這個信息很關鍵,爲什麼客戶端要等待 30s 才超時呢?不應該越快得到結果越好嗎,我們來驗證下該方法是不是真的等待了 30s。

在 LongPollingRunnable 中的 checkUpdateDataIds 方法前後加上時間計算,然後將所消耗的時間打印出來,如下圖所示:

然後我們啓動客戶端,觀察打印的日誌,如下圖所示:

從打印出來的日誌可以看出來,客戶端足足等了29.5+s,才請求到服務端的結果。然後客戶端得到服務端的結果之後,再做一些後續的操作,全部都執行完畢之後,在 finally 中又重新調用了自身,也就是說這個過程是一直循環下去的。

長輪詢時修改配置

現在我們可以確定的是,客戶端向服務端發起一次請求,最少要29.5s才能得到結果,當然啦,這是在配置沒有發生變化的情況下。

如果客戶端在長輪詢時配置發生變更的話,該請求需要多長時間纔會返回呢,我們繼續做一個實驗,在客戶端長輪詢時修改配置,結果如下圖所示:

上圖中紅框中就是我在客戶端一發起請求時就更新配置後打印的結果,從結果可以看出來該請求並沒有等到 29.5s+ 才返回,而是一個很短的時間就返回了,具體多久需要從服務端的實現中查詢答案。

到目前爲止我們已經知道了客戶端執行長輪詢的邏輯,以及每次請求的響應時間會隨着服務端配置是否變更而發生變化,具體可以用下圖描述:

二、服務端

分析完客戶端的情況,接下來要重點分析服務端是如何實現的,並且要帶着幾個問題去尋找答案:

  • 客戶端長輪詢的響應時間會受什麼影響

  • 爲什麼更改了配置信息後客戶端會立即得到響應

  • 客戶端的超時時間爲什麼要設置爲30s

帶着以上這些問題我們從服務端的代碼中去探尋結論。

首先我們從客戶端發送的 http 請求中可以知道,請求的是服務端的 /v1/cs/configs/listener 這個接口。

我們找到該接口對應的方法,在 ConfigController 類中,如下圖所示:

com.alibaba.nacos.config.server.controller.ConfigController.java

Nacos 的服務端是通過 spring 對外提供的 http 服務,對 HttpServletRequest 中的參數進行轉換後,然後交給一個叫 inner 的對象去執行。

下面我們進入這個叫 inner 的對象中去,該 inner 對象是 ConfigServletInner 類的實例,具體的方法如下所示:

com.alibaba.nacos.config.server.controller.ConfigServletInner.java

可以看到該方法是一個輪詢的接口,除了支持長輪詢外還支持短輪詢的邏輯,這裏我們只關心長輪詢的部分,也就是圖中紅框中的部分。

再次進入 longPollingService 的 addLongPollingClient 方法,如下圖所示:

com.alibaba.nacos.config.server.service.LongPollingService.java

從該方法的名字我們可以知道,該方法主要是將客戶端的長輪詢請求添加到某個東西中去,在方法的最後一行我們得到了答案:服務端將客戶端的長輪詢請求封裝成一個叫 ClientLongPolling 的任務,交給 scheduler 去執行。

但是請注意我用紅框圈出來的代碼,服務端拿到客戶端提交的超時時間後,又減去了 500ms 也就是說服務端在這裏使用了一個比客戶端提交的時間少 500ms 的超時時間,也就是 29.5s,看到這個 29.5s 我們應該有點興奮了。

PS:這裏的 timeout 不一定一直是 29.5,當 isFixedPolling() 方法爲 true 時,timeout 將會是一個固定的間隔時間,這裏爲了描述簡單就直接用 29.5 來進行說明。

接下來我們來看服務端封裝的 ClientLongPolling 的任務到底執行的什麼操作,如下圖所示:

com.alibaba.nacos.config.server.service.LongPollingService.ClientLongPolling.java

ClientLongPolling 被提交給 scheduler 執行之後,實際執行的內容可以拆分成以下四個步驟:

  • 1.創建一個調度的任務,調度的延時時間爲 29.5s

  • 2.將該 ClientLongPolling 自身的實例添加到一個 allSubs 中去

  • 3.延時時間到了之後,首先將該 ClientLongPolling 自身的實例從 allSubs 中移除

  • 4.獲取服務端中保存的對應客戶端請求的 groupKeys 是否發生變更,將結果寫入 response 返回給客戶端

整個過程可以用下面的圖進行描述:

這裏出現了一個很關鍵的 allSubs 對象,該對象是一個 ConcurrentLinkedQueue 隊列,ClientLongPolling 將自身添加到隊列中去肯定是有原因的,這裏需要對 allSubs 留個心眼。

調度任務

我們先不管 allSubs 隊列具體做了什麼事,先來看下服務端過了 29.5s 的延時時間後,執行調度任務時做了什麼,也就是上圖中對應的第三、第四步。

首先將自身從 allSubs 隊列中刪除掉,也就是如註釋中說的:刪除訂閱關係,從這裏我們可以知道 allSubs 和 ClientLongPolling 之間維持了一種訂閱關係,而 ClientLongPolling 是被訂閱的。

PS:刪除掉訂閱關係之後,訂閱方就無法對被訂閱方進行通知了。

然後服務端對客戶端提交上來的 groupKey 進行檢查,如果發現某一個 groupKey 的 md5 值還不是最新的,則說明客戶端的配置項還沒發生變更,所以將該 groupKey 放到一個 changedGroupKeys 列表中,最後將該 changedGroupKeys 返回給客戶端。

對於客戶端來說,只要拿到 changedGroupKeys 即可,後續的操作我在上一篇文章中已經分析過了。

服務端數據變更

服務端直到調度任務的延時時間到了之前,ClientLongPolling 都不會有其他的任務可做,所以在這段時間內,該 allSubs 隊列肯定有事情需要進行處理。

回想到我們在客戶端長輪詢期間,更改了配置之後,客戶端能夠立即得到響應,所以我們有理由相信,這個隊列可能會跟配置變更有關係。

現在我們找一下在 dashboard 上修改配置後,調用的請求,可以很容易的找到該請求對應的 url爲:/v1/cs/configs 並且是一個 POST 請求,具體的方法是 ConfigController 中的 publishConfig 方法,如下圖所示:

我只截取了重要的部分,從紅框中的代碼可以看出,修改配置後,服務端首先將配置的值進行了持久化層的更新,然後觸發了一個 ConfigDataChangeEvent 的事件。

具體的 fireEvent 的方法如下圖所示:

com.alibaba.nacos.config.server.utils.event.EventDispatcher.java

fireEvent 方法實際上是觸發的 AbstractEventListener 的 onEvent 方法,而所有的 listener 是保存在一個叫 listeners 對象中的。

被觸發的 AbstractEventListener 對象則是通過 addEventListener 方法添加到 listeners 中的,所以我們只需要找到 addEventListener 方法在何處被調用的,就知道有哪些 AbstractEventListener 需要被觸發 onEvent 回調方法了。

可以找到是在 AbstractEventListener 類的構造方法中,將自身註冊進去了,如下圖所示:

com.alibaba.nacos.config.server.utils.event.EventDispatcher.AbstractEventListener.java

而 AbstractEventListener 是一個抽象類,所以實際註冊的應該是 AbstractEventListener 的子類,所以我們需要找到所以繼承自 AbstractEventListener 的類,如下圖所示:

可以看到 AbstractEventListener 所有的子類中,有一個我們熟悉的身影,他就是我們剛剛一直在研究的 LongPollingService。

所以到這裏我們就知道了,當我們從 dashboard 中更新了配置項之後,實際會調用到 LongPollingService 的 onEvent 方法。

現在我們繼續回到 LongPollingService 中,查看一下 onEvent 方法,如下圖所示:

com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask.java

發現當觸發了 LongPollingService 的 onEvent 方法時,實際是執行了一個叫 DataChangeTask 的任務,應該是通過該任務來通知客戶端服務端的數據已經發生了變更,我們進入 DataChangeTask 中看下具體的代碼,如下圖所示:

代碼很簡單,可以總結爲兩個步驟:

  • 1.遍歷 allSubs 的隊列

首先遍歷 allSubs 的隊列,該隊列中維持的是所有客戶端的請求任務,需要找到與當前發生變更的配置項的 groupKey 相等的 ClientLongPolling 任務

  • 2.往客戶端寫響應數據

在第一步找到具體的 ClientLongPolling 任務後,只需要將發生變更的 groupKey 通過該 ClientLongPolling 寫入到響應對象中,就完成了一次數據變更的 “推送” 操作了

如果 DataChangeTask 任務完成了數據的 “推送” 之後,ClientLongPolling 中的調度任務又開始執行了怎麼辦呢?

很簡單,只要在進行 “推送” 操作之前,先將原來等待執行的調度任務取消掉就可以了,這樣就防止了推送操作寫完響應數據之後,調度任務又去寫響應數據,這時肯定會報錯的。

可以從 sendResponse 方法中看到,確實是這樣做的:

問題解答

現在讓我們回到剛開始的時候提的幾個問題,相信大家已經有了答案了。

  • 客戶端長輪詢的響應時間會受什麼影響

客戶端長輪詢的響應時間,設置的是30s,但是有時響應很快,有時響應很慢,這取決於服務端的配置有沒有發生變化。當配置發生變化時,響應很快就會返回,當配置一直沒有發生變化時,會等到 29.5s 之後再進行響應。

  • 爲什麼更改了配置信息後客戶端會立即得到響應

因爲服務端會在更改了配置信息後,找到具體的客戶端請求中的 response,然後直接將結果寫入 response 中,就像服務端對客戶端進行的數據 “推送” 一樣,所以客戶端會很快得到響應。

  • 客戶端的超時時間爲什麼要設置爲30s

這應該是一個經驗值,該超時時間關係到服務端調度任務的等待時間,服務端在前29.5s 只需要進行等待,最後的 0.5s 才進行配置變更檢查。

如果設置的太短,那服務端等待的時間就太短,如果這時配置變更的比較頻繁,那很可能無法在等待期對客戶端做推送,而是滑動到檢查期對數據進行檢查後才能將數據變更發回給客戶端,檢查期相比等待期需要進行數據的檢查,涉及到 IO 操作,而 IO 操作是比較昂貴的,我們應該儘量在等待期就將數據變更發送給客戶端。

http 請求本來就是無狀態的,所以沒必要也不能將超時時間設置的太長,這樣是對資源的一種浪費。

總結

1、客戶端的請求到達服務端後,服務端將該請求加入到一個叫 allSubs 的隊列中,等待配置發生變更時 DataChangeTask 主動去觸發,並將變更後的數據寫入響應對象,如下圖所示:

2、與此同時服務端也將該請求封裝成一個調度任務去執行,等待調度的期間就是等待 DataChangeTask 主動觸發的,如果延遲時間到了 DataChangeTask 還未觸發的話,則調度任務開始執行數據變更的檢查,然後將檢查的結果寫入響應對象,如下圖所示:

基於上述的分析,最終總結了以下結論:

  • 1.Nacos 客戶端會循環請求服務端變更的數據,並且超時時間設置爲30s,當配置發生變化時,請求的響應會立即返回,否則會一直等到 29.5s+ 之後再返回響應

  • 2.Nacos 客戶端能夠實時感知到服務端配置發生了變化。

  • 3.實時感知是建立在客戶端拉和服務端“推”的基礎上,但是這裏的服務端“推”需要打上引號,因爲服務端和客戶端直接本質上還是通過 http 進行數據通訊的,之所以有“推”的感覺,是因爲服務端主動將變更後的數據通過 http 的 response 對象提前寫入了。

至此,正如標題所說的,推+拉打造 Nacos 配置信息的實時更新的原理已經分析清楚了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章