推+拉打造Nacos客戶端配置信息的實時更新

上篇文章《Nacos 配置中心原理分析 》我和你們分析了 Nacos 的配置中心原理,主要分析了 Nacos 客戶端是如何感知到服務端的配置變動的,可是隻是從客戶端的角度進行了分析,並無從服務端的角度進行分析,本篇文章我將結合服務端從兩個角度來分析配置變動是如何通知到客戶端的。java

PS:文章有點長,由於涉及到多個細節須要闡述,若是看不下去的話,能夠直接轉到文末看總結便可。spring

1、客戶端

從上篇文章中咱們已經知道了 Nacos 的客戶端維護了一個長輪詢的任務,去檢查服務端的配置信息是否發生變動,若是發生了變動,那麼客戶端會拿到變動的 groupKey 再根據 groupKey 去獲取配置項的最新值便可。post

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

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

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

客戶端長輪詢

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

check-update-config.jpg

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

這個信息很關鍵,爲何客戶端要等待 30s 才超時呢?不該該越快獲得結果越好嗎,咱們來驗證下該方法是否是真的等待了 30s。對象

在 LongPollingRunnable 中的 checkUpdateDataIds 方法先後加上時間計算,而後將所消耗的時間打印出來,以下圖所示:blog

print-cost-check-update-config.jpg

而後咱們啓動客戶端,觀察打印的日誌,以下圖所示:

long-polling-cost-result.jpg

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

長輪詢時修改配置

如今咱們能夠肯定的是,客戶端向服務端發起一次請求,最少要29.5s才能獲得結果,固然啦,這是在配置沒有發生變化的狀況下。

若是客戶端在長輪詢時配置發生變動的話,該請求須要多長時間纔會返回呢,咱們繼續作一個實驗,在客戶端長輪詢時修改配置,結果以下圖所示:

long-polling-cost-result-2.jpg

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

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

nacos-client-request.jpg

2、服務端

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

  • 客戶端長輪詢的響應時間會受什麼影響
  • 爲何更改了配置信息後客戶端會當即獲得響應
  • 客戶端的超時時間爲何要設置爲30s

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

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

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

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

config-controller-listener.jpg

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

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

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

do-polling-config.jpg

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

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

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

add-long-polling-client.jpg

從該方法的名字咱們能夠知道,該方法主要是將客戶端的長輪詢請求添加到某個東西中去,在方法的最後一行咱們獲得了答案:服務端將客戶端的長輪詢請求封裝成一個叫 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

client-long-polling.jpg

ClientLongPolling 被提交給 scheduler 執行以後,實際執行的內容能夠拆分紅如下四個步驟:

  • 1.建立一個調度的任務,調度的延時時間爲 29.5s
  • 2.將該 ClientLongPolling 自身的實例添加到一個 allSubs 中去
  • 3.延時時間到了以後,首先將該 ClientLongPolling 自身的實例從 allSubs 中移除
  • 4.獲取服務端中保存的對應客戶端請求的 groupKeys 是否發生變動,將結果寫入 response 返回給客戶端

整個過程能夠用下面的圖進行描述:

client-long-polling-process.jpg

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

調度任務

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

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

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

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

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

服務端數據變動

服務端直到調度任務的延時時間到了以前,ClientLongPolling 都不會有其餘的任務可作,因此在這段時間內,該 allSubs 隊列確定有事情須要進行處理。

回想到咱們在客戶端長輪詢期間,更改了配置以後,客戶端可以當即獲得響應,因此咱們有理由相信,這個隊列可能會跟配置變動有關係。

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

publish-config.jpg

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

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

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

fire-event.jpg

fireEvent 方法其實是觸發的 AbstractEventListener 的 onEvent 方法,而全部的 listener 是保存在一個叫 listeners 對象中的。

被觸發的 AbstractEventListener 對象則是經過 addEventListener 方法添加到 listeners 中的,因此咱們只須要找到 addEventListener 方法在何處被調用的,就知道有哪些 AbstractEventListener 須要被觸發 onEvent 回調方法了。

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

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

abstract-event-listener.jpg

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

abstract-event-listener-subclass.jpg

能夠看到 AbstractEventListener 全部的子類中,有一個咱們熟悉的身影,他就是咱們剛剛一直在研究的 LongPollingService。

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

如今咱們繼續回到 LongPollingService 中,查看一下 onEvent 方法,以下圖所示:

on-event.jpg

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

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

data-change-task.jpg

代碼很簡單,能夠總結爲兩個步驟:

  • 1.遍歷 allSubs 的隊列

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

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

在第一步找到具體的 ClientLongPolling 任務後,只須要將發生變動的 groupKey 經過該 ClientLongPolling 寫入到響應對象中,就完成了一次數據變動的 「推送」 操做了

若是 DataChangeTask 任務完成了數據的 「推送」 以後,ClientLongPolling 中的調度任務又開始執行了怎麼辦呢?

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

能夠從 sendResponse 方法中看到,確實是這樣作的:

send-response.jpg

問題解答

如今讓咱們回到剛開始的時候提的幾個問題,相信你們已經有了答案了。

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

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

  • 爲何更改了配置信息後客戶端會當即獲得響應

由於服務端會在更改了配置信息後,找到具體的客戶端請求中的 response,而後直接將結果寫入 response 中,就像服務端對客戶端進行的數據 「推送」 同樣,因此客戶端會很快獲得響應。

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

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

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

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

總結

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

nacos-config-update-1.jpg

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

nacos-config-update-2.jpg

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

  • 1.Nacos 客戶端會循環請求服務端變動的數據,而且超時時間設置爲30s,當配置發生變化時,請求的響應會當即返回,不然會一直等到 29.5s+ 以後再返回響應
  • 2.Nacos 客戶端可以實時感知到服務端配置發生了變化。
  • 3.實時感知是創建在客戶端拉和服務端「推」的基礎上,可是這裏的服務端「推」須要打上引號,由於服務端和客戶端直接本質上仍是經過 http 進行數據通信的,之因此有「推」的感受,是由於服務端主動將變動後的數據經過 http 的 response 對象提早寫入了。

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

逅弈逐碼,專一於原創分享,用通俗易懂的圖文描述源碼及原理

相關文章
相關標籤/搜索