Nacos 架構原理①:一條註冊請求會經歷什麼?
大家好,我是悟空呀。
前言
上篇我們講解了如何使用 Nacos 作為註冊中心和配置中心。
這次我們來聊下 Nacos 的註冊服務的底層原理。
Nacos 作為註冊中心,用來接收客戶端(服務例項)發起的註冊請求,並將註冊資訊存放到註冊中心進行管理。
那麼一條註冊請求到底會經歷哪些步驟呢?
知識點預告
先上一張整體的流程圖:
- 叢集環境:如果是 Nacos 叢集環境,那麼拓撲結構是什麼樣的。
- 組裝請求:客戶端組裝註冊請求,下一步對 Nacos 服務發起遠端呼叫。
- 隨機節點:客戶端隨機選擇叢集中的一個 Nacos 節點發起註冊,實現負載均衡。
-
路由轉發
- 處理請求:轉發給指定的節點後,該節點就會將註冊請求中的例項資訊解析出來,存到自定義的記憶體結構中。
-
最終一致性:通過 Nacos 自研的 Distro 協議執行
延遲非同步任務
,將註冊資訊同步給叢集中的其他節點,保證了資料的最終一致性。 - 非同步重試:如果註冊失敗,客戶端將會切換 Nacos 節點,再次發起註冊請求,保證高可用性。
這些知識點裡面還有很多細節,我會通過畫圖 + 原始碼剖析的方式給大家解答。如果遇到原始碼看不太懂的地方,可以多看下我畫的圖,然後翻下原始碼,對照著一起看。
小 Tip:本文使用的 Nacos 版本: 2.0.4。
一、源頭:發起註冊
1.1 閱讀原始碼的小技巧
上篇我們講到加上一個註解 @EnableDiscoveryClient
就可以使服務自動註冊到 Nacos。
那麼這個發起註冊的地方到底在哪呢?註冊資訊又是長什麼樣的呢?
告訴大家一個看原始碼的小技巧,拿到原始碼後,不是直接各個檔案都看一篇,而是先看原始碼中帶的 example 資料夾。如下圖所示,找到 example 的 App 類,裡面就有發起註冊的例項程式碼。如下圖所示:
當然,我們也可以通過官網給的 curl 命令發起 HTTP 請求:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
留個問題:我們都是加一個 Nacos 註解 @EnableDiscoveryClient
,就會自動把服務例項註冊到 Nacos,這個是怎麼做到的?
1.2 發起註冊的流程圖
先來看一下程式碼的流程圖:
跟著這個流程圖,我們 debug 來看下。
1.3 組裝註冊的例項資訊
入口的核心程式碼如下圖所示,它會組裝註冊的例項資訊
,放到一個 instance 變數裡面:
通過程式碼除錯,我們可以看到裡面的例項資訊長這樣:
1.4 組裝註冊請求 request
發起註冊的核心方法是 doRegisterService(),組裝的 request 如下圖所示,裡面有之前組裝的例項資訊 instance,還有指定的 namespace(Nacos 的名稱空間)、serviceName(服務名),groupName(Nacos 的分組)。
1.5 發起遠端呼叫
requestToServer() 方法裡面會呼叫 RpcClient 的 request() 方法:
response = this.currentConnection.request(request, timeoutMills);
就是向 Nacos 發起遠端呼叫,如果是 Nacos 叢集,則是向叢集中的某個 Nacos 節點發起遠端呼叫。
接下來我們看下客戶端是如何選擇一個 Nacos 節點進行註冊的。
二、叢集環境:分散式的前提
如果是 Nacos 叢集環境,客戶端會隨機選擇一個 Nacos 節點發起註冊。
2.1 搭建好一套Nacos 叢集環境
為了講解客戶端是如何註冊到 Nacos 叢集環境的底層原理,我在本地搭建了一個 Nacos 叢集環境,有 3 個 Nacos 服務,它們的 IP 相同,埠號不同。
192.168.10.197:8848
192.168.10.197:8858
192.168.10.197:8868
然後服務 A 和服務 B 都是配置了 Nacos 叢集的 IP 和 埠號的,配置如下所示
spring.cloud.nacos.discovery.server-addr
=192.168.10.197:8848,192.168.10.197:8858,192.168.10.197:8868
整體的結構如下圖所示,服務 A 和 服務 B 都往 Nacos 叢集進行註冊。
但是裡面有一個問題:服務 A 註冊時,是向所有 Nacos 節點發起註冊呢?還是隻向其中一個節點發起註冊?如果只向一個節點註冊,要向哪個節點註冊呢?
答案:在 Client 發起註冊之前,會有一個後臺執行緒隨機拿到 Nacos 叢集服務列表中的一個地址。
Nacos 為什麼會這樣設計?
- 這其實就是一個負載均衡的思想在裡面,每個節點都均勻的分攤請求。
- 保證高可用,當某個節點宕機後,重新拿到其他的 Nacos 節點來建立連線。
接下來我們看下服務 A 是怎麼隨機拿到一個 Nacos 節點的。
三、隨機節點:平等的世界
我們來看下客戶端是如何隨機選擇一個節點的,流程圖如下:
那麼如何找到這些程式碼邏輯呢?思路是怎麼樣的?
我們之前講過,RpcClient 會發起 request 請求,用的是和 Nacos 建立 currentConnection
連線來發起呼叫,程式碼如下:
// 發起呼叫
response = this.currentConnection.request(request, timeoutMills);
這個 currentConnection
是客戶端和 Nacos 叢集中的某個節點建立的連線,我們找下它在哪裡賦值的。程式碼如下:
// 拿到 Nacos 節點資訊
serverInfo = recommendServer.get() == null ? nextRpcServer() : recommendServer.get();
// 連線 Nacos 節點
connectToServer = connectToServer(serverInfo);
// 賦值 currentConnection
this.currentConnection = connectToServer;
而連線的資訊是通過引數 serverInfo 傳進去的,所以我們再看下 serverInfo 在哪裡賦值的。
這個 nextRpcServer() 方法裡面會拿到一個隨機的 Nacos 地址:
// 一個 int 隨機數,範圍 [0 ~ Nacos 個數)
currentIndex.set(new Random().nextInt(serverList.size()));
// index 自增 1
int index = currentIndex.incrementAndGet() % getServerList().size();
// 返回 Nacos 地址
return getServerList().get(index);
小結:客戶端生成一個隨機數,然後通過這個隨機數從 Nacos 服務列表中拿到一個 Nacos 服務地址返回給客戶端,然後客戶端通過這個地址和 Nacos 服務建立連線。Nacos 服務列表中的節點都是平等的,隨機拿到的任何一個節點都是可以用來發起呼叫的。
四、路由轉發:不是我的菜
4.1 發起和轉發請求的流程
為了演示發起註冊的流程,我在這裡模擬了一個註冊請求。
用的是 curl 命令,對 Nacos 節點(127.0.0.1:8848)發起註冊請求:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
請求 URL:/nacos/v1/ns/instance
請求引數:
- serviceName=nacos.naming.serviceName
- ip=20.18.7.11
- port=8080'
之前我們講到,Nacos 的有多個節點可以分別處理請求,當節點發現這個請求不是屬於自己的,就會進行轉發。
如下圖所示:
服務 A 隨機選擇一個 Nacos 節點(圖中為 Nacos1)發起註冊請求,請求引數中包含了例項資訊,Nacos 1 根據例項資訊 hash + 取模拿到正確的節點,如果不屬於自己,則將請求轉發給其他節點(圖中為 Nacos2)
那麼路由轉發的細節是怎麼樣的?這個就涉及到 Distro 協議了,我們接著往下看。
4.1 路由轉發的邏輯
其實 Nacos 節點的路由轉發邏輯比較簡單,先來看下流程圖:
步驟如下:
- ① Nacos 節點從客戶端發起的 request 中拿到客戶端的例項資訊生成 distroTag,如 IP + port 或 service name。
- ② Nacos 根據 distroTag 生成 hash 值。
- ③ 用 hash 值對 Nacos 節點數進行
取餘
,拿到餘數,比如 0、1、2、3。 - ④ 根據餘數從 Nacos 節點列表中拿到指定的節點地址。
我沒看懂的點:我這裡啟動了三個 Nacos 節點,如下圖所示的 三個 Running 節點。但是為什麼 Nacos 的 ServersList 會多了一個 192.168.10.197:8848的節點?
4.2 路由轉發原始碼分析
入口檔案是 DistroFilter.java:
naming/src/main/java/com/alibaba/nacos/naming/web/DistroFilter.java
請求會先到 DistroFilter 類的 doFilter() 方法,拿到正確的節點地址後,將請求轉發出去。
獲取需要轉發節點地址的程式碼如下:
// 找到 Nacos 叢集中的目標節點
final String targetServer = distroMapper.mapSrv(distroTag);
// mapSrv 方法會先 hash,然後再取模,responsibleTag的值類似這樣:"20.18.7.11:8080"
int index = distroHash(responsibleTag) % servers.size();
// distroHash 方法裡面會對 客戶端的 ip+port 字串或者服務名字串 進行 hash
Math.abs(responsibleTag.hashCode() % Integer.MAX_VALUE);
不論是自己處理註冊請求還是轉發給其他節點來處理,都會把例項資訊儲存起來,那麼是如何進行儲存的?
五、處理請求:快到碗裡來
Nacos 目前有兩個版本,v1 和 v2,如果是 v1,則是 instanceController 來處理註冊請求,否則用 instanceControllerV2。本篇我們只講解 v1 版本是怎麼處理請求的。
先上流程圖:
測試用的發起註冊的命令:
curl -X POST 'http://127.0.0.1:8858/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.11&port=8080'
核心程式碼就是這個:
有一個 synchronized 鎖,將臨時的例項資訊存放起來,所以重點看下 這個 consistencyService.put() 方法做了什麼事情。
先看下原始碼:
onPut(key, value);
// 開啟 1s 的延遲任務,將資料同步給其他 Nacos 節點
distroProtocol.sync(new DistroKey(key,KeyBuilder.INSTANCE_LIST_KEY_PREFIX),DataOperation.CHANGE,
DistroConfig.getInstance().getSyncDelayMillis());
這裡面做了三件事情:
- ① 將例項資訊存放到記憶體快取 concurrentHashMap 裡面。
- ② 新增一個任務到 BlockingQueue 裡面,這個任務就是將最新的例項列表通過 UDP 的方式推送給所有客戶端(服務例項),這樣客戶端就拿到了最新的服務例項列表。
- ③ 開啟 1s 的延遲任務,將資料通過給其他 Nacos 節點。
注意:針對第二點和第三點,屬於 Distro 一致性協議的一部分,裡面的內容還比較多,我們放到下一講專門來講。
知識點預告:
-
這裡的儲存例項和同步的方式和 Eureka 有什麼區別?Eureka 用的三層快取架構,Nacos 用的 CopyOnWrite 技術。
-
如何推送給所有客戶端的?UDP 方式。
-
如何同步給 Nacos 其他節點的?Distro 一致性協議。
六、總結
本文通過發起一條註冊請求,講解了 Nacos 客戶端如何隨機選擇節點、Nacos Server 如何路由、Nacos Server 如何儲存註冊例項。
核心流程:
下一篇預告:Nacos 的一致性協議 Distro 協議,揭祕 AP 架構。
我是悟空,期待與你一起打怪升級變強!