螞蟻面試:Springcloud核心組件的底層原理,你知道多少?

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


螞蟻面試:Springcloud核心組件的底層原理,你知道多少?

尼恩特別說明: 尼恩的文章,都會在 《技術自由圈》 公號 發佈, 並且維護最新版本。 如果發現圖片 不可見, 請去 《技術自由圈》 公號 查找

尼恩說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,最近有小夥伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團、螞蟻、得物的面試資格,遇到很多很重要的相關面試題:

說說:螞蟻面試:Springcloud核心組件的底層原理,你知道多少?越多越好。

說說:Springcloud 生態的基礎組件的底層原理?

最近有小夥伴在面試螞蟻,問到了相關的面試題,可以說是逢面必問。

小夥伴沒有系統的去梳理和總結,所以支支吾吾的說了幾句,面試官不滿意,面試掛了。

所以,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,可以充分展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。

當然,這道面試題,以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V175版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。

《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請到文末公號【技術自由圈】獲取

總結: Springcloud體系的幾個核心組件

Nacos 註冊中心的底層原理

與Eureka 、Zookeeper集羣不同Nacos 既能支持AP,又能支持 CP。

Nacos 支持 CP+AP 模式,這意味着 Nacos 可以根據配置識別爲 CP 模式或 AP 模式,默認情況下爲 AP 模式。

  • 如果註冊到Nacos的client節點註冊時ephemeral=true,那麼Nacos集羣對這個client節點的效果就是AP,採用distro協議實現;
  • 而註冊Nacos的client節點註冊時ephemeral=false,那麼Nacos集羣對這個節點的效果就是CP的,採用raft協議實現。

根據client註冊時的ephemeral屬性,AP,CP同時混合存在,只是對不同的client節點效果不同。因此,Nacos 能夠很好地滿足不同場景的業務需求。

網易一面:Eureka怎麼AP?Nacos既CP又AP,怎麼實現的?

AP 模式的 Distro 分佈式協議

Distro 協議是 Nacos 自主研發的一種 AP 分佈式協議,專爲臨時實例設計,確保在部分 Nacos 節點宕機時,整個臨時實例仍可正常運行。

作爲一款具有狀態的中間件應用的內置協議,Distro 確保了各 Nacos 節點在處理大量註冊請求時的統一協調和存儲。

Distro 協議 與Eureka Peer to Peer 模式同步過程, 大致是類似的。

Distro 協議的同步過程,大致如下:

  • 每個節點是平等的都可以處理寫請求,同時將新數據同步至其他節點。
  • 每個節點只負責部分數據,定時發送自己負責數據的校驗值,到其他節點來保持數據⼀致性。
  • 每個節點獨立處理讀請求,並及時從本地發出響應。

Distro 協議 的具體介紹,請參見42歲老架構師尼恩的文章:

網易一面:Eureka怎麼AP?Nacos既CP又AP,怎麼實現的?

CP 模式的 Raft 分佈式協議

Raft 適用於一個管理日誌一致性的協議,相比於 Paxos 協議, Raft 更易於理解和去實現它。

爲了提高理解性,Raft 將一致性算法分爲了幾個部分,包括領導選取(leader selection)、日誌複製(log replication)、安全(safety),並且使用了更強的一致性來減少了必須需要考慮的狀態。

相比Paxos,Raft算法理解起來更加直觀。

Raft算法將Server node劃分爲3種狀態,或者也可以稱作角色:

  • Leader:負責Client交互和log複製,同一時刻系統中最多存在1個。
  • Follower:被動響應請求RPC,從不主動發起請求RPC。
  • Candidate:一種臨時的角色,只存在於leader的選舉階段,某個節點想要變成leader,那麼就發起投票請求,同時自己變成candidate。如果選舉成功,則變爲candidate,否則退回爲follower

Raft 分佈式協議中node 狀態或者說角色的流轉如下:

圖片

在Raft中,問題被分解爲:

  • 領導選舉
  • 日誌複製
  • 安全和成員變化。

數據一致性通過複製日誌來實現::

  • 日誌:每臺機器都保存一份日誌,日誌來源於客戶端的請求,包含一系列的命令。

  • 狀態機:狀態機會按順序執行這些命令。

  • 一致性模型:在分佈式環境中,確保多臺機器的日誌保持一致,從而使狀態機回放時的狀態保持一致。

Raft算法選主流程

Raft把集羣中的節點分爲三種狀態:Leader、 Follower 、Candidate,每種狀態下負責的任務也是不一樣的,Raft運行時只存在Leader與Follower兩種狀態。

  • Leader:負責日誌的同步管理,處理來自客戶端的請求,與Follower保持heartBeat的聯繫;
  • Follower:剛啓動時所有節點爲Follower狀態。響應Candidate的請求,選舉完成後它的責任是響應Leader的日誌同步請求,把請求到Follower的事務轉發給Leader;
  • Candidate:負責選舉投票,一輪選舉開始時節點從Follower轉爲Candidate發起選舉,選舉出Leader後從Candidate轉爲Leader狀態;

Raft中使用心跳機制來出發leader選舉。當服務器啓動的時候,服務器成爲follower。

只要follower從leader或者candidate收到有效的RPCs就會保持follower狀態。

如果follower在一段時間內(該段時間被稱爲election timeout)沒有收到消息,則它會假設當前沒有可用的leader,然後開啓選舉新leader的流程。

1.基礎概念之 Term 任期

Term任期的概念類比中國歷史上的朝代更替,Raft 算法將時間劃分成爲任意不同長度的任期(term)。

時間劃分爲不同的Term(任期)。

Term(任期)用連續的數字進行表示。

一個Term包括兩個階段:

  • 選舉階段

  • 正常階段。

每個服務器都會保存當前的任期Current Term。用於發送和接受RPC消息時驗證比較。

如果一個候選人贏得選舉,它將在該任期的剩餘時間內擔任領導者。

在某些情況下,選票可能會被平分,導致沒有選出領導者,此時將開始新的任期並立即進行下一次選舉。Raft 算法確保在給定的任期中只有一個領導者。

每次導致新的選舉發生,Term就會改變+1。

每次Term的遞增都將發生新一輪的選舉,Raft保證一個Term只有一個Leader,在Raft正常運轉中所有的節點的Term都是一致的,如果節點不發生故障一個Term(任期)會一直保持下去,當某節點收到的請求中Term比當前Term小時則拒絕該請求;
raft

每一個任期的開始都是一次選舉(election),一個或多個候選人嘗試成爲領導者。

2.基礎概念之 Log Entry 日誌條目

每一個client操作,對於Leader來說,都是增加一個Log Entry,然後複製同步到其他的Server。包括以下數據:

  • term Leader收到log時的term
  • index log下標。log存儲結構是一個List
  • command 操作指令

3.基礎概念之 RPC

Raft 算法中服務器節點之間通信使用遠程過程調用(RPCs),並且基本的一致性算法只需要兩種類型的 RPCs,爲了在服務器之間傳輸快照增加了第三種 RPC。

RPC有三種:

  • RequestVote RPC:候選人在選舉期間發起
  • AppendEntries RPC:領導人發起的一種心跳機制,複製日誌也在該命令中完成
  • InstallSnapshot RPC:領導者使用該RPC來發送快照給太落後的追隨者

RequestVote RPC(RequestVote 遠程過程調用)通常用於選舉領導者或者主節點。在分佈式系統中,節點之間需要進行選舉以確定領導者,而 RequestVote RPC 就是用來實現選舉過程的通信機制之一。

RequestVote RPC 的基本流程如下:

  1. 節點發送 RequestVote 請求給其他節點,請求它們投票支持自己成爲領導者。
  2. 其他節點收到請求後,會根據自己的選舉算法判斷是否給予支持。
  3. 如果其他節點認爲該節點可以成爲領導者,就會向其發送投票,並更新自己的狀態以反映投票結果。
  4. 請求節點收到足夠多的投票後,就可以成爲領導者,並開始執行相應的操作。

RequestVote RPC 的內容通常包括:

  • 請求節點的ID:用於標識請求的發起者。
  • 請求的任期號:用於確保只有最新的領導者才能獲得其他節點的投票。
  • 候選人的最後日誌條目的索引和任期號:用於其他節點判斷候選人的日誌是否比自己的日誌更新,從而決定是否給予投票支持。
  • 投票結果:表示其他節點是否投票支持候選人成爲領導者。

通過 RequestVote RPC,分佈式系統中的節點可以進行選舉,確保系統在沒有領導者時能夠快速選舉出新的領導者,從而保證系統的正常運行。

AppendEntries RPC(AppendEntries 遠程過程調用)通常用於領導者節點向其他節點發送日誌條目,以實現日誌複製。這個屬於日誌增量複製的類型。

領導者節點負責將自己的日誌複製給其他節點,以確保系統中的所有節點都擁有相同的日誌副本,從而保持數據一致性。

AppendEntries RPC 的基本流程如下:

  1. 領導者節點將自己的日誌條目以 AppendEntries 請求的形式發送給其他節點。
  2. 其他節點收到請求後,會根據領導者的日誌條目信息進行處理,將日誌條目追加到自己的日誌中。
  3. 如果其他節點的日誌中存在與領導者發送的日誌衝突的條目,節點會根據一定的規則進行日誌的比較和衝突解決。
  4. 處理完請求後,其他節點會向領導者發送響應,表示是否成功追加日誌條目。

AppendEntries RPC 的內容通常包括:

  • 領導者的ID:用於標識發送請求的節點。
  • 領導者的任期號:用於確保其他節點只接受來自最新領導者的日誌複製請求。
  • 領導者的日誌條目:包括日誌條目的索引、任期號以及具體的日誌內容。
  • 領導者的前一條日誌條目的索引和任期號:用於其他節點在追加日誌條目時進行一致性檢查。

通過 AppendEntries RPC,領導者節點可以向其他節點發送日誌條目,實現日誌的複製和一致性維護,從而確保整個分佈式系統的數據一致性和正確性。

InstallSnapshot RPC(安裝快照遠程過程調用)是用於在Raft一致性算法中進行日誌複製的一種通信協議,屬於日誌的全量複製。

在Raft中,當領導者節點發現跟隨者節點的日誌太過龐大,或者跟隨者節點剛剛加入集羣時,會通過InstallSnapshot RPC來快速複製日誌狀態。

InstallSnapshot RPC 的基本流程如下:

  1. 領導者節點檢測到某個跟隨者節點的日誌太過龐大,或者該節點剛剛加入集羣。
  2. 領導者節點將當前的系統狀態(快照)打包,並通過InstallSnapshot RPC將該快照發送給跟隨者節點。
  3. 跟隨者節點接收到領導者發送的快照後,將其應用到自己的狀態機中,使得自己的狀態與領導者節點的狀態一致。
  4. 跟隨者節點同時接收到快照的元數據(如快照的最後一個包含的日誌索引和任期號等),並根據這些元數據更新自己的日誌。

InstallSnapshot RPC 的內容通常包括:

  • 領導者的ID:用於標識發送請求的節點。
  • 領導者的任期號:用於確保其他節點只接受來自最新領導者的快照。
  • 快照數據:包括當前系統的狀態信息,如存儲的數據、索引等。
  • 快照元數據:包括快照的最後一個包含的日誌索引和任期號等信息,用於更新跟隨者節點的日誌狀態。

通過InstallSnapshot RPC,Raft算法可以實現在節點之間快速複製大規模的系統狀態,從而提高了系統的效率和性能。

Raft 選舉流程(Election)

下面兩種情況會發起選舉

  • Raft初次啓動,不存在Leader,發起選舉;
  • Leader宕機或Follower沒有接收到Leader的heartBeat,發生election timeout從而發起選舉。

Raft初始狀態時所有節點都處於Follower狀態,並且隨機睡眠一段時間,這個時間在0~1000ms之間。

最先醒來的節點 進入Candidate狀態,並且發起投票,即向其它所有節點發出requst_vote請求,同時投自己一票,這個過程會有三種結果:

  1. 自己被選成了主。當收到了大多數的投票後,狀態切成leader,並且定期給其它的所有server發心跳消息(其實是不帶log的AppendEntriesRPC)以告訴對方自己是current_term_id所標識的term的leader。每個term最多隻有一個leader,term id作爲logical clock,在每個RPC消息中都會帶上,用於檢測過期的消息,一個server收到的RPC消息中的rpc_term_id比本地的current_term_id更大時,就更新current_term_id爲rpc_term_id,並且如果當前state爲leader或者candidate時,將自己的狀態切成follower。如果rpc_term_id比本地的current_term_id更小,則拒絕這個RPC消息。
  2. 別人成爲了主。如1所述,當candidate在等待投票的過程中,收到了大於或者等於本地的current_term_id的聲明對方是leader的AppendEntriesRPC時,則將自己的state切成follower,並且更新本地的current_term_id。
  3. 沒有選出主。當投票被瓜分,沒有任何一個candidate收到了majority的vote時,沒有leader被選出。這種情況下,每個candidate等待的投票的過程就超時了,接着candidates都會將本地的current_term_id再加1,發起RequestVoteRPC進行新一輪的leader election。

投票策略:
一個任期內,一個節點只能投一張票,具體的是否同意和後續的Safety有關。

投票選舉流程圖解

(1)follower增加當前的term,轉變爲candidate。

(2)candidate投票給自己,併發送RequestVote RPC給集羣中的其他服務器。

(3)收到RequestVote的服務器,在同一term中只會按照先到先得投票給至多一個candidate。且只會投票給log至少和自身一樣新的candidate。

圖片初始節點

圖片Node1 轉爲 Candidate 發起選舉

圖片Node 確認選舉

圖片

Node1 成爲 leader,發送 Heartbeat

candidate節點保持(2)的狀態,直到下面三種情況中的一種發生。

  • 該節點贏得選舉,即收到大多數節點的投票,然後轉變爲 leader 狀態。
  • 另一個服務器成爲 leader,即收到合法心跳包(term 值大於或等於當前自身 term 值),然後轉變爲 follower 狀態。
  • 一段時間後仍未確定勝者,此時會啓動新一輪的選舉。

爲了解決當票數相同時無法確定 leader 的問題,Raft 使用隨機選舉超時時間。

Raft 日誌複製

當 Leader 選舉產生後,它開始負責處理客戶端的請求。

所有的事務(更新操作)請求都必須先由 Leader 處理。日誌複製(Log Replication)就是爲了確保執行相同的操作序列所做的工作。

日誌複製(Log Replication)的主要目的是確保節點的一致性,在此階段執行的操作都是爲了確保一致性和高可用性。

在 Raft 中,當接收到客戶端的日誌(事務請求)後,先把該日誌追加到本地的Log中,然後通過heartbeat把該Entry同步給其他Follower,Follower接收到日誌後記錄日誌然後向Leader發送ACK,當Leader收到大多數(n/2+1)Follower的ACK信息後將該日誌設置爲已提交併追加到本地磁盤中,通知客戶端並在下個heartbeat中Leader將通知所有的Follower將該日誌存儲在自己的本地磁盤中。

Leader選舉出來後,就可以開始處理客戶端請求。流程如下:

  1. Leader收到客戶端請求後,leader會把它作爲一個log entry,append到它自己的日誌中。並向其它server發送AppendEntriesRPC(添加日誌)請求。
  2. 其它server收到AppendEntriesRPC請求後,判斷該append請求滿足接收條件,如果滿足條件就將其添加到本地的log中,並給Leader發送添加成功的response。
  3. 如果某個follower宕機了或者運行的很慢,或者網絡丟包了,則會一直給這個follower發AppendEntriesRPC直到日誌一致。
  4. Leader在收到大多數server添加成功的response後,就將該條log正式提交。提交後的log日誌就意味着已經被raft系統接受,並能應用到狀態機中了。每個日誌條目也包含一個整數索引來表示它在日誌中的位置。

日誌由有序編號的日誌條目組成。

每個日誌條目包含它被創建時的任期號(每個方塊中的數字),並且包含用於狀態機執行的命令。任期號用來檢測在不同服務器上日誌的不一致性。
raft

如上圖所示,索引 1-7 的日誌至少在其他兩個節點上覆製成功,就認爲該日誌是 commited 狀態,而索引爲8 的日誌並未複製到多數節點,是 uncommitted 狀態。

Leader Append-Only 原則

leader 對自己的日誌不能覆蓋和刪除,只能進行 append 新日誌的操作。

Log Matching 特性

Raft 的日誌機制來維護不同服務器的日誌之間的高層次的一致性。有下面兩條特性

  • 如果在不同的日誌中的兩個條目擁有相同的索引和任期號,那麼他們存儲了相同的指令。
  • 如果在不同的日誌中的兩個條目擁有相同的索引和任期號,那麼他們之前的所有日誌條目也全部相同。

第一個特性來自這樣的一個事實,領導人最多在一個任期裏在指定的一個日誌索引位置創建一條日誌條目,同時日誌條目在日誌中的位置也從來不會改變。

第二個特性由附加日誌 RPC 的一個簡單的一致性檢查所保證。在發送附加日誌 RPC 的時候,領導人會把新的日誌條目緊接着之前的條目的索引位置和任期號包含在裏面。如果跟隨者在它的日誌中找不到包含相同索引位置和任期號的條目,那麼他就會拒絕接收新的日誌條目。一致性檢查就像一個歸納步驟:一開始空的日誌狀態肯定是滿足日誌匹配特性的,然後一致性檢查保護了日誌匹配特性當日志擴展的時候。因此,每當附加日誌 RPC 返回成功時,領導人就知道跟隨者的日誌一定是和自己相同的了。

強制複製

當一個新的leader選出來的時候,它的日誌和其它的follower的日誌可能不一樣,這個時候,就需要一個機制來保證日誌是一致的。

如下圖所示,一個新leader產生時,集羣狀態可能如下:
在這裏插入圖片描述

最上面這個是新leader,a~f是follower,都出現了日誌不一致的情況。

  • a,b:follower 可能丟失部分日誌
  • c,d:follower 本地可能 uncommited 的日誌
  • e,f:follower 可能既缺少本該有的日誌,也多出額外的日誌

在 Raft 算法中,領導人處理不一致是通過強制跟隨者直接複製自己的日誌來解決了。這意味着在跟隨者中的衝突的日誌條目會被領導人的日誌覆蓋。leader會爲每個follower維護一個nextIndex,表示leader給各個follower發送的下一條log entry在log中的index,初始化爲leader的最後一條log entry的下一個位置。leader給follower發送AppendEntriesRPC消息,帶着{term_id, (nextIndex-1)}, follower接收到AppendEntriesRPC後,會從自己的log中找是不是存在這樣的log entry,如果不存在,就給leader回覆拒絕消息,然後leader則將nextIndex減1,再重複發送AppendEntriesRPC,直到AppendEntriesRPC消息被接收。

舉個例子
以leader和b爲例:初始化,nextIndex爲11,leader給b發送AppendEntriesRPC(6,10),b在自己log的10號槽位中沒有找到term_id爲6的log entry。則給leader迴應一個拒絕消息。

接着,leader將nextIndex減一,變成10,然後給b發送AppendEntriesRPC(6, 9),b在自己log的9號槽位中同樣沒有找到term_id爲6的log entry。

循環下去,直到leader發送了AppendEntriesRPC(4,4),b在自己log的槽位4中找到了term_id爲4的log entry。

接收了消息。隨後,leader就可以從槽位5開始給b推送日誌了。

解決衝突的性能優化

當附加日誌 RPC 的請求被拒絕的時候,跟隨者可以返回衝突的條目的任期號和自己存儲的那個任期的最早的索引地址。藉助這些信息,領導人可以減小 nextIndex 越過所有那個任期衝突的所有日誌條目;這樣就變成每個任期需要一次附加條目 RPC 而不是每個條目一次。

Nacos 如何實現Raft算法

Nacos server在啓動時,會通過RunningConfig.onApplicationEvent()方法調用RaftCore.init()方法。

啓動選舉

public static void init() throws Exception {
 
    Loggers.RAFT.info("initializing Raft sub-system");
 
    // 啓動Notifier,輪詢Datums,通知RaftListener
    executor.submit(notifier);
     
    // 獲取Raft集羣節點,更新到PeerSet中
    peers.add(NamingProxy.getServers());
 
    long start = System.currentTimeMillis();
 
    // 從磁盤加載Datum和term數據進行數據恢復
    RaftStore.load();
 
    Loggers.RAFT.info("cache loaded, peer count: {}, datum count: {}, current term: {}",
        peers.size(), datums.size(), peers.getTerm());
 
    while (true) {
        if (notifier.tasks.size() <= 0) {
            break;
        }
        Thread.sleep(1000L);
        System.out.println(notifier.tasks.size());
    }
 
    Loggers.RAFT.info("finish to load data from disk, cost: {} ms.", (System.currentTimeMillis() - start));
 
    GlobalExecutor.register(new MasterElection()); // Leader選舉
    GlobalExecutor.register1(new HeartBeat()); // Raft心跳
    GlobalExecutor.register(new AddressServerUpdater(),         GlobalExecutor.ADDRESS_SERVER_UPDATE_INTERVAL_MS);
 
    if (peers.size() > 0) {
        if (lock.tryLock(INIT_LOCK_TIME_SECONDS, TimeUnit.SECONDS)) {
            initialized = true;
            lock.unlock();
        }
    } else {
        throw new Exception("peers is empty.");
    }
 
    Loggers.RAFT.info("timer started: leader timeout ms: {}, heart-beat timeout ms: {}",
        GlobalExecutor.LEADER_TIMEOUT_MS, GlobalExecutor.HEARTBEAT_INTERVAL_MS);
}

在init方法主要做了如下幾件事:

  1. 獲取Raft集羣節點 peers.add(NamingProxy.getServers());
  2. Raft集羣數據恢復 RaftStore.load();
  3. Raft選舉 GlobalExecutor.register(new MasterElection());
  4. Raft心跳 GlobalExecutor.register(new HeartBeat());
  5. Raft發佈內容
  6. Raft保證內容一致性

選舉流程

其中,raft集羣內部節點間是通過暴露的Restful接口,代碼在 RaftController 中。

RaftController控制器是raft集羣內部節點間通信使用的,具體的信息如下

POST HTTP://{ip:port}/v1/ns/raft/vote : 進行投票請求

POST HTTP://{ip:port}/v1/ns/raft/beat : Leader向Follower發送心跳信息

GET HTTP://{ip:port}/v1/ns/raft/peer : 獲取該節點的RaftPeer信息

PUT HTTP://{ip:port}/v1/ns/raft/datum/reload : 重新加載某日誌信息

POST HTTP://{ip:port}/v1/ns/raft/datum : Leader接收傳來的數據並存入

DELETE HTTP://{ip:port}/v1/ns/raft/datum : Leader接收傳來的數據刪除操作

GET HTTP://{ip:port}/v1/ns/raft/datum : 獲取該節點存儲的數據信息

GET HTTP://{ip:port}/v1/ns/raft/state : 獲取該節點的狀態信息{UP or DOWN}

POST HTTP://{ip:port}/v1/ns/raft/datum/commit : Follower節點接收Leader傳來得到數據存入操作

DELETE HTTP://{ip:port}/v1/ns/raft/datum : Follower節點接收Leader傳來的數據刪除操作

GET HTTP://{ip:port}/v1/ns/raft/leader : 獲取當前集羣的Leader節點信息

GET HTTP://{ip:port}/v1/ns/raft/listeners : 獲取當前Raft集羣的所有事件監聽者
RaftPeerSet

心跳機制

Raft中使用心跳機制來觸發leader選舉。

心跳定時任務是在GlobalExecutor 中,通過 GlobalExecutor.register(new HeartBeat())註冊心跳定時任務,具體操作包括:

  • 重置Leader節點的heart timeout、election timeout;
  • sendBeat()發送心跳包
public class HeartBeat implements Runnable {
    @Override
    public void run() {
        try {

            if (!peers.isReady()) {
                return;
            }

            RaftPeer local = peers.local();
            local.heartbeatDueMs -= GlobalExecutor.TICK_PERIOD_MS;
            if (local.heartbeatDueMs > 0) {
                return;
            }

            local.resetHeartbeatDue();

            sendBeat();
        } catch (Exception e) {
            Loggers.RAFT.warn("[RAFT] error while sending beat {}", e);
        }
    }
}

簡單說明了下Nacos中的Raft一致性實現,更詳細的流程,可以下載源碼,查看 RaftCore 進行了解。

源碼可以通過以下地址檢出:

git clone https://github.com/alibaba/nacos.git

sentinel高可用組件的底層原理

Sentinel是一個系統性的高可用保障工具,提供了等一系列的能力

  • 限流
  • 降級
  • 熔斷
  • 預熱

基於這些能力做了語意化概念抽象,這些概念對於理解實現機制特別有幫助。

圖片

流量控制有以下幾個角度:

  • 資源的調用關係,例如資源的調用鏈路,資源和資源之間的關係;
  • 運行指標,例如 QPS、線程池、系統負載等;
  • 控制的效果,例如直接限流、冷啓動、排隊等。

Sentinel 的設計理念是讓您自由選擇控制的角度,並進行靈活組合,從而達到想要的效果。

Sentinel使用滑動時間窗口算法來實現流量控制,流量統計。

滑動時間窗算法的核心思想是將一段時間劃分爲多個時間窗口,並在每個時間窗口內對請求進行計數,以確定是否允許繼續請求。

以下是Sentinel底層滑動時間窗口限流算法的簡要實現步驟:

  • 時間窗口劃分:將整個時間範圍劃分爲多個固定大小的時間窗口(例如1秒一個窗口)。這些時間窗口會隨着時間的流逝依次滑動。
  • 計數器:爲每個時間窗口維護一個計數器,用於記錄在該時間窗口內的請求數。
  • 請求計數:當有請求到來時,將其計入當前時間窗口的計數器中。
  • 滑動時間窗口:定期滑動時間窗口,將過期的時間窗口刪除,並創建新的時間窗口。這樣可以保持時間窗口的滾動。
  • 限流判斷:當有請求到來時,Sentinel會檢查當前時間窗口內的請求數是否超過了預設的限制閾值。如果超過了限制閾值,請求將被拒絕或執行降級策略。
  • 計數重置:定期重置過期時間窗口的計數器,以確保計數器不會無限增長。

這種滑動時間窗口算法允許在一段時間內平滑控制請求的流量,而不是僅基於瞬時請求速率進行限流。

它考慮了請求的歷史分佈,更適用於應對突發流量和週期性負載的情況。

Sentinel熔斷降級,是如何實現的?

在微服務架構中,Sentinel 作爲一種流量控制、熔斷降級和服務降級的解決方案,得到了廣泛的應用。

Sentinel是一個開源的流量控制和熔斷降級庫,用於保護分佈式系統免受大量請求的影響。

Sentinel熔斷降級,是如何實現的?尼恩建議大家,從以下的幾個維度去作答:

第一個維度,Sentinel主要功能:

一:熔斷機制

  1. Sentinel使用滑動窗口統計請求的成功和失敗情況。這些統計信息包括成功的請求數、失敗的請求數等。
  2. 當某個資源(例如一個API接口)的錯誤率超過閾值或其他指標達到預設的條件,Sentinel將觸發熔斷機制。
  3. 一旦熔斷觸發,Sentinel將暫時阻止對該資源的請求,防止繼續失敗的請求對系統造成更大的影響。

二:降級機制

  1. Sentinel還提供了降級機制,可以在資源負載過重或其他異常情況下,限制資源的訪問速率,以保護系統免受過多的請求衝擊。
  2. 降級策略可以根據需要定製,可以是慢調用降級、異常比例降級等。

三:高可用性機制

Sentinel的高可用性主要通過以下方式來實現:

a. 多節點部署:將Sentinel配置爲多節點部署,確保即使一個節點發生故障,其他節點仍然能夠繼續工作。

b. 持久化配置:Sentinel支持將配置信息持久化到外部存儲,如Nacos、Redis等。這樣,即使Sentinel節點重啓,它可以加載之前的配置信息。

c. 集羣流控規則:Sentinel支持集羣流控規則,多個節點可以共享流量控制規則,以協同工作來保護系統。

d. 實時監控:Sentinel提供了實時監控和儀表板,可以查看系統的流量控制和熔斷降級情況,幫助及時發現問題並採取措施。

四:自適應控制

Sentinel具有自適應控制的功能,它可以根據系統的實際情況自動調整流量控制和熔斷降級策略,以適應不同的負載和流量模式。

總的來說,Sentinel的高可用性熔斷降級機制是通過多節點部署、持久化配置、實時監控、自適應控制等多種手段來實現的。

這使得Sentinel能夠在分佈式系統中保護關鍵資源免受異常流量的影響,並保持系統的穩定性和可用性。

那麼,Sentinel是如何實現這些功能的呢?在說說 Sentinel 的基本組件。

第二個維度, Sentinel 的基本組件:

Sentinel 主要包括以下幾個部分:資源(Resource)、規則(Rule)、上下文(Context)和插槽(Slot)。

  • 資源是我們想要保護的對象,比如一個遠程服務、一個數據庫連接等。
  • 規則是定義如何保護資源的,比如我們可以通過設置閾值、時間窗口等方式來決定何時進行限流、熔斷等操作。
  • 上下文是一個臨時的存儲空間,用於存儲資源的狀態信息,比如當前的 QPS 等。
  • 插槽屬於責任鏈模式中的處理器/過濾器, 完成資源規則的計算和驗證。

第三個維度, Sentinel 的流量治理幾個核心步驟:

在 Sentinel 的運行過程中,主要分爲以下幾個核心步驟:

  1. 資源註冊:當一個資源被創建時,需要將其註冊到 Sentinel。在註冊過程中,會爲資源創建一個對應的上下文,並將資源的規則存儲到插槽中。
  2. 流量控制:當有請求訪問資源時,Sentinel 會根據資源的規則進行流量控制。如果當前 QPS 超過了規則設定的閾值,Sentinel 就會拒絕請求,以防止系統過載。
  3. 熔斷降級:當資源出現異常時,Sentinel 會根據規則進行熔斷或降級處理。熔斷是指暫時切斷對資源的訪問,以防止異常擴散。降級則是提供一種備用策略,當主策略無法正常工作時,可以切換到備用策略。
  4. 規則更新:在某些情況下,我們可能需要動態調整資源的規則。Sentinel 提供了 API 接口,可以方便地更新資源的規則。

通過以上分析,我們可以看出,Sentinel 的核心思想是通過規則來管理和控制資源。這種設計使得 Sentinel 具有很強的可擴展性和靈活性。我們可以根據業務需求,定製各種複雜的規則。

第四個維度, Sentinel 的源碼架構維度:

Sentinel 是一種非常強大的流量控制、熔斷降級和服務降級的解決方案。 已經成爲了替代Hystrix的主要高可用組件。

Sentinel 的源碼層面的兩個核心架構:

Sentinel可以用來幫助我們實現流量控制、服務降級、服務熔斷,而這些功能的實現都離不開接口被調用的實時指標數據。

Sentinel 是一種非常強大的流量控制、熔斷降級和服務降級的解決方案。 已經成爲了替代Hystrix的主要高可用組件。

回到源碼層面,在 Sentinel 源碼,包括以下二大架構:

  • 責任鏈模式架構
  • 滑動窗口數據統計架構

在這裏插入圖片描述

尼恩說明: 兩大架構的源碼,簡單說說就可以了,具體可以參見《Sentinel 學習聖經》 最新版本。

更加詳細的架構圖,具體如下:

圖片

上圖中的右上角就是滑動窗口的示意圖,是 StatisticSlot 的具體實現。

StatisticSlot 是 Sentinel 的核心功能插槽之一,用於統計實時的調用數據。

Sentinel 是基於滑動窗口實現的實時指標數據收集統計,底層採用高性能的滑動窗口數據結構 LeapArray 來統計實時的秒級指標數據,可以很好地支撐寫多於讀的高併發場景。

圖片

有關Sentinel的更多的、更加系統化知識,請參見尼恩寫的5W字PDF 《Sentinel學習聖經》

滑動窗口的核心數據結構

  • ArrayMetric:滑動窗口核心實現類。
  • LeapArray:滑動窗口頂層數據結構,包含一個一個的窗口數據。
  • WindowWrap:每一個滑動窗口的包裝類,其內部的數據結構用 MetricBucket 表示。
  • MetricBucket:指標桶,例如通過數量、阻塞數量、異常數量、成功數量、響應時間,已通過未來配額(搶佔下一個滑動窗口的數量)。
  • MetricEvent:指標類型,例如通過數量、阻塞數量、異常數量、成功數量、響應時間等。

ArrayMetric 源碼

滑動窗口的入口類爲 ArrayMetric,實現了 Metric 指標收集核心接口,該接口主要定義一個滑動窗口中成功的數量、異常數量、阻塞數量,TPS、響應時間等數據。

public class ArrayMetric implements Metric {

    private final LeapArray<MetricBucket> data;

    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
        if (enableOccupy) {
            this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
        } else {
            this.data = new BucketLeapArray(sampleCount, intervalInMs);
        }
    }
 }
  • int intervalInMs:表示一個採集的時間間隔,即滑動窗口的總時間,例如 1 分鐘。
  • int sampleCount:在一個採集間隔中抽樣的個數,默認爲 2,即一個採集間隔中會包含兩個相等的區間,一個區間就是一個窗口。
  • boolean enableOccupy:是否允許搶佔,即當前時間戳已經達到限制後,是否可以佔用下一個時間窗口的容量。

LeapArray 源碼

LeapArray 用來承載滑動窗口,即成員變量 array,類型爲 AtomicReferenceArray<WindowWrap<T>>,保證創建窗口的原子性(CAS)。

public abstract class LeapArray<T> {

    //每一個窗口的時間間隔,單位爲毫秒
    protected int windowLengthInMs;
    //抽樣個數,就一個統計時間間隔中包含的滑動窗口個數
    protected int sampleCount;
    //一個統計的時間間隔
    protected int intervalInMs;
    //滑動窗口的數組,滑動窗口類型爲 WindowWrap<MetricBucket>
    protected final AtomicReferenceArray<WindowWrap<T>> array;
    private final ReentrantLock updateLock = new ReentrantLock();
    
    public LeapArray(int sampleCount, int intervalInMs) {
        this.windowLengthInMs = intervalInMs / sampleCount;
        this.intervalInMs = intervalInMs;
        this.sampleCount = sampleCount;
        this.array = new AtomicReferenceArray<>(sampleCount);
    }
 }

MetricBucket 源碼

Sentinel 使用 MetricBucket 統計一個窗口時間內的各項指標數據,這些指標數據包括請求總數、成功總數、異常總數、總耗時、最小耗時、最大耗時等,而一個 Bucket 可以是記錄一秒內的數據,也可以是 10 毫秒內的數據,這個時間長度稱爲窗口時間。

public class MetricBucket {
    /**
     * 存儲各事件的計數,比如異常總數、請求總數等
     */
    private final LongAdder[] counters;
    /**
     * 這段事件內的最小耗時
     */
    private volatile long minRt;
}

Bucket 記錄一段時間內的各項指標數據用的是一個 LongAdder 數組,數組的每個元素分別記錄一個時間窗口內的請求總數、異常數、總耗時。也就是說:MetricBucket 包含一個 LongAdder 數組,數組的每個元素代表一類 MetricEvent。LongAdder 保證了數據修改的原子性,並且性能比 AtomicLong 表現更好。

public enum MetricEvent {
    PASS,
    BLOCK,
    EXCEPTION,
    SUCCESS,
    RT,
    OCCUPIED_PASS
}

當需要獲取 Bucket 記錄總的成功請求數或者異常總數、總的請求處理耗時,可根據事件類型 (MetricEvent) 從 Bucket 的 LongAdder 數組中獲取對應的 LongAdder,並調用 sum 方法獲取總數。

public long get(MetricEvent event) {
    return counters[event.ordinal()].sum();
}

當需要 Bucket 記錄一個成功請求或者一個異常請求、處理請求的耗時,可根據事件類型(MetricEvent)從 LongAdder 數組中獲取對應的 LongAdder,並調用其 add 方法。

public void add(MetricEvent event, long n) {
     counters[event.ordinal()].add(n);
}

WindowWrap 源碼

因爲 Bucket 自身並不保存時間窗口信息,所以 Sentinel 給 Bucket 加了一個包裝類 WindowWrap。Bucket 用於統計各項指標數據,WindowWrap 用於記錄 Bucket 的時間窗口信息(窗口的開始時間、窗口的大小),WindowWrap 數組就是一個滑動窗口。

public class WindowWrap<T> {
    /**
     * 單個窗口的時間長度(毫秒)
     */
    private final long windowLengthInMs;
    /**
     * 窗口的開始時間戳(毫秒)
     */
    private long windowStart;
    /**
     * 統計數據
     */
    private T value;
}

總的來說:

  • WindowWrap 用於包裝 Bucket,隨着 Bucket 一起創建。
  • WindowWrap 數組實現滑動窗口,Bucket 只負責統計各項指標數據,WindowWrap 用於記錄 Bucket 的時間窗口信息。
  • 定位 Bucket 實際上是定位 WindowWrap,拿到 WindowWrap 就能拿到 Bucket。

loadbanlancer負載均衡組件的底層原理

介紹loadbanlancer負載均衡組件之前,首先尼恩給大家介紹一下 負載均衡的基礎知識。

基礎原理:負載均衡的類型

  • 服務器端負載均衡
  • 客戶端側負載均衡

1、服務器端負載均衡:

傳統的方式前端發送請求會到我們的的nginx上去,nginx作爲反向代理,然後路由給後端的服務器,由於負載均衡算法是nginx提供的,而nginx是部署到服務器端的,

所以這種方式又被稱爲服務器端負載均衡。

2、客戶端側負載均衡:

現在有三個實例,內容中心可以通過discoveryClient 獲取到用戶中心的實例信息,如果我們再訂單中心寫一個負載均衡 的規則計算請求那個實例,交給restTemplate進行請求,這樣也可以實現負載均衡,這個算法裏面,負載均衡是有訂單中心提供的,而訂單中心相對於用戶中心是一個客戶端,所以這種方式又稱爲客戶端負負載均衡。

基礎原理:常見的負載均衡算法的實現

無論是服務器端負載均衡,還是客戶端側負載均衡,都可以使用下面的常見的負載均衡算法包括:

  1. 輪詢(Round Robin):按照順序依次將請求分配給每個服務器,循環往復。適用於服務器性能相近的情況。
  2. 加權輪詢(Weighted Round Robin):在輪詢的基礎上,給每個服務器分配一個權重,根據權重比例分配請求。適用於服務器性能不均勻的情況。
  3. 隨機(Random):隨機選擇一個服務器來處理請求,不考慮服務器的性能。適用於服務器性能相近且負載不高的情況。
  4. 最小連接數(Least Connections):選擇當前連接數最少的服務器來處理請求。適用於服務器性能差異較大,但負載相對均勻的情況。
  5. IP哈希(IP Hash):根據客戶端的IP地址進行哈希計算,然後將請求分配給對應的服務器。適用於需要將同一個客戶端的請求始終分配給同一臺服務器的場景,比如會話保持。
  6. 一致性哈希(Consistent Hashing):根據請求的鍵(如URL、客戶端ID等)進行哈希計算,然後將請求路由到哈希環上最近的服務器。適用於需要動態擴展和縮減服務器集羣的場景。

隨機(Random)負載均衡算法的實現

利用隨機數從所有服務器中隨機選取一臺,可以用服務器數組下標獲取。

public class RandomLoadBalance {
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Server {
        private int serverId;
        private String name;
    }

  // 隨機算法的核心邏輯
    public static Server selectServer(List<Server> serverList) {
        Random selector = new Random();
        int next = selector.nextInt(serverList.size());
        return serverList.get(next);
    }

    public static void main(String[] args) {
        List<Server> serverList = new ArrayList<>();
        serverList.add(new Server(1, "服務器1"));
        serverList.add(new Server(2, "服務器2"));
        serverList.add(new Server(3, "服務器3"));
        for (int i = 0; i < 10; i++) {
            Server selectedServer = selectServer(serverList);
            System.out.format("第%d次請求,選擇服務器%s\n", i + 1, selectedServer.toString());
        }
    }
}

輪詢(Round Robin、RR)負載均衡算法的實現

依次將用戶的訪問請求,按循環順序分配到web服務節點上,從1開始到最後一臺服務器節點結束,然後再開始新一輪的循環。這種算法簡單,但是沒有考慮到每臺節點服務器的具體性能,請求分發往往不均衡。

已知服務器:

服務器 權重
s1 1
s2 2
s3 3

優點:實現簡單,無需記錄各種服務的狀態,是一種無狀態的負載均衡策略。
缺點:實現絕對公平,當各個服務器性能不一致的情況,無法根據服務器性能去分配,無法合理利用服務器資源。

public class RoundRobin {
    //計數器:每次輪詢一個節點自增1
    private static AtomicInteger NEXT_SERVER_COUNTER = new AtomicInteger(0);

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Server {
        private int serverId;
        private String name;
    }

    /**
     * 輪詢下標
     * @param modulo 節點總數
     * @return
     */
    private static int select(int modulo) {
        for (; ; ) {
            int current = NEXT_SERVER_COUNTER.get();
            //NEXT_SERVER_COUNTER + 1 % 節點總數
            int next = (current + 1) % modulo;
            //如果當前NEXT_SERVER_COUNTER爲current,CAS更新爲next
            boolean compareAndSet = NEXT_SERVER_COUNTER.compareAndSet(current, next);
            //CAS更新成功直接返回,否則自旋到當前線程CAS操作成功
            if (compareAndSet) {
                return next;
            }
        }
    }

    /**
     * 選舉節點
     * @param serverList 節點個數
     * @return
     */
    public static Server selectServer(List<Server> serverList) {
        return serverList.get(select(serverList.size()));
    }

    public static void main(String[] args) {
        List<Server> serverList = new ArrayList<>();
        serverList.add(new Server(1, "服務器1"));
        serverList.add(new Server(2, "服務器2"));
        serverList.add(new Server(3, "服務器3"));
        for (int i = 0; i < 10; i++) {
            Server selectedServer = selectServer(serverList);
            System.out.format("第%d次請求,選擇服務器%s\n", i + 1, selectedServer.toString());
        }
    }
}


加權輪詢(WeightedRound-Robin、WRR)負載均衡算法的實現

根據設定的權重值來分配訪問請求,權重值越大的,被分到的請求數也就越多。一般根據每臺節點服務器的具體性能來分配權重。

服務器 權重
s1 1
s2 2
s3 3

​ 可以根據權重我們創建數組{s3,s2,s1,s3,s2,s3},然後再按照輪詢的方式選擇相應的服務器。

public class WeightedRoundRobinSimple {
    //當前下標
    private static Integer index = 0;
    //節點以及對應權值
    private static Map<String, Integer> mapNodes = new HashMap<>();
    //節點的權值列表
    private static List<String> nodes = new ArrayList<>();

    // 準備模擬數據
    static {
        mapNodes.put("192.168.1.101", 1);
        mapNodes.put("192.168.1.102", 3);
        mapNodes.put("192.168.1.103", 2);

        // 關鍵代碼:類似於二維數組 降維成 一維數組,然後使用普通輪詢
        for (Map.Entry<String, Integer> entry : mapNodes.entrySet()) {
            String key = entry.getKey();
            for (int i = 0; i < entry.getValue(); i++) {
                nodes.add(key);
            }
        }
        System.out.println("簡單版的加權輪詢:" + JSON.toJSONString(nodes));//打印所有節點
    }


    public String selectNode() {
        String ip = null;
        synchronized (index) {
            //如果當前下標 >= 節點數,將下標復位
            if (index >= nodes.size()) {
                index = 0;
            }

            //獲取當前下標節點
            ip = nodes.get(index);

            //當前下標自增
            index++;
        }
        return ip;
    }


    // 併發測試:兩個線程循環獲取節點
    public static void main(String[] args) {
        WeightedRoundRobinSimple r = new WeightedRoundRobinSimple();

        new Thread(() -> {
            for (int i = 1; i <= 6; i++) {
                String serverIp = r.selectNode();
                System.out.println(Thread.currentThread().getName() + "==第" + i + "次獲取節點:" + serverIp);
            }
        }).start();

        new Thread(() -> {
            for (int i = 1; i <= 6; i++) {
                String serverIp = r.selectNode();
                System.out.println(Thread.currentThread().getName() + "==第" + i + "次獲取節點:" + serverIp);
            }
        }).start();
    }
}


SpringCloud 整合LoadBalancer 負載均衡

SpringCloud 微服務之間 RPC+負載均衡,常用的技術組件,大致如下:

SpringCloud 新版淘汰了 Ribbon,在 OpenFeign 中整合 LoadBalancer 負載均衡

首先回顧一下老版本的 Ribbon負載均衡

Ribbon負載均衡組件

Ribbon是Netflix開發的一個用於客戶端負載均衡的工具,主要用於在微服務架構中調用其他服務的客戶端。它具有以下特點和功能:

  1. 負載均衡:Ribbon可以將請求平均地分配給多個後端服務實例,以實現負載均衡,提高系統的性能和可靠性。
  2. 容錯機制:當某個服務實例發生故障或不可用時,Ribbon能夠自動將請求轉發給其他健康的實例,提供容錯能力。
  3. 自定義規則:Ribbon提供了豐富的負載均衡策略,用戶可以根據實際需求選擇合適的負載均衡規則,或者自定義自己的規則。
  4. 集成性:Ribbon可以與其他Netflix開發的組件(如Eureka、Hystrix等)無縫集成,提供更全面的服務治理和容錯能力。
  5. 動態性:Ribbon支持動態刷新負載均衡規則和服務列表,能夠隨着系統的變化動態調整負載均衡策略,適應不同的場景和需求。

總的來說,Ribbon是一個強大而靈活的客戶端負載均衡工具,可以幫助開發人員構建高性能、可靠的分佈式系統。

Ribbon重要接口

接口 作用 默認值
IClientConfig 讀取配置 DefaultclientConfigImpl
IRule 負載均衡規則,選擇實例 ZoneAvoidanceRule
IPing 篩選掉ping不通的實例 默認採用DummyPing實現,該檢查策略是一個特殊的實現,
實際上它並不會檢查實例是否可用,而是始終返回true,默認認爲所
有服務實例都是可用的.
ServerList 交給Ribbon的實例列表 Ribbon: ConfigurationBasedServerList
Spring Cloud Alibaba: NacosServerList
ServerListFilter 過濾掉不符合條件的實例 ZonePreferenceServerListFilter
ILoadBalancer Ribbon的入口 ZoneAwareLoadBalancer
ServerListUpdater 更新交給Ribbon的List的策略 PollingServerListUpdater

Ribbon負載均衡規則

規則名稱 特點
RandomRule 隨機選擇一個Server
RetryRule 對選定的負責均衡策略機上充值機制,在一個配置時間段內當選擇Server不成功,則一直嘗試使用subRule的方式選擇一個可用的Server
RoundRobinRule 輪詢選擇,輪詢index,選擇index對應位置Server
WeightedResponseTimeRule 根據相應時間加權,相應時間越長,權重越小,被選中的可能性越低
ZoneAvoidanceRule (默認是這個)該策略能夠在多區域環境下選出最佳區域的實例進行訪問。在沒有Zone的環境下,類似於輪詢(RoundRobinRule)

LoadBalancer 負載均衡組件

LoadBalancer 的優勢主要是,支持響應式編程的方式異步訪問客戶端,依賴 Spring Web Flux 實現客戶端負載均衡調用。

在 2020 年以前的 SpringCloud 採用 Ribbon作爲負載均衡,但是 2020 年之後,SpringCloud 把 Ribbon 移除了,而是使用自己編寫的 LoadBalancer 替代.

因此,在 2020 年以後的的 SpringCloud 版本,如果在沒有加入 LoadBalancer 依賴,使用 RestTemplate 或 OpenFeign 遠程調用,就會報以下錯誤:

img

這就是在告訴你 LoadBalancing 是未定義的,並且問你是不是忘記加入 spring-cloud-starter-loadbalancer 依賴。

OpenFeign + LoadBalancer所需依賴

在需要進行遠程調用的服務中引入openfeign 和 loadbalancer 依賴


        <!--移除ribbon依賴,增加loadBalance依賴  , 添加spring-cloud的依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

maven依賴要排除 ribbon

排除被 其他包 隱形 引入的 ribbon,比如被 nacos client 帶入的 ribbon

OpenFeign + LoadBalancer所需配置

配置文件 禁用 ribbon

OpenFeign + LoadBalancer所需註解

@LoadBalancerClients 是 Spring Cloud 提供的一個註解,用於配置全局性的負載均衡器屬性。比如配置 自定義的負載均衡機制。

@LoadBalanced 是 Spring Cloud 提供的一個註解,用於標記 RestTemplate 或 WebClient 的 Bean,以啓用負載均衡功能。

當一個 RestTemplate 或 WebClient 被標記爲 @LoadBalanced 後,Spring Cloud 將會爲其創建一個代理對象,並在發起 HTTP 請求時,自動添加負載均衡的能力。當發起 RPC 請求時,實際上是由負載均衡器選擇一個目標服務實例,並將請求發送到該實例上。

在使用 RestTemplate 發起 HTTP RPC 請求時,如果 RestTemplate 被標記爲 @LoadBalanced,則可以直接使用服務名作爲 URL,而不需要指定具體的 IP 地址和端口號。Spring Cloud 會根據服務名解析出可用的服務實例,並通過負載均衡器選擇其中一個來處理請求。

OpenFeign + LoadBalancer 的演示

啓動 provider 服務端 , 具體請參見《尼恩Java面試寶典》配套視頻:

啓動 consumer 客戶端的微服務 , 具體請參見《尼恩Java面試寶典》配套視頻:

這個案例的展示界面如下, 具體請參見《尼恩Java面試寶典》配套視頻:

執行請求之後,看斷點

命中了 LoadBalancer 的斷點

LoadBalancer自定義負載均衡策略

LoadBalancer默認提供了1種負載均衡策略:

  • (默認) RoundRobinLoadBalancer - 輪詢分配策略

下面是來自源碼的結構

LoadBalancer基於Nacos權重自定義負載算法

ReactorLoadBalancer接口,實現自定義負載算法需要實現該接口,並實現choose邏輯,選取對應的節點

public interface ReactorLoadBalancer<T> extends ReactiveLoadBalancer<T> {
    Mono<Response<T>> choose(Request request);

    default Mono<Response<T>> choose() {
        return this.choose(REQUEST);
    }
}

通過nacos配置 權重

nacos可以配置不同實例的權重信息,可以在

  1. yaml中配置spirng.cloud.nacos.discovery.weight 數值範圍從1-100 ,默認爲1
  2. 可以在nacos面板找到該實例信息,並實時配置實例的權重

在這裏插入圖片描述

編輯權重

在這裏插入圖片描述

基於nacos權重實現自定義負載

權重:數值越高,代表被選取的概率越大.

根據RoundRobin源碼,自定義NacosWeightLoadBalancer


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.*;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 基於nacos權重的負載均衡
*/
public class NacosWeightLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private static final Log log = LogFactory.getLog(NacosWeightLoadBalancer.class);
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    //nacos權重獲取名稱,在nacos元數據中
    private static final String NACOS_WEIGHT_NAME = "nacos.weight";

    public NacosWeightLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.processInstanceResponse(supplier, serviceInstances);
        });
    }


    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback)supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
        }

        return serviceInstanceResponse;
    }


    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }
        } else {
            //根據權重選擇實例,權重高的被選中的概率大
            //nacos.weight的值越大,被選中的概率越大
            Double totalWeight = 0D;
            for (ServiceInstance instance : instances) {
                String s = instance.getMetadata().get(NACOS_WEIGHT_NAME);
                double weight = Double.parseDouble(s);
                totalWeight = totalWeight + weight;
                //放置當前實例的權重區間
                instance.getMetadata().put("weight",String.valueOf(totalWeight));
            }
            //隨機獲取一個區間類的數值,nacos權重越大,區間越大,則隨機數值落到相應的區間的概率是由區間的大小來決定的。
            double index = ThreadLocalRandom.current().nextDouble(totalWeight);
            //根據權重區間選擇實例
            for (ServiceInstance instance : instances) {
                double weight = Double.parseDouble(instance.getMetadata().get("weight"));
                if (index <= weight) {
                    return new DefaultResponse(instance);
                }
            }

        }
        return new EmptyResponse();
    }
}


配置使用自定義負載均衡器

增加WeightLoadBalanceConfiguration

public class WeightLoadBalanceConfiguration {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> weightLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new NacosWeightLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

修改主類配置,使用NacosWeightLoadBalancer 負載均衡

@LoadBalancerClients({
        @LoadBalancerClient(name = "loadbalance-provider-service", configuration = WeightLoadBalanceConfiguration.class)
})

說在最後:有問題找老架構取經

如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。

最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。

在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典PDF》,裏邊有大量的大廠真題、面試難題、架構難題。很多小夥伴刷完後, 吊打面試官, 大廠橫着走。

在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。

另外,如果沒有面試機會,可以找尼恩來改簡歷、做幫扶。

遇到職業難題,找老架構取經, 可以省去太多的折騰,省去太多的彎路。

尼恩指導了大量的小夥伴上岸,前段時間,剛指導一個40歲+被裁小夥伴,拿到了一個年薪100W的offer。

狠狠卷,實現 “offer自由” 很容易的, 前段時間一個武漢的跟着尼恩捲了2年的小夥伴, 在極度嚴寒/痛苦被裁的環境下, offer拿到手軟, 實現真正的 “offer自由” 。

另外,尼恩也給一線企業提供 《DDD 的架構落地》企業內部培訓,目前給不少企業做過內部的諮詢和培訓,效果非常好。

在這裏插入圖片描述

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章