?【Alibaba中介軟體技術系列】「Nacos技術專題」配置中心載入原理和配置實時更新原理分析(中)

浩宇天尚發表於2022-01-21

官方資源

https://nacos.io/zh-cn/docs/quick-start.html

帶著問題去思考

  • 客戶端長輪詢的響應時間會受什麼影響
  • 為什麼更改了配置資訊後客戶端會立即得到響應
  • 客戶端的超時時間為什麼要設定為30s
  • 帶著以上這些問題我們從服務端的程式碼中去探尋結論。

Nacos之配置中心

  • 動態配置管理是 Nacos的三大功能之一,通過動態配置服務,可以在所有環境中以集中和動態的方式管理所有應用程式或服務的配置資訊。
  • 動態配置中心可以實現配置更新時無需重新部署應用程式和服務即可使相應的配置資訊生效,這極大了增加了系統的運維能力。

動態配置

Nacos的動態配置的能力,看看 Nacos是如何以簡單、優雅、高效的方式管理配置,實現配置的動態變更的,接下來來了解下 Nacos 的動態配置的功能。

客戶端動態化配置機制

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

客戶端去發請求,詢問服務端我所關注的配置項有沒有發生變更,如果間隔時間設定的太長的話有可能無法及時獲取服務端的變更,如果間隔時間設定的太短的話,那麼頻繁的請求對於服務端來說無疑也是一種負擔。

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

客戶端長輪詢

客戶端長輪詢的部分,也就是LongPollingRunnable中的checkUpdateDataIds 方法,該方法就是用來訪問服務端的配置是否發生變更的,該方法最終會呼叫如下圖所示的方法:

http請求操作

客戶端是通過一個http的post 請求去獲取服務端的結果的,並且設定了一個超時時間:30s。一般來講:客戶端足足等了29.5+s,才請求到服務端的結果,然後客戶端得到服務端的結果之後,再做一些後續的操作,全部都執行完畢之後,在 finally 中又重新呼叫了自身,也就是說這個過程是一直迴圈下去的。

長輪詢執行邏輯

客戶端向服務端發起一次請求,最少要29.5s才能得到結果,當然啦,這是在配置沒有發生變化的情況下。如果客戶端在長輪詢時配置發生變更的話,該請求需要多長時間才會返回呢,在客戶端長輪詢時修改配置。

未獲得到修改資料的操作觸發返回

獲得到了修改資料操作立刻觸發返回

服務端controller

上面說到了客戶端傳送的 http 請求中可以知道,請求的是服務端的 /v1/cs/configs/listener 這個介面,com.alibaba.nacos.config.server.controller.ConfigController.java,在 ConfigController 類中,如下圖所示:

服務端是通過springMVC對外提供的 http 服務,對 HttpServletRequest 中的引數進行轉換後,然後交給一個叫 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 將自身新增到佇列中。

排程任務

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

服務端資料變更

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

在客戶端長輪詢期間,更改了配置之後,客戶端能夠立即得到響應。

服務端資料變更介面

呼叫的請求,可以很容易的找到該請求對應的 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 所有的子類中LongPollingService。當我們從 dashboard 中更新了配置項之後,實際會呼叫到 LongPollingService 的 onEvent 方法。

回到 LongPollingService 中,檢視一下 onEvent 方法,如下圖所示:

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

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

遍歷 allSubs 的佇列

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

往客戶端寫響應資料

丟i與ClientLongPolling 任務後,只需要將發生變更的 groupKey 通過該 ClientLongPolling 寫入到響應物件中,就完成了一次資料變更的 “推送” 操作了

如果 DataChangeTask 任務完成了資料的 “推送” 之後,需要將原來等待執行的排程任務取消掉了,這樣就防止了推送操作寫完響應資料之後,排程任務又去寫響應資料。

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

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

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

總結結論:

  1. Nacos 客戶端會迴圈請求服務端變更的資料,並且超時時間設定為30s,當配置發生變化時,請求的響應會立即返回,否則會一直等到 29.5s+ 之後再返回響應
  2. Nacos 客戶端能夠實時感知到服務端配置發生了變化。
  3. 實時感知是建立在客戶端拉和服務端“推”的基礎上,但是這裡的服務端“推”需要打上引號,因為服務端和客戶端直接本質上還是通過 http 進行資料通訊的,之所以有“推”的感覺,是因為服務端主動將變更後的資料通過 http 的 response 物件提前寫入了。

相關文章