Nacos注冊中心和配置中心的底層原理全面解讀
臨時(shí)實(shí)例和永久實(shí)例
臨時(shí)實(shí)例
在注冊到注冊中心之后僅僅只保存在服務(wù)端內(nèi)部一個(gè)緩存中,不會(huì)持久化到磁盤
這個(gè)服務(wù)端內(nèi)部的緩存在注冊中心屆一般被稱為服務(wù)注冊表
當(dāng)服務(wù)實(shí)例出現(xiàn)異?;蛘呦戮€之后,就會(huì)把這個(gè)服務(wù)實(shí)例從服務(wù)注冊表中剔除
永久服務(wù)
實(shí)例不僅僅會(huì)存在服務(wù)注冊表中,同時(shí)也會(huì)被持久化到磁盤文件中
當(dāng)服務(wù)實(shí)例出現(xiàn)異常或者下線,Nacos 只會(huì)將服務(wù)實(shí)例的健康狀態(tài)設(shè)置為不健康,并不會(huì)對將其從服務(wù)注冊表中剔除
所以這個(gè)服務(wù)實(shí)例的信息你還是可以從注冊中心看到,只不過處于不健康狀態(tài)
為什么 Nacos 要將服務(wù)實(shí)例分為臨時(shí)實(shí)例和永久實(shí)例?
臨時(shí)實(shí)例就比較適合于業(yè)務(wù)服務(wù),服務(wù)下線之后可以不需要在注冊中心中查看到
永久實(shí)例就比較適合需要運(yùn)維的服務(wù),這種服務(wù)幾乎是永久存在的,比如說 MySQL、Redis 等等
當(dāng)然如果你想改成永久實(shí)例,可以通過下面這個(gè)配置項(xiàng)來完成
spring
cloud:
nacos:
discovery: #ephemeral單詞是臨時(shí)的意思,設(shè)置成false,就是永久實(shí)例了 ephemeral: false1.x 版本和2.x版本的區(qū)別

在 1.x 版本中,一個(gè)服務(wù)中可以既有臨時(shí)實(shí)例也有永久實(shí)例,服務(wù)實(shí)例是永久還是臨時(shí)是由服務(wù)實(shí)例本身決定的
但是 2.x 版本中,一個(gè)服務(wù)中的所有實(shí)例要么都是臨時(shí)的要么都是永久的,是由服務(wù)決定的,而不是具體的服務(wù)實(shí)例
服務(wù)注冊
作為一個(gè)服務(wù)注冊中心,服務(wù)注冊肯定是一個(gè)非常重要的功能
所謂的服務(wù)注冊,就是通過注冊中心提供的客戶端 SDK(或者是控制臺(tái))將服務(wù)本身的一些元信息,比如 ip、端口等信息發(fā)送到注冊中心服務(wù)端
服務(wù)端在接收到服務(wù)之后,會(huì)將服務(wù)的信息保存到前面提到的服務(wù)注冊表中
1.x 版本的實(shí)現(xiàn)
服務(wù)注冊是通過 Http 接口實(shí)現(xiàn)的,Nacos 服務(wù)端本身就是用 SpringBoot 寫的

代碼如下

2.x 版本的實(shí)現(xiàn)
2.x 版本相比于 1.x 版本最主要的升級就是客戶端和服務(wù)端通信協(xié)議的改變,由 1.x 版本的 Http 改成了 2.x 版本 gRPC
之所以改成了 gRPC,主要是因?yàn)?Http 請求會(huì)頻繁創(chuàng)建和銷毀連接,白白浪費(fèi)資源
所以在 2.x 版本之后,為了提升性能,就將通信協(xié)議改成了 gRPC
根據(jù)官網(wǎng)顯示,整體的效果還是很明顯,相比于 1.x 版本,注冊性能總體提升至少 2 倍
具體實(shí)現(xiàn)
Nacos 客戶端在啟動(dòng)的時(shí)候,會(huì)通過 gRPC 跟服務(wù)端建立長連接

當(dāng)客戶端發(fā)起注冊的時(shí)候,就會(huì)通過這個(gè)長連接,將服務(wù)實(shí)例的信息發(fā)送給服務(wù)端
服務(wù)端拿到服務(wù)實(shí)例,跟 1.x 一樣,也會(huì)存到服務(wù)注冊表
除了注冊之外,當(dāng)注冊的是臨時(shí)實(shí)例時(shí),2.x 還會(huì)將服務(wù)實(shí)例信息存儲(chǔ)到客戶端中的一個(gè)緩存中,供 Redo 操作
所謂的 Redo 操作,其實(shí)就是一個(gè)補(bǔ)償機(jī)制,本質(zhì)是個(gè)定時(shí)任務(wù),默認(rèn)每 3s 執(zhí)行一次
這個(gè)定時(shí)任務(wù)作用是,當(dāng)客戶端與服務(wù)端重新建立連接時(shí)(因?yàn)橐恍┊惓T驅(qū)е逻B接斷開)
那么之前注冊的服務(wù)實(shí)例肯定還要繼續(xù)注冊服務(wù)端(斷開連接服務(wù)實(shí)例就會(huì)被剔除服務(wù)注冊表)
所以這個(gè) Redo 操作一個(gè)很重要的作用就是重連之后的重新注冊的作用
除了注冊之外,比如服務(wù)訂閱之類的操作也需要 Redo 操作,當(dāng)連接重新建立,之前客戶端的操作都需要 Redo 一下
心跳機(jī)制(直接解決了臨時(shí)實(shí)例的心跳機(jī)制)
心跳機(jī)制,也可以被稱為?;顧C(jī)制,它的作用就是服務(wù)實(shí)例告訴注冊中心我這個(gè)服務(wù)實(shí)例還活著

在正常情況下,服務(wù)關(guān)閉了,那么服務(wù)會(huì)主動(dòng)向 Nacos 服務(wù)端發(fā)送一個(gè)服務(wù)下線的請求
Nacos 服務(wù)端在接收到請求之后,會(huì)將這個(gè)服務(wù)實(shí)例從服務(wù)注冊表中剔除
但是對于異常情況下,比如出現(xiàn)網(wǎng)絡(luò)問題,可能導(dǎo)致這個(gè)注冊的服務(wù)實(shí)例無法提供服務(wù),處于不可用狀態(tài),也就是不健康
而此時(shí)在沒有任何機(jī)制的情況下,服務(wù)端是無法知道這個(gè)服務(wù)處于不可用狀態(tài)
所以為了避免這種情況,一些注冊中心,就比如 Nacos、Eureka,就會(huì)用心跳機(jī)制來判斷這個(gè)服務(wù)實(shí)例是否能正常
在 Nacos 中,心跳機(jī)制僅僅是針對臨時(shí)實(shí)例來說的,臨時(shí)實(shí)例需要靠心跳機(jī)制來保活
1.x 心跳實(shí)現(xiàn)
在 1.x 中,心跳機(jī)制實(shí)現(xiàn)是通過客戶端和服務(wù)端各存在的一個(gè)定時(shí)任務(wù)來完成的
在服務(wù)注冊時(shí),發(fā)現(xiàn)是臨時(shí)實(shí)例,客戶端會(huì)開啟一個(gè) 5s 執(zhí)行一次的定時(shí)任務(wù)

這個(gè)定時(shí)任務(wù)會(huì)構(gòu)建一個(gè) Http 請求,攜帶這個(gè)服務(wù)實(shí)例的信息,然后發(fā)送到服務(wù)端

在 Nacos 服務(wù)端也會(huì)開啟一個(gè)定時(shí)任務(wù),默認(rèn)也是 5s 執(zhí)行一次,去檢查這些服務(wù)實(shí)例最后一次心跳的時(shí)間,也就是客戶端最后一次發(fā)送 Http 請求的時(shí)間
- 當(dāng)最后一次心跳時(shí)間超過 15s,但沒有超過 30s,會(huì)把這服務(wù)實(shí)例標(biāo)記成不健康
- 當(dāng)最后一次心跳超過 30s,直接把服務(wù)從服務(wù)注冊表中剔除

1.x 版本的心跳機(jī)制,本質(zhì)就是兩個(gè)定時(shí)任務(wù)
其實(shí) 1.x 的這個(gè)心跳還有一個(gè)作用,就是跟上一節(jié)說的 gRPC 時(shí) Redo 操作的作用是一樣的
服務(wù)在處理心跳的時(shí)候,發(fā)現(xiàn)心跳攜帶這個(gè)服務(wù)實(shí)例的信息在注冊表中沒有,此時(shí)就會(huì)添加到服務(wù)注冊表
所以心跳也有 Redo 的類似效果
2.x 心跳實(shí)現(xiàn)(兼容1.x版本心跳機(jī)制,如果客戶端使用的SDK是1.x的情況下)
在 2.x 版本之后,由于通信協(xié)議改成了 gRPC,客戶端與服務(wù)端保持長連接,所以 2.x 版本之后它是利用這個(gè) gRPC 長連接本身的心跳來?;?/strong>
一旦這個(gè)連接斷開,服務(wù)端就會(huì)認(rèn)為這個(gè)連接注冊的服務(wù)實(shí)例不可用,之后就會(huì)將這個(gè)服務(wù)實(shí)例從服務(wù)注冊表中提出剔除
除了連接本身的心跳之外,Nacos 還有服務(wù)端的一個(gè)主動(dòng)檢測機(jī)制
Nacos 服務(wù)端也會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù),默認(rèn)每隔 3s 執(zhí)行一次
這個(gè)任務(wù)會(huì)去檢查超過 20s 沒有發(fā)送請求數(shù)據(jù)的連接
一旦發(fā)現(xiàn)有連接已經(jīng)超過 20s 沒發(fā)送請求,那么就會(huì)向這個(gè)連接對應(yīng)的客戶端發(fā)送一個(gè)請求
如果請求不通或者響應(yīng)失敗,此時(shí)服務(wù)端也會(huì)認(rèn)為與客戶端的這個(gè)連接異常,從而將這個(gè)客戶端注冊的服務(wù)實(shí)例從服務(wù)注冊表中剔除
所以對于 2.x 版本,主要是兩種機(jī)制來進(jìn)行?;睿?/p>
基于gRPC長連接雙向活躍檢測:gRPC 連接未立即斷開,但數(shù)據(jù)無法完整往返
- 客戶端進(jìn)程卡死但心跳線程存活(假心跳場景) 例如:應(yīng)用主線程死鎖,但心跳線程仍能發(fā)送TCP包,此時(shí)服務(wù)實(shí)際不可用,但心跳正常。
- 網(wǎng)絡(luò)隔離(Network Partition)(假心跳場景) 客戶端與Nacos Server之間出現(xiàn)單向網(wǎng)絡(luò)故障(客戶端能發(fā)數(shù)據(jù),但收不到響應(yīng))。
Nacos 主動(dòng)檢查機(jī)制(連接存活但不活躍),服務(wù)端會(huì)對 20s 沒有發(fā)送數(shù)據(jù)的連接進(jìn)行檢查,出現(xiàn)異常時(shí)也會(huì)主動(dòng)斷開連接,剔除服務(wù)實(shí)例
健康檢查(為解決永久實(shí)例的心跳機(jī)制)
心跳機(jī)制僅僅是臨時(shí)實(shí)例用來保護(hù)的機(jī)制
而對于永久實(shí)例來說,一般來說無法主動(dòng)上報(bào)心跳
就比如說 MySQL 實(shí)例,肯定是不會(huì)主動(dòng)上報(bào)心跳到 Nacos 的,所以這就導(dǎo)致無法通過心跳機(jī)制來?;?/p>
所以針對永久實(shí)例的情況,Nacos 通過一種叫健康檢查的機(jī)制去判斷服務(wù)實(shí)例是否活著
健康檢查跟心跳機(jī)制剛好相反,心跳機(jī)制是服務(wù)實(shí)例向服務(wù)端發(fā)送請求
而所謂的健康檢查就是服務(wù)端主動(dòng)向服務(wù)實(shí)例發(fā)送請求,去探測服務(wù)實(shí)例是否活著

健康檢查機(jī)制在 1.x 和 2.x 的實(shí)現(xiàn)機(jī)制是一樣的
Nacos 服務(wù)端在會(huì)去創(chuàng)建一個(gè)健康檢查任務(wù),這個(gè)任務(wù)每次執(zhí)行時(shí)間間隔會(huì)在 2000~7000 毫秒之間
當(dāng)任務(wù)觸發(fā)的時(shí)候,會(huì)根據(jù)設(shè)置的健康檢查的方式執(zhí)行不同的邏輯,目前主要有以下三種方式:
TCP
- 根據(jù)服務(wù)實(shí)例的 ip 和端口去判斷是否能連接成功,如果連接成功,就認(rèn)為健康,反之就任務(wù)不健康
HTTP
- 向服務(wù)實(shí)例的 ip 和端口發(fā)送一個(gè) Http 請求,請求路徑是需要設(shè)置的,如果能正常請求,說明實(shí)例健康,反之就不健康
MySQL
- 一種特殊的檢查方式,他可以執(zhí)行下面這條 Sql 來判斷數(shù)據(jù)庫是不是主庫

默認(rèn)情況下,都是通過 TCP 的方式來探測服務(wù)實(shí)例是否還活著
服務(wù)發(fā)現(xiàn)
所謂的服務(wù)發(fā)現(xiàn)就是指當(dāng)有服務(wù)實(shí)例注冊成功之后,其它服務(wù)可以發(fā)現(xiàn)這些服務(wù)實(shí)例
Nacos 提供了兩種發(fā)現(xiàn)方式:
主動(dòng)查詢
- 指客戶端主動(dòng)向服務(wù)端查詢需要關(guān)注的服務(wù)實(shí)例,也就是拉(pull)的模式
服務(wù)訂閱
- 指客戶端向服務(wù)端發(fā)送一個(gè)訂閱服務(wù)的請求,當(dāng)被訂閱的服務(wù)有信息變動(dòng)就會(huì)主動(dòng)將服務(wù)實(shí)例的信息推送給訂閱的客戶端,本質(zhì)就是推(push)模式

在我們平時(shí)使用時(shí),一般來說都是選擇使用訂閱的方式,這樣一旦有服務(wù)實(shí)例數(shù)據(jù)的變動(dòng),客戶端能夠第一時(shí)間感知
并且 Nacos 在整合 SpringCloud 的時(shí)候,默認(rèn)就是使用訂閱的方式
對于這兩種服務(wù)發(fā)現(xiàn)方式,1.x 和 2.x 版本實(shí)現(xiàn)也是不一樣
服務(wù)(主動(dòng))查詢
1.x 整體就是發(fā)送 Http 請求去查詢服務(wù)實(shí)例,2.x 只不過是將 Http 請求換成了 gRPC 的請求
服務(wù)端對于查詢的處理過程都是一樣的,從服務(wù)注冊表中查出符合查詢條件的服務(wù)實(shí)例進(jìn)行返回
服務(wù)訂閱
不過對于服務(wù)訂閱,兩者的機(jī)制就稍微復(fù)雜一點(diǎn)
不論是 1.x 還是 2.x 都是通過 SDK 中的NamingService#subscribe方法來發(fā)起訂閱的

當(dāng)有服務(wù)實(shí)例數(shù)據(jù)變動(dòng)的時(shí),客戶端就會(huì)回調(diào)EventListener,就可以拿到最新的服務(wù)實(shí)例數(shù)據(jù)了
1.x服務(wù)發(fā)現(xiàn)訂閱實(shí)現(xiàn)
客戶端在啟動(dòng)的時(shí)候,會(huì)去構(gòu)建一個(gè)叫 PushReceiver 的類
- 這個(gè)類會(huì)去創(chuàng)建一個(gè) UDP Socket,端口是隨機(jī)的
- 作用:通過 UDP 的方式接收服務(wù)端推送的數(shù)據(jù)的

調(diào)用NamingService#subscribe來發(fā)起訂閱時(shí),會(huì)先去服務(wù)端查詢需要訂閱服務(wù)的所有實(shí)例信息之后會(huì)將所有服務(wù)實(shí)例數(shù)據(jù)存到客戶端的一個(gè)內(nèi)部緩存中
- 并且在查詢的時(shí)候,會(huì)將這個(gè) UDP Socket 的端口作為一個(gè)參數(shù)傳到服務(wù)端
- 服務(wù)端接收到這個(gè) UDP 端口后,后續(xù)就通過這個(gè)端口給客戶端推送服務(wù)實(shí)例數(shù)據(jù)
會(huì)為這次訂閱開啟一個(gè)不定時(shí)執(zhí)行的任務(wù)
之所以不定時(shí),是因?yàn)檫@個(gè)當(dāng)執(zhí)行異常的時(shí)候,下次執(zhí)行的時(shí)間間隔就會(huì)變長,但是最多不超過 60s,正常是 10s,這個(gè) 10s 是查詢服務(wù)實(shí)例是服務(wù)端返回的
這個(gè)任務(wù)會(huì)去從服務(wù)端查詢訂閱的服務(wù)實(shí)例信息,然后更新內(nèi)部緩存
既然有了服務(wù)變動(dòng)推送的功能,為什么還要定時(shí)去查詢更新服務(wù)實(shí)例信息呢?
- 那就是因?yàn)?UDP 通信不穩(wěn)定導(dǎo)致的
- 雖然有 Push,但是由于 UDP 通信自身的不確定性,有可能會(huì)導(dǎo)致客戶端接收變動(dòng)信息失敗
- 所以這里就加了一個(gè)定時(shí)任務(wù),彌補(bǔ)這種可能性,屬于一個(gè)兜底的方案。
2.x服務(wù)發(fā)現(xiàn)訂閱實(shí)現(xiàn)
由于 2.x 版本換成了 gRPC 長連接的方式,所以 2.x 版本服務(wù)數(shù)據(jù)變更推送已經(jīng)完全拋棄了 1.x 的 UDP 做法
當(dāng)有服務(wù)實(shí)例變動(dòng)的時(shí)候,服務(wù)端直接通過這個(gè)長連接將服務(wù)信息發(fā)送給客戶端
客戶端拿到最新服務(wù)實(shí)例數(shù)據(jù)之后的處理方式就跟 1.x 是一樣了
除了處理方式一樣,2.x 也繼承了 1.x 的其他的東西
比如客戶端依然會(huì)有服務(wù)實(shí)例的緩存
定時(shí)對比機(jī)制也保留了,只不過這個(gè)定時(shí)對比的機(jī)制默認(rèn)是關(guān)閉狀態(tài)
之所以默認(rèn)關(guān)閉,主要還是因?yàn)?/strong>長連接還是比較穩(wěn)定的原因
當(dāng)客戶端出現(xiàn)異常,接收不到請求,那么服務(wù)端會(huì)直接跟客戶端斷開連接
當(dāng)恢復(fù)正常,由于有 Redo 操作,所以還是能拿到最新的實(shí)例信息的

細(xì)節(jié)
在 1.x 版本的時(shí)候,任何服務(wù)都是可以被訂閱的
但是在 2.x 版本中,只支持訂閱臨時(shí)服務(wù),對于永久服務(wù),已經(jīng)不支持訂閱了
數(shù)據(jù)一致性
由于 Nacos 是支持集群模式的,所以一定會(huì)涉及到分布式系統(tǒng)中不可避免的數(shù)據(jù)一致性問題
服務(wù)實(shí)例的責(zé)任機(jī)制
什么是服務(wù)實(shí)例的責(zé)任機(jī)制?
比如上面提到的服務(wù)注冊、心跳管理、監(jiān)控檢查機(jī)制,當(dāng)只有一個(gè) Nacos 服務(wù)時(shí),那么自然而言這個(gè)服務(wù)會(huì)去檢查所有的服務(wù)實(shí)例的心跳時(shí)間,執(zhí)行所有服務(wù)實(shí)例的健康檢查任務(wù)

但是當(dāng)出現(xiàn) Nacos 服務(wù)出現(xiàn)集群時(shí),為了平衡各 Nacos 服務(wù)的壓力,Nacos 會(huì)根據(jù)一定的規(guī)則讓每個(gè) Nacos 服務(wù)只管理一部分服務(wù)實(shí)例的
當(dāng)然每個(gè) Nacos 服務(wù)的注冊表還是全部的服務(wù)實(shí)例數(shù)據(jù)

這個(gè)管理機(jī)制我給他起了一個(gè)名字,就叫做責(zé)任機(jī)制,因?yàn)槲以?1.x 和 2.x 都提到了responsible這個(gè)單詞
本質(zhì)就是 Nacos 服務(wù)對哪些服務(wù)實(shí)例負(fù)有心跳監(jiān)測,健康檢查的責(zé)任。
BASE 理論(CAP妥協(xié)之后的產(chǎn)物)
- 基本可用(Basically Available):系統(tǒng)出現(xiàn)故障還是能夠?qū)ν馓峁┓?wù),不至于直接無法用了
- 軟狀態(tài)(Soft State):允許各個(gè)節(jié)點(diǎn)的數(shù)據(jù)短暫的不一致
- 最終一致性,(Eventually Consistent):雖然允許各個(gè)節(jié)點(diǎn)的數(shù)據(jù)不一致,但是在一定時(shí)間之后,各個(gè)節(jié)點(diǎn)的數(shù)據(jù)最終需要一致的
Nacos 的 AP 和 CP
Nacos 其實(shí)目前是同時(shí)支持 AP 和 CP 的
具體使用 AP 還是 CP 得取決于 Nacos 內(nèi)部的具體功能,并不是有的文章說的可以通過一個(gè)配置自由切換。
就以服務(wù)注冊舉例來說,對于臨時(shí)實(shí)例來說,Nacos 會(huì)優(yōu)先保證可用性,也就是 AP
對于永久實(shí)例,Nacos 會(huì)優(yōu)先保證數(shù)據(jù)的一致性,也就是 CP
Nacos 的 AP 實(shí)現(xiàn)
對于 AP 來說,Nacos 使用的是阿里自研的 Distro 協(xié)議
在這個(gè)協(xié)議中,每個(gè)服務(wù)端節(jié)點(diǎn)是一個(gè)平等的狀態(tài),每個(gè)服務(wù)端節(jié)點(diǎn)正常情況下數(shù)據(jù)是一樣的,每個(gè)服務(wù)端節(jié)點(diǎn)都可以接收來自客戶端的讀寫請求

當(dāng)某個(gè)節(jié)點(diǎn)剛啟動(dòng)時(shí),他會(huì)向集群中的某個(gè)節(jié)點(diǎn)發(fā)送請求,拉取所有的服務(wù)實(shí)例數(shù)據(jù)到自己的服務(wù)注冊表中

這樣其它客戶端就可以從這個(gè)服務(wù)節(jié)點(diǎn)中獲取到服務(wù)實(shí)例數(shù)據(jù)了
當(dāng)某個(gè)服務(wù)端節(jié)點(diǎn)接收到注冊臨時(shí)服務(wù)實(shí)例的請求,不僅僅會(huì)將這個(gè)服務(wù)實(shí)例存到自身的服務(wù)注冊表,同時(shí)也會(huì)向其它所有服務(wù)節(jié)點(diǎn)發(fā)送請求,將這個(gè)服務(wù)數(shù)據(jù)同步到其它所有節(jié)點(diǎn)

所以此時(shí)從任意一個(gè)節(jié)點(diǎn)都是可以獲取到所有的服務(wù)實(shí)例數(shù)據(jù)的。
即使數(shù)據(jù)同步的過程發(fā)生異常,服務(wù)實(shí)例也成功注冊到一個(gè) Nacos 服務(wù)中,對外部而言,整個(gè) Nacos 集群是可用的,也就達(dá)到了 AP 的效果
同時(shí)為了滿足 BASE 理論,Nacos 也有下面兩種機(jī)制保證最終節(jié)點(diǎn)間數(shù)據(jù)最終是一致的:
失敗重試機(jī)制
- 數(shù)據(jù)同步給其它節(jié)點(diǎn)失敗時(shí),會(huì)每隔 3s 重試一次,直到成功
定時(shí)對比機(jī)制
- 每個(gè) Nacos 服務(wù)節(jié)點(diǎn)會(huì)定時(shí)向所有的其它服務(wù)節(jié)點(diǎn)發(fā)送一些認(rèn)證的請求
- 這個(gè)請求會(huì)告訴每個(gè)服務(wù)節(jié)點(diǎn)自己負(fù)責(zé)的服務(wù)實(shí)例的對應(yīng)的版本號,這個(gè)版本號隨著服務(wù)實(shí)例的變動(dòng)就會(huì)變動(dòng)
- 如果其它服務(wù)節(jié)點(diǎn)的數(shù)據(jù)的版本號跟自己的對不上,那就說明其它服務(wù)節(jié)點(diǎn)的數(shù)據(jù)不是最新的
- 此時(shí)這個(gè) Nacos 服務(wù)節(jié)點(diǎn)就會(huì)將自己負(fù)責(zé)的服務(wù)實(shí)例數(shù)據(jù)發(fā)給不是最新數(shù)據(jù)的節(jié)點(diǎn),這樣就保證了每個(gè)節(jié)點(diǎn)的數(shù)據(jù)是一樣的了。
Nacos 的 CP 實(shí)現(xiàn)
Nacos 的 CP 實(shí)現(xiàn)是基于 Raft 算法來實(shí)現(xiàn)的
在 1.x 版本早期,Nacos 是自己手動(dòng)實(shí)現(xiàn) Raft 算法
在 2.x 版本,Nacos 移除了手動(dòng)實(shí)現(xiàn) Raft 算法,轉(zhuǎn)而擁抱基于螞蟻開源的 JRaft 框架
在 Raft 算法,每個(gè)節(jié)點(diǎn)主要有三個(gè)狀態(tài)
- Leader,負(fù)責(zé)所有的讀寫請求,一個(gè)集群只有一個(gè)
- Follower,從節(jié)點(diǎn),主要是負(fù)責(zé)復(fù)制 Leader 的數(shù)據(jù),保證數(shù)據(jù)的一致性
- Candidate,候選節(jié)點(diǎn),最終會(huì)變成 Leader 或者 Follower
集群啟動(dòng)時(shí)都是節(jié)點(diǎn) Follower,經(jīng)過一段時(shí)間會(huì)轉(zhuǎn)換成 Candidate 狀態(tài),再經(jīng)過一系列復(fù)雜的選擇算法,選出一個(gè) Leader

當(dāng)有寫請求時(shí),如果請求的節(jié)點(diǎn)不是 Leader 節(jié)點(diǎn)時(shí),會(huì)將請求轉(zhuǎn)給 Leader 節(jié)點(diǎn),由 Leader 節(jié)點(diǎn)處理寫請求
比如,有個(gè)客戶端連到的上圖中的Nacos服務(wù)2節(jié)點(diǎn),之后向Nacos服務(wù)2注冊服務(wù)
Nacos服務(wù)2接收到請求之后,會(huì)判斷自己是不是 Leader 節(jié)點(diǎn),發(fā)現(xiàn)自己不是
此時(shí)Nacos服務(wù)2就會(huì)向 Leader 節(jié)點(diǎn)發(fā)送請求,Leader 節(jié)點(diǎn)接收到請求之后,會(huì)處理服務(wù)注冊的過程
為什么說 Raft 是保證 CP 的呢?
主要是因?yàn)?Raft 在處理寫的時(shí)候有一個(gè)判斷過程
- 首先,Leader 在處理寫請求時(shí),不會(huì)直接數(shù)據(jù)應(yīng)用到自己的系統(tǒng),而是先向所有的 Follower 發(fā)送請求,讓他們先處理這個(gè)請求
- 當(dāng)超過半數(shù)的 Follower 成功處理了這個(gè)寫請求之后,Leader 才會(huì)寫數(shù)據(jù),并返回給客戶端請求處理成功
- 如果超過一定時(shí)間未收到超過半數(shù)處理成功 Follower 的信號,此時(shí) Leader 認(rèn)為這次寫數(shù)據(jù)是失敗的,就不會(huì)處理寫請求,直接返回給客戶端請求失敗
小細(xì)節(jié)需要注意
Nacos 在處理查詢服務(wù)實(shí)例的請求直接時(shí),并不會(huì)將請求轉(zhuǎn)發(fā)給 Leader 節(jié)點(diǎn)處理,而是直接查當(dāng)前 Nacos 服務(wù)實(shí)例的注冊表
這其實(shí)就會(huì)引發(fā)一個(gè)問題
如果客戶端查詢的 Follower 節(jié)點(diǎn)沒有及時(shí)處理 Leader 同步過來的寫請求(過半響應(yīng)的節(jié)點(diǎn)中不包括這個(gè)節(jié)點(diǎn)),此時(shí)在這個(gè) Follower 其實(shí)是查不到最新的數(shù)據(jù)的,這就會(huì)導(dǎo)致數(shù)據(jù)的不一致
所以說,雖然 Raft 協(xié)議規(guī)定要求從 Leader 節(jié)點(diǎn)查最新的數(shù)據(jù),但是 Nacos 至少在讀服務(wù)實(shí)例數(shù)據(jù)時(shí)并沒有遵守這個(gè)協(xié)議
當(dāng)然對于其它的一些數(shù)據(jù)的讀寫請求有的還是遵守了這個(gè)協(xié)議。
數(shù)據(jù)模型
在 Nacos 中,一個(gè)服務(wù)的確定是由三部分信息確定
- 命名空間(Namespace):多租戶隔離用的,默認(rèn)是
public - 分組(Group):這個(gè)其實(shí)可以用來做環(huán)境隔離,服務(wù)注冊時(shí)可以指定服務(wù)的分組,比如是測試環(huán)境或者是開發(fā)環(huán)境,默認(rèn)是
DEFAULT_GROUP - 服務(wù)名(ServiceName):這個(gè)就不用多說了
在服務(wù)注冊和訂閱的時(shí)候,必須要指定上述三部分信息,如果不指定,Nacos 就會(huì)提供默認(rèn)的信息
不過,在 Nacos 中,在服務(wù)里面其實(shí)還是有一個(gè)集群的概念

在服務(wù)注冊的時(shí)候,可以指定這個(gè)服務(wù)實(shí)例在哪個(gè)集體的集群中,默認(rèn)是在DEFAULT集群下
在 SpringCloud 環(huán)境底下可以通過如下配置去設(shè)置
spring
cloud:
nacos:
discovery:
cluster-name: sanyoujavaCluster配置中心
Spring Boot 應(yīng)用
↓
Spring Cloud Nacos Config
↓
ConfigService(客戶端核心類)
↓
LongPollingRunnable(長輪詢?nèi)蝿?wù))
↓
HTTP 請求 /nacos/v1/cs/configs/listener
↓
Nacos Server 檢測配置變化
↓
返回新配置 → 客戶端觸發(fā) Listener → 事件驅(qū)動(dòng)更新 BeanNacos 配置中心是支持配置項(xiàng)自動(dòng)刷新的,而其實(shí)現(xiàn)的原理是通過長輪詢+事件驅(qū)動(dòng)+本地回調(diào)機(jī)制的方式來實(shí)現(xiàn)的,具體來說:
- 客戶端向 Nacos 服務(wù)器發(fā)送一個(gè)帶有監(jiān)聽器(Listener)的請求,以獲取某個(gè)特定配置的值。
- Nacos 服務(wù)器接收到請求后,會(huì)檢查該配置是否發(fā)生了變化。如果沒有變化,則該請求將被阻塞,直到超時(shí)或配置發(fā)生變化。
- 當(dāng)配置發(fā)生變化時(shí),Nacos 服務(wù)器會(huì)立即響應(yīng),并將新的配置值返回給客戶端。
- 客戶端接收到新的配置值后,可以根據(jù)需要更新自身的配置。
長輪詢:服務(wù)器端接收到客戶端的請求之后,如果沒有數(shù)據(jù)更新,則連接保持一段時(shí)間,直到有數(shù)據(jù)或者超時(shí)才會(huì)返回。
gRPC 長連接是 Nacos 2.x 的推薦通信方式,性能更優(yōu),但為保證兼容性、適配多語言客戶端和輕量場景,Nacos 仍然保留了 HTTP 長輪詢機(jī)制。兩者可以共存,動(dòng)態(tài)選擇,適配更廣泛的實(shí)際業(yè)務(wù)場景。
| 機(jī)制 | 說明 |
| 長輪詢 | 保證實(shí)時(shí)監(jiān)聽變化,服務(wù)端主動(dòng)推送 |
| 客戶端本地緩存 | 保證容錯(cuò)、降級能力 |
| Bean 自動(dòng)刷新 | 與 Spring 深度集成,支持注解級別的動(dòng)態(tài)刷新 |
| 異步監(jiān)聽線程 | 保證主線程業(yè)務(wù)不被阻塞 |
如何集成 Nacos Config 實(shí)現(xiàn)配置項(xiàng)動(dòng)態(tài)刷新?
使用 @NacosValue 注解注入配置
import com.alibaba.nacos.api.config.annotation.NacosValue;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class NacosValueExample {
@NacosValue(value = "${my.config.value}", autoRefreshed = true)
private String configValue;
@PostConstruct
public void init() {
System.out.println("讀取到配置:" + configValue);
}
public String getConfigValue() {
return configValue;
}
}使用 @NacosConfigListener 手動(dòng)監(jiān)聽配置變更
import com.alibaba.nacos.api.config.annotation.NacosConfigListener;
import org.springframework.stereotype.Component;
@Component
public class NacosListenerExample {
@NacosConfigListener(dataId = "my-config.yaml", groupId = "DEFAULT_GROUP")
public void onChange(String newConfig) {
System.out.println("配置發(fā)生變化,新內(nèi)容為:" + newConfig);
// 你可以在這里解析 YAML 并手動(dòng)更新 Bean 或緩存
}
}使用 @ConfigurationProperties + @RefreshScope
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "my.config")
public class ConfigPropertiesExample {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}使用 bootstrap.yml 配置動(dòng)態(tài)讀取 Nacos 的配置
# bootstrap.yml
spring:
application:
name: nacos-demo # 用作默認(rèn)的 dataId:nacos-demo.yaml
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
group: DEFAULT_GROUP
namespace: public
refresh-enabled: true # 開啟全局自動(dòng)刷新
extension-configs:
- data-id: my-config.yaml
group: DEFAULT_GROUP
refresh: true # 支持動(dòng)態(tài)刷新
配置項(xiàng)作用
server-addrNacos 服務(wù)地址
file-extension默認(rèn) dataId 后綴,比如 nacos-demo.yaml
refresh-enabled全局開啟動(dòng)態(tài)刷新
extension-configs可以加載多個(gè)額外配置文件
refresh: true為該配置開啟動(dòng)態(tài)刷新Spring Boot 啟動(dòng)階段讀取 bootstrap.yml,初始化 Spring Cloud Nacos。
從配置中心讀取 dataId 對應(yīng)的配置(如 my-config.yaml),并注入到環(huán)境中。
如果設(shè)置了 refresh: true,則在配置變更時(shí),Nacos 會(huì)通過監(jiān)聽機(jī)制自動(dòng)刷新對應(yīng)的 Bean。
- 如果你用的是
@RefreshScope或@NacosValue(autoRefreshed = true),對應(yīng)字段會(huì)自動(dòng)更新。 - 也可以通過
/actuator/refresh手動(dòng)刷新(Spring Cloud 原生方式)。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
ElasticSearch6.2.3+head插件安裝的方法步驟
這篇文章主要介紹了ElasticSearch6.2.3+head插件安裝的方法步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
java中PreparedStatement和Statement詳細(xì)講解
這篇文章主要介紹了java中PreparedStatement和Statement詳細(xì)講解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
通過自定制LogManager實(shí)現(xiàn)程序完全自定義的logger
本章主要闡述怎么完全定制化LogManager來實(shí)現(xiàn)應(yīng)用程序完全自定制的logger,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
詳解SpringBoot中關(guān)于%2e的Trick
這篇文章主要介紹了SpringBoot中關(guān)于%2e的Trick,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
Spring MVC的項(xiàng)目準(zhǔn)備和連接建立方法
SpringWebMVC是基于Servlet API的Web框架,屬于Spring框架的一部分,主要用于簡化Web應(yīng)用程序的開發(fā),SpringMVC通過控制器接收請求,使用模型處理數(shù)據(jù),并通過視圖展示結(jié)果,感興趣的朋友跟隨小編一起看看吧2024-10-10

