文本已收錄至個人GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是如今
我知道不少人不玩qq了,可是懷舊一下,歡迎加入六脈神劍Java菜鳥學習羣,羣聊號碼:549684836 鼓勵你們在技術的路上寫博客java
團隊準備作一個新項目,而後採用的是微服務架構和分佈式系統開發,恰好開始用的時候SpringBoot 2.2.0 而後碰到一些問題記錄一下,而後再一塊兒來學習學習 Nacos作配置中心的大體原理,若是有時間 還能夠看看源碼,哈哈mysql
Failed to bind properties under 'mybatis-plus.configuration.incomplete-result-maps[0].assistant.configuration.mapped-statements[0].paameter-map.parameter-mappings[0]' to org.apache.ibatis.mapping.ParameterMappinggit
真的是每過10秒就會發一次,後面查資料 查了很久才知道這個是SpringBoot的一個小buggithub
官方issue: github.com/spring-pro... 構造器注入的問題, mybatis 私有構造器不能綁定屬性, 形成其餘 依賴mybatis 的框架 類型 mybatis-plus 這種問題 gitee.com/baomidou/my...spring
而後解決辦法sql
也能夠參考冷冷大神的文章數據庫
而後這個問題就解決了。apache
Nacos 致力於幫助您發現、配置和管理微服務。Nacos 提供了一組簡單易用的特性集,幫助您快速實現動態服務發現、服務配置、服務元數據及流量管理。segmentfault
Nacos 幫助您更敏捷和容易地構建、交付和管理微服務平臺。 Nacos 是構建以「服務」爲中心的現代應用架構 (例如微服務範式、雲原生範式) 的服務基礎設施。緩存
Nacos的做用仍是不少的,可是在咱們的項目裏面,目前我就接觸到了其中的2項,一個服務的註冊中心,和一個服務的配置中心,既然用到了,固然得學學人家是怎麼實現的吧,哈哈,對於剛接觸這些東西的小白,本身對源碼和原理的理解都是再前輩的基礎上,先看山是山吧 而後看山不是山,最後看山仍是山,小六六本身的學習方法論哈哈。
動態配置管理是 Nacos 的三大功能之一,經過動態配置服務,咱們能夠在全部環境中以集中和動態的方式管理全部應用程序或服務的配置信息。
動態配置中心能夠實現配置更新時無需從新部署應用程序和服務便可使相應的配置信息生效,這極大了增長了系統的運維能力。
之前咱們修改了一些東西,總得重啓系統,如今有這個咱們就能夠動態的去修改配置了,不過提及實現的原理仍是沒有那麼簡單的,其中涉及到了spring +springcloud+nacos的服務端+nacos的客戶端;其中每個角色都是這個實現的必不可少的一部分。小六六會跟你們一塊兒來學習學習,本身自己比較菜,也是剛接觸,那小六六就一個個慢慢的來看看
下面我未來和你們一塊兒來了解下 Nacos 的動態配置的能力,看看 Nacos 是如何以簡單、優雅、高效的方式管理配置,實現配置的動態變動的。
首先咱們要準備一個 Nacos 的服務端,如今有兩種方式獲取 Nacos 的服務端:
1.經過源碼編譯 2.下載 Release 包
兩種方法能夠得到 Nacos 的可執行程序,下面我用第一種方式經過源碼編譯一個可執行程序,可能有人會問爲啥不直接下載 Release 包,還要本身去編譯呢?首先 Release 包也是經過源碼編譯獲得的,其次咱們經過本身編譯能夠了解一些過程也有可能會碰到一些問題,這些都是很重要的經驗,好了那咱們直接源碼編譯吧。
首先 fork 一份 nacos 的代碼到本身的 github 庫,而後把代碼 clone 到本地。
而後在項目的根目錄下執行如下命令(假設咱們已經配置好了 java 和 maven 環境):
mvn -Prelease-nacos clean install -U
複製代碼
而後在項目的 distribution 目錄下咱們就能夠找到可執行程序了,包括兩個壓縮包,這兩個壓縮包就是nacos 的 github 官網上發佈的 Release 包。
sh startup.sh -m standalone
複製代碼
接下來咱們在控制檯上建立一個簡單的配置項,以下圖所示:
當服務端以及配置項都準備好以後,就能夠建立客戶端了,以下圖所示新建一個 Nacos 的 ConfigService 來接收數據:
執行後將打印以下信息:
修改配置信息
執行後將打印以下信息:
至此一個簡單的動態配置管理功能已經講完了,刪除配置和更新配置操做相似,這裏再也不贅述。
瞭解了動態配置管理的效果以後,咱們知道了大概的原理了,Nacos 服務端保存了配置信息,客戶端鏈接到服務端以後,根據 dataID,group能夠獲取到具體的配置信息,當服務端的配置發生變動時,客戶端會收到通知。當客戶端拿到變動後的最新配置信息後,就能夠作本身的處理了,這很是有用,全部須要使用配置的場景均可以經過 Nacos 來進行管理。
能夠說 Nacos 有不少的適用場景,包括但不限於如下這些狀況:
如今咱們瞭解了 Nacos 的配置管理的功能了,可是有一個問題咱們須要弄明白,那就是 Nacos 客戶端是怎麼實時獲取到 Nacos 服務端的最新數據的。
其實客戶端和服務端之間的數據交互,無外乎兩種狀況:
那究竟是推仍是拉呢,從 Nacos 客戶端經過 Listener 來接收最新數據的這個作法來看,感受像是服務端推的數據,可是不能想固然,要想知道答案,最快最準確的方法就是從源碼中去尋找。
ConfigService configService = NacosFactory.createConfigService(properties);
複製代碼
從咱們的 demo 中能夠知道,首先是建立了一個 ConfigService。而 ConfigService 是經過 ConfigFactory 類建立的,以下圖所示:
/**
* Create Config
*
* @param properties init param
* @return Config
* @throws NacosException Exception
*/
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService)constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(-400, e.getMessage());
}
}
複製代碼
能夠看到實際是經過反射調用了 NacosConfigService 的構造方法來建立 ConfigService 的,並且是有一個 Properties 參數的構造方法。
須要注意的是,這裏並無經過單例或者緩存技術,也就是說每次調用都會從新建立一個 ConfigService的實例。
實例化時主要是初始化了兩個對象,他們分別是:
其中 agent 是經過裝飾着模式實現的,ServerHttpAgent 是實際工做的類,MetricsHttpAgent 在內部也是調用了 ServerHttpAgent 的方法,另外加上了一些統計操做,因此咱們只須要關心 ServerHttpAgent 的功能就能夠了。
agent 實際是在 ClientWorker 中發揮能力的,下面咱們來看下 ClientWorker 類。
如下是 ClientWorker 的構造方法,以下圖所示:
能夠看到 ClientWorker 除了將 HttpAgent 維持在本身內部,還建立了兩個線程池:
第一個線程池是隻擁有一個線程用來執行定時任務的 executor,executor 每隔 10ms 就會執行一次 checkConfigInfo() 方法,從方法名上能夠知道是每 10 ms 檢查一次配置信息。
第二個線程池是一個普通的線程池,從 ThreadFactory 的名稱能夠看到這個線程池是作長輪詢的。(科普一下長輪詢:客戶端向服務器端發送 Ajax 請求,服務器端接收到請求後保持住鏈接,直到有新消息才返回響應信息並關閉鏈接。客戶端在處理請求返回信息(超時或有效數據)後再次發出請求,從新創建鏈接。缺點是:服務器保持鏈接會消耗較多的資源。)
如今讓咱們來看下 executor 每 10ms 執行的方法究竟是幹什麼的,以下圖所示:
這個和我前面用SpringBoot致使mybatis 不兼容的狀況報錯時間間隔是同樣的 哈哈 牛逼!
提一句 看咱們中國人本身寫的代碼就是舒服 註釋都是中文,美滋滋。
能夠看到,checkConfigInfo 方法是取出了一批任務,而後提交給 executorService 線程池去執行,執行的任務就是 LongPollingRunnable,每一個任務都有一個 taskId。
如今咱們來看看 LongPollingRunnable 作了什麼,主要分爲兩部分,第一部分是檢查本地的配置信息,第二部分是獲取服務端的配置信息而後更新到本地。
1.本地檢查經過跟蹤 checkLocalConfig 方法,能夠看到 Nacos 將配置信息保存在了
~/nacos/config/fixed-{address}_8848_nacos/snapshot/DEFAULT_GROUP/{dataId}
而後經過 checkUpdateDataIds() 方法從服務端獲取那些值發生了變化的 dataId 列表,
經過 getServerConfig 方法,根據 dataId 到服務端獲取最新的配置信息,接着將最新的配置信息保存到 CacheData 中。
最後調用 CacheData 的 checkListenerMd5 方法,能夠看到該方法在第一部分也被調用過,咱們須要重點關注一下。
能夠看到,在該任務的最後,也就是在 finally 中又從新經過 executorService 提交了本任務。
好了如今咱們能夠爲 ConfigService 來添加一個 Listener 了,最終是調用了 ClientWorker 的 addTenantListeners 方法,以下圖所示:
該方法分爲兩個部分,首先根據 dataId,group 和當前的場景獲取一個 CacheData 對象,而後將當前要添加的 listener 對象添加到 CacheData 中去。
也就是說 listener 最終是被這裏的 CacheData 所持有了,那 listener 的回調方法 receiveConfigInfo 就應該是在 CacheData 中觸發的。
咱們發現 CacheData 是出現頻率很是高的一個類,在 LongPollingRunnable 的任務中,幾乎全部的方法都圍繞着 CacheData 類,如今添加 Listener 的時候,實際上該 Listener 也被委託給了 CacheData,那咱們要重點關注下 CacheData 類了。
能夠看到除了 dataId,group,content,taskId 這些跟配置相關的屬性,還有兩個比較重要的屬性:listeners、md5。
listeners 是該 CacheData 所關聯的全部 listener,不過不是保存的原始的 Listener 對象,而是包裝後的 ManagerListenerWrap 對象,該對象除了持有 Listener 對象,還持有了一個 lastCallMd5 屬性。
另一個屬性 md5 就是根據當前對象的 content 計算出來的 md5 值。
這個就很明瞭,怎麼判斷是否有變化,只要是他們的內容的md5值不同,那麼就說明他們內容是有變化的
如今咱們對 ConfigService 有了大體的瞭解了,如今剩下最後一個重要的問題尚未答案,那就是 ConfigService 的 Listener 是在何時觸發回調方法 receiveConfigInfo 的。
如今讓咱們回過頭來想一下,在 ClientWorker 中的定時任務中,啓動了一個長輪詢的任務:LongPollingRunnable,該任務屢次執行了 cacheData.checkListenerMd5() 方法,那如今就讓咱們來看下這個方法到底作了些什麼,以下圖所示:
到這裏應該就比較清晰了,該方法會檢查 CacheData 當前的 md5 與 CacheData 持有的全部 Listener 中保存的 md5 的值是否一致,若是不一致,就執行一個安全的監聽器的通知方法:safeNotifyListener,通知什麼呢?咱們能夠大膽的猜一下,應該是通知 Listener 的使用者,該 Listener 所關注的配置信息已經發生改變了。如今讓咱們來看一下 safeNotifyListener 方法,以下圖所示:
能夠看到在 safeNotifyListener 方法中,重點關注下紅框中的三行代碼:獲取最新的配置信息,調用 Listener 的回調方法,將最新的配置信息做爲參數傳入,這樣 Listener 的使用者就能接收到變動後的配置信息了,最後更新 ListenerWrap 的 md5 值。和咱們猜想的同樣, Listener 的回調方法就是在該方法中觸發的。
那 CacheData 的 md5 值是什麼時候發生改變的呢?咱們能夠回想一下,在上面的 LongPollingRunnable 所執行的任務中,在獲取服務端發生變動的配置信息時,將最新的 content 數據寫入了 CacheData 中,咱們能夠看下該方法以下:
能夠看到是在長輪詢的任務中,當服務端配置信息發生變動時,客戶端將最新的數據獲取下來以後,保存在了 CacheData 中,同時更新了該 CacheData 的 md5 值,因此當下次執行 checkListenerMd5 方法時,就會發現當前 listener 所持有的 md5 值已經和 CacheData 的 md5 值不同了,也就意味着服務端的配置信息發生改變了,這時就須要將最新的數據通知給 Listener 的持有者。
至此配置中心的完整流程已經分析完畢了,能夠發現,Nacos 並非經過推的方式將服務端最新的配置信息發送給客戶端的,而是客戶端維護了一個長輪詢的任務,定時去拉取發生變動的配置信息,而後將最新的數據推送給 Listener 的持有者。
客戶端拉取服務端的數據與服務端推送數據給客戶端相比,優點在哪呢,爲何 Nacos 不設計成主動推送數據,而是要客戶端去拉取呢?若是用推的方式,服務端須要維持與客戶端的長鏈接,這樣的話須要耗費大量的資源,而且還須要考慮鏈接的有效性,例如須要經過心跳來維持二者之間的鏈接。而用拉的方式,客戶端只須要經過一個無狀態的 http 請求便可獲取到服務端的數據。
Nacos 服務端建立了相關的配置項後,客戶端就能夠進行監聽了。
客戶端是經過一個定時任務來檢查本身監聽的配置項的數據的,一旦服務端的數據發生變化時,客戶端將會獲取到最新的數據,並將最新的數據保存在一個 CacheData 對象中,而後會從新計算 CacheData 的 md5 屬性的值,此時就會對該 CacheData 所綁定的 Listener 觸發 receiveConfigInfo 回調。
客戶端是經過一個定時任務來檢查本身監聽的配置項的數據的(每10秒檢查一次) 檢查 CacheData 中的md5 和每一個Listener 持有的md5 是否相同,若是不一樣 說明服務端的配置發生了改變,這個時候咱們就得去拉取新的服務端的數據並刷新(@RefreshScope 這個後面再學習),那麼咱們還得想 這個md5是何時變的呢?這個就是咱們前面說的那個長鏈接了,這個長鏈接的超時時間是30s而且在這個方法的final方法中是調用本身,說明這個輪詢會一直跑下去,這樣每次服務端 有配置變動就可用通知的到客戶端了
考慮到服務端故障的問題,客戶端將最新數據獲取後會保存在本地的 snapshot 文件中,之後會優先從文件中獲取配置信息的值
今天咱們就學到這裏了,今天把客戶端的學了一下,明天咱們就來看看服務端怎麼參與到這個自動配置刷新中來,再後面 咱們還得結合spring springCloud的一塊兒來分析分析。
好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是真粉。
創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見
六脈神劍 | 文 【原創】若是本篇博客有任何錯誤,請批評指教,不勝感激 !