Nacos注冊(cè)中心和配置中心的底層原理全面解讀
臨時(shí)實(shí)例和永久實(shí)例
臨時(shí)實(shí)例
在注冊(cè)到注冊(cè)中心之后僅僅只保存在服務(wù)端內(nèi)部一個(gè)緩存中,不會(huì)持久化到磁盤
這個(gè)服務(wù)端內(nèi)部的緩存在注冊(cè)中心屆一般被稱為服務(wù)注冊(cè)表
當(dāng)服務(wù)實(shí)例出現(xiàn)異?;蛘呦戮€之后,就會(huì)把這個(gè)服務(wù)實(shí)例從服務(wù)注冊(cè)表中剔除
永久服務(wù)
實(shí)例不僅僅會(huì)存在服務(wù)注冊(cè)表中,同時(shí)也會(huì)被持久化到磁盤文件中
當(dāng)服務(wù)實(shí)例出現(xiàn)異?;蛘呦戮€,Nacos 只會(huì)將服務(wù)實(shí)例的健康狀態(tài)設(shè)置為不健康,并不會(huì)對(duì)將其從服務(wù)注冊(cè)表中剔除
所以這個(gè)服務(wù)實(shí)例的信息你還是可以從注冊(cè)中心看到,只不過處于不健康狀態(tài)
為什么 Nacos 要將服務(wù)實(shí)例分為臨時(shí)實(shí)例和永久實(shí)例?
臨時(shí)實(shí)例就比較適合于業(yè)務(wù)服務(wù),服務(wù)下線之后可以不需要在注冊(cè)中心中查看到
永久實(shí)例就比較適合需要運(yùn)維的服務(wù),這種服務(wù)幾乎是永久存在的,比如說 MySQL、Redis 等等
當(dāng)然如果你想改成永久實(shí)例,可以通過下面這個(gè)配置項(xiàng)來完成
spring cloud: nacos: discovery: #ephemeral單詞是臨時(shí)的意思,設(shè)置成false,就是永久實(shí)例了 ephemeral: false
1.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ù)注冊(cè)
作為一個(gè)服務(wù)注冊(cè)中心,服務(wù)注冊(cè)肯定是一個(gè)非常重要的功能
所謂的服務(wù)注冊(cè),就是通過注冊(cè)中心提供的客戶端 SDK(或者是控制臺(tái))將服務(wù)本身的一些元信息,比如 ip、端口等信息發(fā)送到注冊(cè)中心服務(wù)端
服務(wù)端在接收到服務(wù)之后,會(huì)將服務(wù)的信息保存到前面提到的服務(wù)注冊(cè)表中
1.x 版本的實(shí)現(xiàn)
服務(wù)注冊(cè)是通過 Http 接口實(shí)現(xiàn)的,Nacos 服務(wù)端本身就是用 SpringBoot 寫的
代碼如下
2.x 版本的實(shí)現(xiàn)
2.x 版本相比于 1.x 版本最主要的升級(jí)就是客戶端和服務(wù)端通信協(xié)議的改變,由 1.x 版本的 Http 改成了 2.x 版本 gRPC
之所以改成了 gRPC,主要是因?yàn)?Http 請(qǐng)求會(huì)頻繁創(chuàng)建和銷毀連接,白白浪費(fèi)資源
所以在 2.x 版本之后,為了提升性能,就將通信協(xié)議改成了 gRPC
根據(jù)官網(wǎng)顯示,整體的效果還是很明顯,相比于 1.x 版本,注冊(cè)性能總體提升至少 2 倍
具體實(shí)現(xiàn)
Nacos 客戶端在啟動(dòng)的時(shí)候,會(huì)通過 gRPC 跟服務(wù)端建立長連接
當(dāng)客戶端發(fā)起注冊(cè)的時(shí)候,就會(huì)通過這個(gè)長連接,將服務(wù)實(shí)例的信息發(fā)送給服務(wù)端
服務(wù)端拿到服務(wù)實(shí)例,跟 1.x 一樣,也會(huì)存到服務(wù)注冊(cè)表
除了注冊(cè)之外,當(dāng)注冊(cè)的是臨時(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接斷開)
那么之前注冊(cè)的服務(wù)實(shí)例肯定還要繼續(xù)注冊(cè)服務(wù)端(斷開連接服務(wù)實(shí)例就會(huì)被剔除服務(wù)注冊(cè)表)
所以這個(gè) Redo 操作一個(gè)很重要的作用就是重連之后的重新注冊(cè)的作用
除了注冊(cè)之外,比如服務(wù)訂閱之類的操作也需要 Redo 操作,當(dāng)連接重新建立,之前客戶端的操作都需要 Redo 一下
心跳機(jī)制(直接解決了臨時(shí)實(shí)例的心跳機(jī)制)
心跳機(jī)制,也可以被稱為?;顧C(jī)制,它的作用就是服務(wù)實(shí)例告訴注冊(cè)中心我這個(gè)服務(wù)實(shí)例還活著
在正常情況下,服務(wù)關(guān)閉了,那么服務(wù)會(huì)主動(dòng)向 Nacos 服務(wù)端發(fā)送一個(gè)服務(wù)下線的請(qǐng)求
Nacos 服務(wù)端在接收到請(qǐng)求之后,會(huì)將這個(gè)服務(wù)實(shí)例從服務(wù)注冊(cè)表中剔除
但是對(duì)于異常情況下,比如出現(xiàn)網(wǎng)絡(luò)問題,可能導(dǎo)致這個(gè)注冊(cè)的服務(wù)實(shí)例無法提供服務(wù),處于不可用狀態(tài),也就是不健康
而此時(shí)在沒有任何機(jī)制的情況下,服務(wù)端是無法知道這個(gè)服務(wù)處于不可用狀態(tài)
所以為了避免這種情況,一些注冊(cè)中心,就比如 Nacos、Eureka,就會(huì)用心跳機(jī)制來判斷這個(gè)服務(wù)實(shí)例是否能正常
在 Nacos 中,心跳機(jī)制僅僅是針對(duì)臨時(shí)實(shí)例來說的,臨時(shí)實(shí)例需要靠心跳機(jī)制來?;?/strong>
1.x 心跳實(shí)現(xiàn)
在 1.x 中,心跳機(jī)制實(shí)現(xiàn)是通過客戶端和服務(wù)端各存在的一個(gè)定時(shí)任務(wù)來完成的
在服務(wù)注冊(cè)時(shí),發(fā)現(xiàn)是臨時(shí)實(shí)例,客戶端會(huì)開啟一個(gè) 5s 執(zhí)行一次的定時(shí)任務(wù)
這個(gè)定時(shí)任務(wù)會(huì)構(gòu)建一個(gè) Http 請(qǐng)求,攜帶這個(gè)服務(wù)實(shí)例的信息,然后發(fā)送到服務(wù)端
在 Nacos 服務(wù)端也會(huì)開啟一個(gè)定時(shí)任務(wù),默認(rèn)也是 5s 執(zhí)行一次,去檢查這些服務(wù)實(shí)例最后一次心跳的時(shí)間,也就是客戶端最后一次發(fā)送 Http 請(qǐng)求的時(shí)間
- 當(dāng)最后一次心跳時(shí)間超過 15s,但沒有超過 30s,會(huì)把這服務(wù)實(shí)例標(biāo)記成不健康
- 當(dāng)最后一次心跳超過 30s,直接把服務(wù)從服務(wù)注冊(cè)表中剔除
1.x 版本的心跳機(jī)制,本質(zhì)就是兩個(gè)定時(shí)任務(wù)
其實(shí) 1.x 的這個(gè)心跳還有一個(gè)作用,就是跟上一節(jié)說的 gRPC 時(shí) Redo 操作的作用是一樣的
服務(wù)在處理心跳的時(shí)候,發(fā)現(xiàn)心跳攜帶這個(gè)服務(wù)實(shí)例的信息在注冊(cè)表中沒有,此時(shí)就會(huì)添加到服務(wù)注冊(cè)表
所以心跳也有 Redo 的類似效果
2.x 心跳實(shí)現(xiàn)(兼容1.x版本心跳機(jī)制,如果客戶端使用的SDK是1.x的情況下)
在 2.x 版本之后,由于通信協(xié)議改成了 gRPC,客戶端與服務(wù)端保持長連接,所以 2.x 版本之后它是利用這個(gè) gRPC 長連接本身的心跳來保活
一旦這個(gè)連接斷開,服務(wù)端就會(huì)認(rèn)為這個(gè)連接注冊(cè)的服務(wù)實(shí)例不可用,之后就會(huì)將這個(gè)服務(wù)實(shí)例從服務(wù)注冊(cè)表中提出剔除
除了連接本身的心跳之外,Nacos 還有服務(wù)端的一個(gè)主動(dòng)檢測(cè)機(jī)制
Nacos 服務(wù)端也會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù),默認(rèn)每隔 3s 執(zhí)行一次
這個(gè)任務(wù)會(huì)去檢查超過 20s 沒有發(fā)送請(qǐng)求數(shù)據(jù)的連接
一旦發(fā)現(xiàn)有連接已經(jīng)超過 20s 沒發(fā)送請(qǐng)求,那么就會(huì)向這個(gè)連接對(duì)應(yīng)的客戶端發(fā)送一個(gè)請(qǐng)求
如果請(qǐng)求不通或者響應(yīng)失敗,此時(shí)服務(wù)端也會(huì)認(rèn)為與客戶端的這個(gè)連接異常,從而將這個(gè)客戶端注冊(cè)的服務(wù)實(shí)例從服務(wù)注冊(cè)表中剔除
所以對(duì)于 2.x 版本,主要是兩種機(jī)制來進(jìn)行?;睿?/p>
基于gRPC長連接雙向活躍檢測(cè):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ì)對(duì) 20s 沒有發(fā)送數(shù)據(jù)的連接進(jìn)行檢查,出現(xiàn)異常時(shí)也會(huì)主動(dòng)斷開連接,剔除服務(wù)實(shí)例
健康檢查(為解決永久實(shí)例的心跳機(jī)制)
心跳機(jī)制僅僅是臨時(shí)實(shí)例用來保護(hù)的機(jī)制
而對(duì)于永久實(shí)例來說,一般來說無法主動(dòng)上報(bào)心跳
就比如說 MySQL 實(shí)例,肯定是不會(huì)主動(dòng)上報(bào)心跳到 Nacos 的,所以這就導(dǎo)致無法通過心跳機(jī)制來?;?/p>
所以針對(duì)永久實(shí)例的情況,Nacos 通過一種叫健康檢查的機(jī)制去判斷服務(wù)實(shí)例是否活著
健康檢查跟心跳機(jī)制剛好相反,心跳機(jī)制是服務(wù)實(shí)例向服務(wù)端發(fā)送請(qǐng)求
而所謂的健康檢查就是服務(wù)端主動(dòng)向服務(wù)實(shí)例發(fā)送請(qǐng)求,去探測(cè)服務(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 請(qǐng)求,請(qǐng)求路徑是需要設(shè)置的,如果能正常請(qǐng)求,說明實(shí)例健康,反之就不健康
MySQL
- 一種特殊的檢查方式,他可以執(zhí)行下面這條 Sql 來判斷數(shù)據(jù)庫是不是主庫
默認(rèn)情況下,都是通過 TCP 的方式來探測(cè)服務(wù)實(shí)例是否還活著
服務(wù)發(fā)現(xiàn)
所謂的服務(wù)發(fā)現(xiàn)就是指當(dāng)有服務(wù)實(shí)例注冊(cè)成功之后,其它服務(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ù)的請(qǐng)求,當(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)就是使用訂閱的方式
對(duì)于這兩種服務(wù)發(fā)現(xiàn)方式,1.x 和 2.x 版本實(shí)現(xiàn)也是不一樣
服務(wù)(主動(dòng))查詢
1.x 整體就是發(fā)送 Http 請(qǐng)求去查詢服務(wù)實(shí)例,2.x 只不過是將 Http 請(qǐng)求換成了 gRPC 的請(qǐng)求
服務(wù)端對(duì)于查詢的處理過程都是一樣的,從服務(wù)注冊(cè)表中查出符合查詢條件的服務(wù)實(shí)例進(jìn)行返回
服務(wù)訂閱
不過對(duì)于服務(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í)對(duì)比機(jī)制也保留了,只不過這個(gè)定時(shí)對(duì)比的機(jī)制默認(rèn)是關(guān)閉狀態(tài)
之所以默認(rèn)關(guān)閉,主要還是因?yàn)?/strong>長連接還是比較穩(wěn)定的原因
當(dāng)客戶端出現(xiàn)異常,接收不到請(qǐng)求,那么服務(wù)端會(huì)直接跟客戶端斷開連接
當(dāng)恢復(fù)正常,由于有 Redo 操作,所以還是能拿到最新的實(shí)例信息的
細(xì)節(jié)
在 1.x 版本的時(shí)候,任何服務(wù)都是可以被訂閱的
但是在 2.x 版本中,只支持訂閱臨時(shí)服務(wù),對(duì)于永久服務(wù),已經(jīng)不支持訂閱了
數(shù)據(jù)一致性
由于 Nacos 是支持集群模式的,所以一定會(huì)涉及到分布式系統(tǒng)中不可避免的數(shù)據(jù)一致性問題
服務(wù)實(shí)例的責(zé)任機(jī)制
什么是服務(wù)實(shí)例的責(zé)任機(jī)制?
比如上面提到的服務(wù)注冊(cè)、心跳管理、監(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ù)的注冊(cè)表還是全部的服務(wù)實(shí)例數(shù)據(jù)
這個(gè)管理機(jī)制我給他起了一個(gè)名字,就叫做責(zé)任機(jī)制,因?yàn)槲以?1.x 和 2.x 都提到了responsible這個(gè)單詞
本質(zhì)就是 Nacos 服務(wù)對(duì)哪些服務(wù)實(shí)例負(fù)有心跳監(jiān)測(cè),健康檢查的責(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ù)注冊(cè)舉例來說,對(duì)于臨時(shí)實(shí)例來說,Nacos 會(huì)優(yōu)先保證可用性,也就是 AP
對(duì)于永久實(shí)例,Nacos 會(huì)優(yōu)先保證數(shù)據(jù)的一致性,也就是 CP
Nacos 的 AP 實(shí)現(xiàn)
對(duì)于 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)都可以接收來自客戶端的讀寫請(qǐng)求
當(dāng)某個(gè)節(jié)點(diǎn)剛啟動(dòng)時(shí),他會(huì)向集群中的某個(gè)節(jié)點(diǎn)發(fā)送請(qǐng)求,拉取所有的服務(wù)實(shí)例數(shù)據(jù)到自己的服務(wù)注冊(cè)表中
這樣其它客戶端就可以從這個(gè)服務(wù)節(jié)點(diǎn)中獲取到服務(wù)實(shí)例數(shù)據(jù)了
當(dāng)某個(gè)服務(wù)端節(jié)點(diǎn)接收到注冊(cè)臨時(shí)服務(wù)實(shí)例的請(qǐng)求,不僅僅會(huì)將這個(gè)服務(wù)實(shí)例存到自身的服務(wù)注冊(cè)表,同時(shí)也會(huì)向其它所有服務(wù)節(jié)點(diǎn)發(fā)送請(qǐng)求,將這個(gè)服務(wù)數(shù)據(jù)同步到其它所有節(jié)點(diǎn)
所以此時(shí)從任意一個(gè)節(jié)點(diǎn)都是可以獲取到所有的服務(wù)實(shí)例數(shù)據(jù)的。
即使數(shù)據(jù)同步的過程發(fā)生異常,服務(wù)實(shí)例也成功注冊(cè)到一個(gè) Nacos 服務(wù)中,對(duì)外部而言,整個(gè) Nacos 集群是可用的,也就達(dá)到了 AP 的效果
同時(shí)為了滿足 BASE 理論,Nacos 也有下面兩種機(jī)制保證最終節(jié)點(diǎn)間數(shù)據(jù)最終是一致的:
失敗重試機(jī)制
- 數(shù)據(jù)同步給其它節(jié)點(diǎn)失敗時(shí),會(huì)每隔 3s 重試一次,直到成功
定時(shí)對(duì)比機(jī)制
- 每個(gè) Nacos 服務(wù)節(jié)點(diǎn)會(huì)定時(shí)向所有的其它服務(wù)節(jié)點(diǎn)發(fā)送一些認(rèn)證的請(qǐng)求
- 這個(gè)請(qǐng)求會(huì)告訴每個(gè)服務(wù)節(jié)點(diǎn)自己負(fù)責(zé)的服務(wù)實(shí)例的對(duì)應(yīng)的版本號(hào),這個(gè)版本號(hào)隨著服務(wù)實(shí)例的變動(dòng)就會(huì)變動(dòng)
- 如果其它服務(wù)節(jié)點(diǎn)的數(shù)據(jù)的版本號(hào)跟自己的對(duì)不上,那就說明其它服務(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é)所有的讀寫請(qǐng)求,一個(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)有寫請(qǐng)求時(shí),如果請(qǐng)求的節(jié)點(diǎn)不是 Leader 節(jié)點(diǎn)時(shí),會(huì)將請(qǐng)求轉(zhuǎn)給 Leader 節(jié)點(diǎn),由 Leader 節(jié)點(diǎn)處理寫請(qǐng)求
比如,有個(gè)客戶端連到的上圖中的Nacos服務(wù)2節(jié)點(diǎn),之后向Nacos服務(wù)2注冊(cè)服務(wù)
Nacos服務(wù)2接收到請(qǐng)求之后,會(huì)判斷自己是不是 Leader 節(jié)點(diǎn),發(fā)現(xiàn)自己不是
此時(shí)Nacos服務(wù)2就會(huì)向 Leader 節(jié)點(diǎn)發(fā)送請(qǐng)求,Leader 節(jié)點(diǎn)接收到請(qǐng)求之后,會(huì)處理服務(wù)注冊(cè)的過程
為什么說 Raft 是保證 CP 的呢?
主要是因?yàn)?Raft 在處理寫的時(shí)候有一個(gè)判斷過程
- 首先,Leader 在處理寫請(qǐng)求時(shí),不會(huì)直接數(shù)據(jù)應(yīng)用到自己的系統(tǒng),而是先向所有的 Follower 發(fā)送請(qǐng)求,讓他們先處理這個(gè)請(qǐng)求
- 當(dāng)超過半數(shù)的 Follower 成功處理了這個(gè)寫請(qǐng)求之后,Leader 才會(huì)寫數(shù)據(jù),并返回給客戶端請(qǐng)求處理成功
- 如果超過一定時(shí)間未收到超過半數(shù)處理成功 Follower 的信號(hào),此時(shí) Leader 認(rèn)為這次寫數(shù)據(jù)是失敗的,就不會(huì)處理寫請(qǐng)求,直接返回給客戶端請(qǐng)求失敗
小細(xì)節(jié)需要注意
Nacos 在處理查詢服務(wù)實(shí)例的請(qǐng)求直接時(shí),并不會(huì)將請(qǐng)求轉(zhuǎn)發(fā)給 Leader 節(jié)點(diǎn)處理,而是直接查當(dāng)前 Nacos 服務(wù)實(shí)例的注冊(cè)表
這其實(shí)就會(huì)引發(fā)一個(gè)問題
如果客戶端查詢的 Follower 節(jié)點(diǎn)沒有及時(shí)處理 Leader 同步過來的寫請(qǐng)求(過半響應(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)然對(duì)于其它的一些數(shù)據(jù)的讀寫請(qǐng)求有的還是遵守了這個(gè)協(xié)議。
數(shù)據(jù)模型
在 Nacos 中,一個(gè)服務(wù)的確定是由三部分信息確定
- 命名空間(Namespace):多租戶隔離用的,默認(rèn)是
public
- 分組(Group):這個(gè)其實(shí)可以用來做環(huán)境隔離,服務(wù)注冊(cè)時(shí)可以指定服務(wù)的分組,比如是測(cè)試環(huán)境或者是開發(fā)環(huán)境,默認(rèn)是
DEFAULT_GROUP
- 服務(wù)名(ServiceName):這個(gè)就不用多說了
在服務(wù)注冊(cè)和訂閱的時(shí)候,必須要指定上述三部分信息,如果不指定,Nacos 就會(huì)提供默認(rèn)的信息
不過,在 Nacos 中,在服務(wù)里面其實(shí)還是有一個(gè)集群的概念
在服務(wù)注冊(cè)的時(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 請(qǐng)求 /nacos/v1/cs/configs/listener ↓ Nacos Server 檢測(cè)配置變化 ↓ 返回新配置 → 客戶端觸發(fā) Listener → 事件驅(qū)動(dòng)更新 Bean
Nacos 配置中心是支持配置項(xiàng)自動(dòng)刷新的,而其實(shí)現(xiàn)的原理是通過長輪詢+事件驅(qū)動(dòng)+本地回調(diào)機(jī)制的方式來實(shí)現(xiàn)的,具體來說:
- 客戶端向 Nacos 服務(wù)器發(fā)送一個(gè)帶有監(jiān)聽器(Listener)的請(qǐng)求,以獲取某個(gè)特定配置的值。
- Nacos 服務(wù)器接收到請(qǐng)求后,會(huì)檢查該配置是否發(fā)生了變化。如果沒有變化,則該請(qǐng)求將被阻塞,直到超時(shí)或配置發(fā)生變化。
- 當(dāng)配置發(fā)生變化時(shí),Nacos 服務(wù)器會(huì)立即響應(yīng),并將新的配置值返回給客戶端。
- 客戶端接收到新的配置值后,可以根據(jù)需要更新自身的配置。
長輪詢:服務(wù)器端接收到客戶端的請(qǐng)求之后,如果沒有數(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ò)、降級(jí)能力 |
Bean 自動(dòng)刷新 | 與 Spring 深度集成,支持注解級(jí)別的動(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
對(duì)應(yīng)的配置(如 my-config.yaml
),并注入到環(huán)境中。
如果設(shè)置了 refresh: true
,則在配置變更時(shí),Nacos 會(huì)通過監(jiān)聽機(jī)制自動(dòng)刷新對(duì)應(yīng)的 Bean。
- 如果你用的是
@RefreshScope
或@NacosValue(autoRefreshed = true)
,對(duì)應(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ì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02java中PreparedStatement和Statement詳細(xì)講解
這篇文章主要介紹了java中PreparedStatement和Statement詳細(xì)講解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(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ì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04Spring MVC的項(xiàng)目準(zhǔn)備和連接建立方法
SpringWebMVC是基于Servlet API的Web框架,屬于Spring框架的一部分,主要用于簡化Web應(yīng)用程序的開發(fā),SpringMVC通過控制器接收請(qǐng)求,使用模型處理數(shù)據(jù),并通過視圖展示結(jié)果,感興趣的朋友跟隨小編一起看看吧2024-10-10