萬字+20張圖探秘Nacos注冊中心核心實現(xiàn)原理
大家好,我是三友~~
今天就應(yīng)某位小伙伴的要求,來講一講Nacos作為服務(wù)注冊中心底層的實現(xiàn)原理
不知你是否跟我一樣,在使用Nacos時有以下幾點疑問:
- 臨時實例和永久實例是什么?有什么區(qū)別?
- 服務(wù)實例是如何注冊到服務(wù)端的?
- 服務(wù)實例和服務(wù)端之間是如何?;畹??
- 服務(wù)訂閱是如何實現(xiàn)的?
- 集群間數(shù)據(jù)是如何同步的?CP還是AP?
- Nacos的數(shù)據(jù)模型是什么樣的?
- ...
本文就通過探討上述問題來探秘Nacos服務(wù)注冊中心核心的底層實現(xiàn)原理。
雖然Nacos最新版本已經(jīng)到了2.x版本,但是為了照顧那些還在用1.x版本的同學(xué),所以本文我會同時去講1.x版本和2.x版本的實現(xiàn)
觀前提醒,本文又又又是一篇超長的干貨,非常適合一鍵三連~~
臨時實例和永久實例
臨時實例和永久實例在Nacos中是一個非常非常重要的概念
之所以說它重要,主要是因為我在讀源碼的時候發(fā)現(xiàn),臨時實例和永久實例在底層的許多實現(xiàn)機制是完全不同的
臨時實例
臨時實例在注冊到注冊中心之后僅僅只保存在服務(wù)端內(nèi)部一個緩存中,不會持久化到磁盤
這個服務(wù)端內(nèi)部的緩存在注冊中心屆一般被稱為服務(wù)注冊表
當(dāng)服務(wù)實例出現(xiàn)異?;蛘呦戮€之后,就會把這個服務(wù)實例從服務(wù)注冊表中剔除
永久實例
永久服務(wù)實例不僅僅會存在服務(wù)注冊表中,同時也會被持久化到磁盤文件中
當(dāng)服務(wù)實例出現(xiàn)異常或者下線,Nacos只會將服務(wù)實例的健康狀態(tài)設(shè)置為不健康,并不會對將其從服務(wù)注冊表中剔除
所以這個服務(wù)實例的信息你還是可以從注冊中心看到,只不過處于不健康狀態(tài)
這是就是兩者最最最基本的區(qū)別
當(dāng)然除了上述最基本的區(qū)別之外,兩者還有很多其它的區(qū)別,接下來本文還會提到
這里你可能會有一個疑問
為什么Nacos要將服務(wù)實例分為臨時實例和永久實例?
主要還是因為應(yīng)用場景不同
臨時實例就比較適合于業(yè)務(wù)服務(wù),服務(wù)下線之后可以不需要在注冊中心中查看到
永久實例就比較適合需要運維的服務(wù),這種服務(wù)幾乎是永久存在的,比如說MySQL、Redis等等
MySQL、Redis等服務(wù)實例可以通過SDK手動注冊
對于這些服務(wù),我們需要一直看到服務(wù)實例的狀態(tài),即使出現(xiàn)異常,也需要能夠查看時實的狀態(tài)
所以從這可以看出Nacos跟你印象中的注冊中心不太一樣,他不僅僅可以注冊平時業(yè)務(wù)中的實例,還可以注冊像MySQL、Redis這個服務(wù)實例的信息到注冊中心
在SpringCloud環(huán)境底下,一般其實都是業(yè)務(wù)服務(wù),所以默認(rèn)注冊服務(wù)實例都是臨時實例
當(dāng)然如果你想改成永久實例,可以通過下面這個配置項來完成
spring
cloud:
nacos:
discovery:
#ephemeral單詞是臨時的意思,設(shè)置成false,就是永久實例了
ephemeral: false
這里還有一個小細(xì)節(jié)
在1.x版本中,一個服務(wù)中可以既有臨時實例也有永久實例,服務(wù)實例是永久還是臨時是由服務(wù)實例本身決定的
但是2.x版本中,一個服務(wù)中的所有實例要么都是臨時的要么都是永久的,是由服務(wù)決定的,而不是具體的服務(wù)實例
所以在2.x可以說是臨時服務(wù)和永久服務(wù)
圖片
為什么2.x把臨時還是永久的屬性由實例本身決定改成了由服務(wù)決定?
其實很簡單,你想想,假設(shè)對一個MySQL服務(wù)來說,它的每個服務(wù)實例肯定都是永久的,不會出現(xiàn)一些是永久的,一些是臨時的情況吧
所以臨時還是永久的屬性由服務(wù)本身決定其實就更加合理了
服務(wù)注冊
作為一個服務(wù)注冊中心,服務(wù)注冊肯定是一個非常重要的功能
所謂的服務(wù)注冊,就是通過注冊中心提供的客戶端SDK(或者是控制臺)將服務(wù)本身的一些元信息,比如ip、端口等信息發(fā)送到注冊中心服務(wù)端
服務(wù)端在接收到服務(wù)之后,會將服務(wù)的信息保存到前面提到的服務(wù)注冊表中
1、1.x版本的實現(xiàn)
在Nacos在1.x版本的時候,服務(wù)注冊是通過Http接口實現(xiàn)的
圖片
代碼如下
圖片
整個邏輯比較簡單,因為Nacos服務(wù)端本身就是用SpringBoot寫的
但是在2.x版本的實現(xiàn)就比較復(fù)雜了
2、2.x版本的實現(xiàn)
2.1、通信協(xié)議的改變
2.x版本相比于1.x版本最主要的升級就是客戶端和服務(wù)端通信協(xié)議的改變,由1.x版本的Http改成了2.x版本gRPC
gRPC是谷歌公司開發(fā)的一個高性能、開源和通用的RPC框架,Java版本的實現(xiàn)底層也是基于Netty來的
之所以改成了gRPC,主要是因為Http請求會頻繁創(chuàng)建和銷毀連接,白白浪費資源
所以在2.x版本之后,為了提升性能,就將通信協(xié)議改成了gRPC
根據(jù)官網(wǎng)顯示,整體的效果還是很明顯,相比于1.x版本,注冊性能總體提升至少2倍
雖然通信方式改成了gRPC,但是2.x版本服務(wù)端依然保留了Http注冊的接口,所以用1.x的Nacos SDK依然可以注冊到2.x版本的服務(wù)端
2.2、具體的實現(xiàn)
Nacos客戶端在啟動的時候,會通過gRPC跟服務(wù)端建立長連接
圖片
這個連接會一直存在,之后客戶端與服務(wù)端所有的通信都是基于這個長連接來的
當(dāng)客戶端發(fā)起注冊的時候,就會通過這個長連接,將服務(wù)實例的信息發(fā)送給服務(wù)端
服務(wù)端拿到服務(wù)實例,跟1.x一樣,也會存到服務(wù)注冊表
除了注冊之外,當(dāng)注冊的是臨時實例時,2.x還會將服務(wù)實例信息存儲到客戶端中的一個緩存中,供Redo操作
所謂的Redo操作,其實就是一個補償機制,本質(zhì)是個定時任務(wù),默認(rèn)每3s執(zhí)行一次
這個定時任務(wù)作用是,當(dāng)客戶端與服務(wù)端重新建立連接時(因為一些異常原因?qū)е逻B接斷開)
那么之前注冊的服務(wù)實例肯定還要繼續(xù)注冊服務(wù)端(斷開連接服務(wù)實例就會被剔除服務(wù)注冊表)
所以這個Redo操作一個很重要的作用就是重連之后的重新注冊的作用
除了注冊之外,比如服務(wù)訂閱之類的操作也需要Redo操作,當(dāng)連接重新建立,之前客戶端的操作都需要Redo一下
小總結(jié)
1.x版本是通過Http協(xié)議來進(jìn)行服務(wù)注冊的
2.x由于客戶端與服務(wù)端的通信改成了gRPC長連接,所以改成通過gRPC長連接來注冊
2.x比1.x多個Redo操作,當(dāng)注冊的服務(wù)實例是臨時實例是,出現(xiàn)網(wǎng)絡(luò)異常,連接重新建立之后,客戶端需要將服務(wù)注冊、服務(wù)訂閱之類的操作進(jìn)行重做
這里你可能會有個疑問
既然2.x有Redo機制保證客戶端與服務(wù)端通信正常之后重新注冊,那么1.x有類似的這種Redo機制么?
當(dāng)然也會有,接下往下看。
心跳機制
心跳機制,也可以被稱為?;顧C制,它的作用就是服務(wù)實例告訴注冊中心我這個服務(wù)實例還活著
圖片
在正常情況下,服務(wù)關(guān)閉了,那么服務(wù)會主動向Nacos服務(wù)端發(fā)送一個服務(wù)下線的請求
Nacos服務(wù)端在接收到請求之后,會將這個服務(wù)實例從服務(wù)注冊表中剔除
但是對于異常情況下,比如出現(xiàn)網(wǎng)絡(luò)問題,可能導(dǎo)致這個注冊的服務(wù)實例無法提供服務(wù),處于不可用狀態(tài),也就是不健康
而此時在沒有任何機制的情況下,服務(wù)端是無法知道這個服務(wù)處于不可用狀態(tài)
所以為了避免這種情況,一些注冊中心,就比如Nacos、Eureka,就會用心跳機制來判斷這個服務(wù)實例是否能正常
在Nacos中,心跳機制僅僅是針對臨時實例來說的,臨時實例需要靠心跳機制來?;?/p>
心跳機制在1.x和2.x版本的實現(xiàn)也是不一樣的
1.x心跳實現(xiàn)
在1.x中,心跳機制實現(xiàn)是通過客戶端和服務(wù)端各存在的一個定時任務(wù)來完成的
在服務(wù)注冊時,發(fā)現(xiàn)是臨時實例,客戶端會開啟一個5s執(zhí)行一次的定時任務(wù)
圖片
這個定時任務(wù)會構(gòu)建一個Http請求,攜帶這個服務(wù)實例的信息,然后發(fā)送到服務(wù)端
圖片
在Nacos服務(wù)端也會開啟一個定時任務(wù),默認(rèn)也是5s執(zhí)行一次,去檢查這些服務(wù)實例最后一次心跳的時間,也就是客戶端最后一次發(fā)送Http請求的時間
- 當(dāng)最后一次心跳時間超過15s,但沒有超過30s,會把這服務(wù)實例標(biāo)記成不健康
- 當(dāng)最后一次心跳超過30s,直接把服務(wù)從服務(wù)注冊表中剔除
圖片
這就是1.x版本的心跳機制,本質(zhì)就是兩個定時任務(wù)
其實1.x的這個心跳還有一個作用,就是跟上一節(jié)說的gRPC時Redo操作的作用是一樣的
服務(wù)在處理心跳的時候,發(fā)現(xiàn)心跳攜帶這個服務(wù)實例的信息在注冊表中沒有,此時就會添加到服務(wù)注冊表
所以心跳也有Redo的類似效果
2.x心跳實現(xiàn)
在2.x版本之后,由于通信協(xié)議改成了gRPC,客戶端與服務(wù)端保持長連接,所以2.x版本之后它是利用這個gRPC長連接本身的心跳來?;?/p>
一旦這個連接斷開,服務(wù)端就會認(rèn)為這個連接注冊的服務(wù)實例不可用,之后就會將這個服務(wù)實例從服務(wù)注冊表中提出剔除
除了連接本身的心跳之外,Nacos還有服務(wù)端的一個主動檢測機制
Nacos服務(wù)端也會啟動一個定時任務(wù),默認(rèn)每隔3s執(zhí)行一次
這個任務(wù)會去檢查超過20s沒有發(fā)送請求數(shù)據(jù)的連接
一旦發(fā)現(xiàn)有連接已經(jīng)超過20s沒發(fā)送請求,那么就會向這個連接對應(yīng)的客戶端發(fā)送一個請求
如果請求不通或者響應(yīng)失敗,此時服務(wù)端也會認(rèn)為與客戶端的這個連接異常,從而將這個客戶端注冊的服務(wù)實例從服務(wù)注冊表中剔除
所以對于2.x版本,主要是兩種機制來進(jìn)行?;睿?/p>
- 連接本身的心跳機制,斷開就直接剔除服務(wù)實例
- Nacos主動檢查機制,服務(wù)端會對20s沒有發(fā)送數(shù)據(jù)的連接進(jìn)行檢查,出現(xiàn)異常時也會主動斷開連接,剔除服務(wù)實例
小總結(jié)
心跳機制僅僅針對臨時實例而言
1.x心跳機制是通過客戶端和服務(wù)端兩個定時任務(wù)來完成的,客戶端定時上報心跳信息,服務(wù)端定時檢查心跳時間,超過15s標(biāo)記不健康,超過30s直接剔除
1.x心跳機制還有類似2.x的Redo作用,服務(wù)端發(fā)現(xiàn)心跳的服務(wù)信息不存在會,會將服務(wù)信息添加到注冊表,相當(dāng)于重新注冊了
2.x是基于gRPC長連接本身的心跳機制和服務(wù)端的定時檢查機制來的,出現(xiàn)異常直接剔除
健康檢查
前面說了,心跳機制僅僅是臨時實例用來保護(hù)的機制
而對于永久實例來說,一般來說無法主動上報心跳
就比如說MySQL實例,肯定是不會主動上報心跳到Nacos的,所以這就導(dǎo)致無法通過心跳機制來?;?/p>
所以針對永久實例的情況,Nacos通過一種叫健康檢查的機制去判斷服務(wù)實例是否活著
健康檢查跟心跳機制剛好相反,心跳機制是服務(wù)實例向服務(wù)端發(fā)送請求
而所謂的健康檢查就是服務(wù)端主動向服務(wù)實例發(fā)送請求,去探測服務(wù)實例是否活著
圖片
健康檢查機制在1.x和2.x的實現(xiàn)機制是一樣的
Nacos服務(wù)端在會去創(chuàng)建一個健康檢查任務(wù),這個任務(wù)每次執(zhí)行時間間隔會在2000~7000毫秒之間
當(dāng)任務(wù)觸發(fā)的時候,會根據(jù)設(shè)置的健康檢查的方式執(zhí)行不同的邏輯,目前主要有以下三種方式:
- TCP
- HTTP
- MySQL
TCP的方式就是根據(jù)服務(wù)實例的ip和端口去判斷是否能連接成功,如果連接成功,就認(rèn)為健康,反之就任務(wù)不健康
HTTP的方式就是向服務(wù)實例的ip和端口發(fā)送一個Http請求,請求路徑是需要設(shè)置的,如果能正常請求,說明實例健康,反之就不健康
MySQL的方式是一種特殊的檢查方式,他可以執(zhí)行下面這條Sql來判斷數(shù)據(jù)庫是不是主庫
圖片
默認(rèn)情況下,都是通過TCP的方式來探測服務(wù)實例是否還活著
服務(wù)發(fā)現(xiàn)
所謂的服務(wù)發(fā)現(xiàn)就是指當(dāng)有服務(wù)實例注冊成功之后,其它服務(wù)可以發(fā)現(xiàn)這些服務(wù)實例
Nacos提供了兩種發(fā)現(xiàn)方式:
- 主動查詢
- 服務(wù)訂閱
主動查詢就是指客戶端主動向服務(wù)端查詢需要關(guān)注的服務(wù)實例,也就是拉(pull)的模式
服務(wù)訂閱就是指客戶端向服務(wù)端發(fā)送一個訂閱服務(wù)的請求,當(dāng)被訂閱的服務(wù)有信息變動就會主動將服務(wù)實例的信息推送給訂閱的客戶端,本質(zhì)就是推(push)模式
圖片
在我們平時使用時,一般來說都是選擇使用訂閱的方式,這樣一旦有服務(wù)實例數(shù)據(jù)的變動,客戶端能夠第一時間感知
并且Nacos在整合SpringCloud的時候,默認(rèn)就是使用訂閱的方式
對于這兩種服務(wù)發(fā)現(xiàn)方式,1.x和2.x版本實現(xiàn)也是不一樣
服務(wù)查詢其實兩者實現(xiàn)都很簡單
1.x整體就是發(fā)送Http請求去查詢服務(wù)實例,2.x只不過是將Http請求換成了gRPC的請求
服務(wù)端對于查詢的處理過程都是一樣的,從服務(wù)注冊表中查出符合查詢條件的服務(wù)實例進(jìn)行返回
不過對于服務(wù)訂閱,兩者的機制就稍微復(fù)雜一點
在Nacos客戶端,不論是1.x還是2.x都是通過SDK中的NamingService#subscribe方法來發(fā)起訂閱的
圖片
當(dāng)有服務(wù)實例數(shù)據(jù)變動的時,客戶端就會回調(diào)EventListener,就可以拿到最新的服務(wù)實例數(shù)據(jù)了
雖然1.x還是2.x都是同樣的方法,但是具體的實現(xiàn)邏輯是不一樣的
1.x服務(wù)訂閱實現(xiàn)
在1.x版本的時候,服務(wù)訂閱的處理邏輯大致會有以下三步:
第一步,客戶端在啟動的時候,會去構(gòu)建一個叫PushReceiver的類
這個類會去創(chuàng)建一個UDP Socket,端口是隨機的
圖片
其實通過名字就可以知道這個類的作用,就是通過UDP的方式接收服務(wù)端推送的數(shù)據(jù)的
第二步,調(diào)用NamingService#subscribe來發(fā)起訂閱時,會先去服務(wù)端查詢需要訂閱服務(wù)的所有實例信息
之后會將所有服務(wù)實例數(shù)據(jù)存到客戶端的一個內(nèi)部緩存中
圖片
并且在查詢的時候,會將這個UDP Socket的端口作為一個參數(shù)傳到服務(wù)端
服務(wù)端接收到這個UDP端口后,后續(xù)就通過這個端口給客戶端推送服務(wù)實例數(shù)據(jù)
第三步,會為這次訂閱開啟一個不定時執(zhí)行的任務(wù)
之所以不定時,是因為這個當(dāng)執(zhí)行異常的時候,下次執(zhí)行的時間間隔就會變長,但是最多不超過60s,正常是10s,這個10s是查詢服務(wù)實例是服務(wù)端返回的
這個任務(wù)會去從服務(wù)端查詢訂閱的服務(wù)實例信息,然后更新內(nèi)部緩存
這里你可能會有個疑問
既然有了服務(wù)變動推送的功能,為什么還要定時去查詢更新服務(wù)實例信息呢?
其實很簡單,那就是因為UDP通信不穩(wěn)定導(dǎo)致的
雖然有Push,但是由于UDP通信自身的不確定性,有可能會導(dǎo)致客戶端接收變動信息失敗
所以這里就加了一個定時任務(wù),彌補這種可能性,屬于一個兜底的方案。
這就是1.x版本的服務(wù)訂閱的實現(xiàn)
圖片
2.x服務(wù)訂閱的實現(xiàn)
講完1.x的版本實現(xiàn),接下來就講一講2.x版本的實現(xiàn)
由于2.x版本換成了gRPC長連接的方式,所以2.x版本服務(wù)數(shù)據(jù)變更推送已經(jīng)完全拋棄了1.x的UDP做法
當(dāng)有服務(wù)實例變動的時候,服務(wù)端直接通過這個長連接將服務(wù)信息發(fā)送給客戶端
客戶端拿到最新服務(wù)實例數(shù)據(jù)之后的處理方式就跟1.x是一樣了
除了處理方式一樣,2.x也繼承了1.x的其他的東西
比如客戶端依然會有服務(wù)實例的緩存
定時對比機制也保留了,只不過這個定時對比的機制默認(rèn)是關(guān)閉狀態(tài)
之所以默認(rèn)關(guān)閉,主要還是因為長連接還是比較穩(wěn)定的原因
當(dāng)客戶端出現(xiàn)異常,接收不到請求,那么服務(wù)端會直接跟客戶端斷開連接
當(dāng)恢復(fù)正常,由于有Redo操作,所以還是能拿到最新的實例信息的
所以2.x版本的服務(wù)訂閱功能的實現(xiàn)大致如下圖所示
圖片
這里還有個細(xì)節(jié)需要注意
在1.x版本的時候,任何服務(wù)都是可以被訂閱的
但是在2.x版本中,只支持訂閱臨時服務(wù),對于永久服務(wù),已經(jīng)不支持訂閱了
小總結(jié)
服務(wù)查詢1.x是通過Http請求;2.x通過gRPC請求
服務(wù)訂閱1.x是通過UDP來推送的;2.x就基于gRPC長連接來實現(xiàn)的
1.x和2.x客戶端都有服務(wù)實例的緩存,也有定時對比機制,只不過1.x會自動開啟;2.x提供了一個開個,可以手動選擇是否開啟,默認(rèn)不開啟
數(shù)據(jù)一致性
由于Nacos是支持集群模式的,所以一定會涉及到分布式系統(tǒng)中不可避免的數(shù)據(jù)一致性問題
1、服務(wù)實例的責(zé)任機制
再說數(shù)據(jù)一致性問題之前,先來討論一下服務(wù)實例的責(zé)任機制
什么是服務(wù)實例的責(zé)任機制?
比如上面提到的服務(wù)注冊、心跳管理、監(jiān)控檢查機制,當(dāng)只有一個Nacos服務(wù)時,那么自然而言這個服務(wù)會去檢查所有的服務(wù)實例的心跳時間,執(zhí)行所有服務(wù)實例的健康檢查任務(wù)
圖片
但是當(dāng)出現(xiàn)Nacos服務(wù)出現(xiàn)集群時,為了平衡各Nacos服務(wù)的壓力,Nacos會根據(jù)一定的規(guī)則讓每個Nacos服務(wù)只管理一部分服務(wù)實例的
當(dāng)然每個Nacos服務(wù)的注冊表還是全部的服務(wù)實例數(shù)據(jù)
圖片
這個管理機制我給他起了一個名字,就叫做責(zé)任機制,因為我在1.x和2.x都提到了responsible這個單詞
本質(zhì)就是Nacos服務(wù)對哪些服務(wù)實例負(fù)有心跳監(jiān)測,健康檢查的責(zé)任。
2、CAP定理和BASE理論
談到數(shù)據(jù)一致性問題,一定離不開兩個著名分布式理論
- CAP定理
- BASE理論
CAP定理中,三個字母分別代表這些含義:
- C,Consistency單詞的縮寫,代表一致性,指分布式系統(tǒng)中各個節(jié)點的數(shù)據(jù)保持強一致,也就是每個時刻都必須一樣,不一樣整個系統(tǒng)就不能對外提供服務(wù)
- A,Availability單詞的縮寫,代表可用性,指整個分布式系統(tǒng)保持對外可用,即使從每個節(jié)點獲取的數(shù)據(jù)可能都不一樣,只要能獲取到就行
- P,Partition tolerance單詞的縮寫,代表分區(qū)容錯性。
所謂的CAP定理,就是指在一個分布式系統(tǒng)中,CAP這三個指標(biāo),最多同時只能滿足其中的兩個,不可能三個都同時滿足
圖片
為什么三者不能同時滿足?
對于一個分布式系統(tǒng),網(wǎng)絡(luò)分區(qū)是一定需要滿足的
而所謂分區(qū)指的是系統(tǒng)中的服務(wù)部署在不同的網(wǎng)絡(luò)區(qū)域中
圖片
比如,同一套系統(tǒng)可能同時在北京和上海都有部署,那么他們就處于不同的網(wǎng)絡(luò)分區(qū),就可能出現(xiàn)無法互相訪問的情況
當(dāng)然,你也可以把所有的服務(wù)都放在一個網(wǎng)絡(luò)分區(qū),但是當(dāng)網(wǎng)絡(luò)出現(xiàn)故障時,整個系統(tǒng)都無法對外提供服務(wù),那這還有什么意義呢?
所以分布式系統(tǒng)一定需要滿足分區(qū)容錯性,把系統(tǒng)部署在不同的區(qū)域網(wǎng)絡(luò)中
此時只剩下了一致性和可用性,它們?yōu)槭裁床荒芡瑫r滿足?
其實答案很簡單,就因為可能出現(xiàn)網(wǎng)絡(luò)分區(qū)導(dǎo)致的通信失敗。
比如說,現(xiàn)在出現(xiàn)了網(wǎng)絡(luò)分區(qū)的問題,上圖中的A網(wǎng)絡(luò)區(qū)域和B網(wǎng)絡(luò)區(qū)域無法相互訪問
此時假設(shè)往上圖中的A網(wǎng)絡(luò)區(qū)域發(fā)送請求,將服務(wù)中的一個值 i 屬性設(shè)置成 1
圖片
如果保證可用性,此時由于A和B網(wǎng)絡(luò)不通,此時只有A中的服務(wù)修改成功,B無法修改成功,此時數(shù)據(jù)AB區(qū)域數(shù)據(jù)就不一致性,也就沒有保證數(shù)據(jù)一致性
如果保證一致性,此時由于A和B網(wǎng)絡(luò)不通,所以此時A也不能修改成功,必須修改失敗,否則就會導(dǎo)致AB數(shù)據(jù)不一致
雖然A沒修改成功,保證了數(shù)據(jù)一致性,AB還是之前相同的數(shù)據(jù),但是此時整個系統(tǒng)已經(jīng)沒有寫可用性了,無法成功寫數(shù)據(jù)了。
所以從上面分析可以看出,在有分區(qū)容錯性的前提下,可用性和一致性是無法同時保證的。
雖然無法同時一致性和可用性,但是能不能換種思路來思考一下這個問題
首先我們可以先保證系統(tǒng)的可用性,也就是先讓系統(tǒng)能夠?qū)憯?shù)據(jù),將A區(qū)域服務(wù)中的i修改成1
之后當(dāng)AB區(qū)域之間網(wǎng)絡(luò)恢復(fù)之后,將A區(qū)域的i值復(fù)制給B區(qū)域,這樣就能夠保證AB區(qū)域間的數(shù)據(jù)最終是一致的了
這不就皆大歡喜了么
這種思路其實就是BASE理論的核心要點,優(yōu)先保證可用性,數(shù)據(jù)最終達(dá)成一致性。
BASE理論主要是包括以下三點:
- 基本可用(Basically Available):系統(tǒng)出現(xiàn)故障還是能夠?qū)ν馓峁┓?wù),不至于直接無法用了
- 軟狀態(tài)(Soft State):允許各個節(jié)點的數(shù)據(jù)不一致
- 最終一致性,(Eventually Consistent):雖然允許各個節(jié)點的數(shù)據(jù)不一致,但是在一定時間之后,各個節(jié)點的數(shù)據(jù)最終需要一致的
BASE理論其實就是妥協(xié)之后的產(chǎn)物。
3、Nacos的AP和CP
Nacos其實目前是同時支持AP和CP的
具體使用AP還是CP得取決于Nacos內(nèi)部的具體功能,并不是有的文章說的可以通過一個配置自由切換。
就以服務(wù)注冊舉例來說,對于臨時實例來說,Nacos會優(yōu)先保證可用性,也就是AP
對于永久實例,Nacos會優(yōu)先保證數(shù)據(jù)的一致性,也就是CP
接下來我們就來講一講Nacos的CP和AP的實現(xiàn)原理
3.1、Nacos的AP實現(xiàn)
對于AP來說,Nacos使用的是阿里自研的Distro協(xié)議
在這個協(xié)議中,每個服務(wù)端節(jié)點是一個平等的狀態(tài),每個服務(wù)端節(jié)點正常情況下數(shù)據(jù)是一樣的,每個服務(wù)端節(jié)點都可以接收來自客戶端的讀寫請求
圖片
當(dāng)某個節(jié)點剛啟動時,他會向集群中的某個節(jié)點發(fā)送請求,拉取所有的服務(wù)實例數(shù)據(jù)到自己的服務(wù)注冊表中
圖片
這樣其它客戶端就可以從這個服務(wù)節(jié)點中獲取到服務(wù)實例數(shù)據(jù)了
當(dāng)某個服務(wù)端節(jié)點接收到注冊臨時服務(wù)實例的請求,不僅僅會將這個服務(wù)實例存到自身的服務(wù)注冊表,同時也會向其它所有服務(wù)節(jié)點發(fā)送請求,將這個服務(wù)數(shù)據(jù)同步到其它所有節(jié)點
圖片
所以此時從任意一個節(jié)點都是可以獲取到所有的服務(wù)實例數(shù)據(jù)的。
即使數(shù)據(jù)同步的過程發(fā)生異常,服務(wù)實例也成功注冊到一個Nacos服務(wù)中,對外部而言,整個Nacos集群是可用的,也就達(dá)到了AP的效果
同時為了滿足BASE理論,Nacos也有下面兩種機制保證最終節(jié)點間數(shù)據(jù)最終是一致的:
- 失敗重試機制
- 定時對比機制
失敗重試機制是指當(dāng)數(shù)據(jù)同步給其它節(jié)點失敗時,會每隔3s重試一次,直到成功
定時對比機制就是指,每個Nacos服務(wù)節(jié)點會定時向所有的其它服務(wù)節(jié)點發(fā)送一些認(rèn)證的請求
這個請求會告訴每個服務(wù)節(jié)點自己負(fù)責(zé)的服務(wù)實例的對應(yīng)的版本號,這個版本號隨著服務(wù)實例的變動就會變動
如果其它服務(wù)節(jié)點的數(shù)據(jù)的版本號跟自己的對不上,那就說明其它服務(wù)節(jié)點的數(shù)據(jù)不是最新的
此時這個Nacos服務(wù)節(jié)點就會將自己負(fù)責(zé)的服務(wù)實例數(shù)據(jù)發(fā)給不是最新數(shù)據(jù)的節(jié)點,這樣就保證了每個節(jié)點的數(shù)據(jù)是一樣的了。
3.2、Nacos的CP實現(xiàn)
Nacos的CP實現(xiàn)是基于Raft算法來實現(xiàn)的
在1.x版本早期,Nacos是自己手動實現(xiàn)Raft算法
在2.x版本,Nacos移除了手動實現(xiàn)Raft算法,轉(zhuǎn)而擁抱基于螞蟻開源的JRaft框架
在Raft算法,每個節(jié)點主要有三個狀態(tài)
- Leader,負(fù)責(zé)所有的讀寫請求,一個集群只有一個
- Follower,從節(jié)點,主要是負(fù)責(zé)復(fù)制Leader的數(shù)據(jù),保證數(shù)據(jù)的一致性
- Candidate,候選節(jié)點,最終會變成Leader或者Follower
集群啟動時都是節(jié)點Follower,經(jīng)過一段時間會轉(zhuǎn)換成Candidate狀態(tài),再經(jīng)過一系列復(fù)雜的選擇算法,選出一個Leader
圖片
這個選舉算法比較復(fù)雜,完全值得另寫一篇文章,這里就不細(xì)說了。不過立個flag,如果本篇文章點贊量超過28個,我連夜爆肝,再來一篇。
當(dāng)有寫請求時,如果請求的節(jié)點不是Leader節(jié)點時,會將請求轉(zhuǎn)給Leader節(jié)點,由Leader節(jié)點處理寫請求
比如,有個客戶端連到的上圖中的Nacos服務(wù)2節(jié)點,之后向Nacos服務(wù)2注冊服務(wù)
Nacos服務(wù)2接收到請求之后,會判斷自己是不是Leader節(jié)點,發(fā)現(xiàn)自己不是
此時Nacos服務(wù)2就會向Leader節(jié)點發(fā)送請求,Leader節(jié)點接收到請求之后,會處理服務(wù)注冊的過程
為什么說Raft是保證CP的呢?
主要是因為Raft在處理寫的時候有一個判斷過程
- 首先,Leader在處理寫請求時,不會直接數(shù)據(jù)應(yīng)用到自己的系統(tǒng),而是先向所有的Follower發(fā)送請求,讓他們先處理這個請求
- 當(dāng)超過半數(shù)的Follower成功處理了這個寫請求之后,Leader才會寫數(shù)據(jù),并返回給客戶端請求處理成功
- 如果超過一定時間未收到超過半數(shù)處理成功Follower的信號,此時Leader認(rèn)為這次寫數(shù)據(jù)是失敗的,就不會處理寫請求,直接返回給客戶端請求失敗
所以,一旦發(fā)生故障,導(dǎo)致接收不到半數(shù)的Follower寫成功的響應(yīng),整個集群就直接寫失敗,這就很符合CP的概念了。
不過這里還有一個小細(xì)節(jié)需要注意
Nacos在處理查詢服務(wù)實例的請求直接時,并不會將請求轉(zhuǎn)發(fā)給Leader節(jié)點處理,而是直接查當(dāng)前Nacos服務(wù)實例的注冊表
這其實就會引發(fā)一個問題
如果客戶端查詢的Follower節(jié)點沒有及時處理Leader同步過來的寫請求(過半響應(yīng)的節(jié)點中不包括這個節(jié)點),此時在這個Follower其實是查不到最新的數(shù)據(jù)的,這就會導(dǎo)致數(shù)據(jù)的不一致
所以說,雖然Raft協(xié)議規(guī)定要求從Leader節(jié)點查最新的數(shù)據(jù),但是Nacos至少在讀服務(wù)實例數(shù)據(jù)時并沒有遵守這個協(xié)議
當(dāng)然對于其它的一些數(shù)據(jù)的讀寫請求有的還是遵守了這個協(xié)議。
JRaft對于讀請求其實是做了很多優(yōu)化的,其實從Follower節(jié)點通過一定的機制也是能夠保證讀到最新的數(shù)據(jù)
數(shù)據(jù)模型
在Nacos中,一個服務(wù)的確定是由三部分信息確定
- 命名空間(Namespace):多租戶隔離用的,默認(rèn)是public
- 分組(Group):這個其實可以用來做環(huán)境隔離,服務(wù)注冊時可以指定服務(wù)的分組,比如是測試環(huán)境或者是開發(fā)環(huán)境,默認(rèn)是DEFAULT_GROUP
- 服務(wù)名(ServiceName):這個就不用多說了
通過上面三者就可以確定同一個服務(wù)了
在服務(wù)注冊和訂閱的時候,必須要指定上述三部分信息,如果不指定,Nacos就會提供默認(rèn)的信息
不過,在Nacos中,在服務(wù)里面其實還是有一個集群的概念
圖片
在服務(wù)注冊的時候,可以指定這個服務(wù)實例在哪個集體的集群中,默認(rèn)是在DEFAULT集群下
在SpringCloud環(huán)境底下可以通過如下配置去設(shè)置
spring
cloud:
nacos:
discovery:
cluster-name: sanyoujavaCluster
在服務(wù)訂閱的時候,可以指定訂閱哪些集群下的服務(wù)實例
當(dāng)然,也可以不指定,如果不指定話,默認(rèn)就是訂閱這個服務(wù)下的所有集群的服務(wù)實例
我們?nèi)粘J褂弥锌梢詫⒉渴鹪谙嗤瑓^(qū)域的服務(wù)劃分為同一個集群,比如杭州屬于一個集群,上海屬于一個集群
這樣服務(wù)調(diào)用的時候,就可以優(yōu)先使用同一個地區(qū)的服務(wù)了,比跨區(qū)域調(diào)用速度更快。