【得物技術】常用註冊中心原理及比較

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前比較常用的註冊中心有Eureka、Zookeeper、Consul和Nacos。最近對這四種註冊中心的整體框架和實現進行了學習,並主要針對Nacos從源碼角度學習了服務註冊和訂閱的具體實現。最後比較了這四種註冊中心的區別。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"一.Eureka","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/52/5222daa10033da2c5f0ac738f6a123d6.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"左上角的Eureka Client是服務提供者:向Eureka Server註冊和更新自己的信息,同時能從Eureka Server註冊表中獲取到其他服務的信息。具體有以下四種操作:","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Register註冊:Client端向Server端註冊自身的元數據以供服務發現;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Renew續約:通過發送心跳到Server以維持和更新註冊表中服務實例元數據的有效性。當在一定時長內,Server沒有收到Client的心跳信息,將默認服務下線,會把服務實例的信息從註冊表中刪除;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Cancel下線:Client在關閉時主動向Server註銷服務實例元數據,這時Client的服務實例數據將從Server的註冊表中刪除;","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Get Registry獲取註冊表:Client向Server請求註冊表信息,用於服務發現,從而發起服務間遠程調用。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Eureka Server服務註冊中心:提供服務註冊和發現的功能。每個Eureka Client向Eureka Server註冊自己的信息,也可以通過Eureka Server獲取到其他服務的信息達到發現和調用其他服務的目的。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Eureka Client服務消費者:通過Eureka Server獲取註冊到其上其他服務的信息,從而根據信息找到所需的服務發起遠程調用。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Replicate同步複製:Eureka Server之間註冊表信息的同步複製,使Eureka Server集羣中不同註冊表中服務實例信息保持一致。由於集羣間的同步複製是通過HTTP的方式進行,基於網絡的不可靠性,集羣中的Eureka Server間的註冊表信息難免存在不同步的時間節點,不滿足CAP中的C(數據一致性)。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Make Remote Call遠程調用:服務客戶端之間的遠程調用。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二.Zookeeper","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.1 Zookeeper整體框架","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f8/f85c05aeb5faea7e78a90cfec43234f9.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Leader:zookeeper 集羣工作的核心,事務請求(寫操作)的唯一調度和處理者,保證集羣事務處理的順序性;集羣內部各個服務的調度者。 對於 create,set data,delete 等有寫操作的請求,則需要統一轉發給 leader 處理,leader 需要決定編號、執行操作,這個過程稱爲一個事務。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Follower:處理客戶端非事務(讀操作)請求 轉發事務請求給 Leader 參與集羣 leader。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Observer:觀察者角色是針對訪問量較大的 zookeeper 集羣新增的角色。觀察zookeeper集羣的最新狀態變化並將這些狀態同步過來,其對於非事務請求可以進行獨立處理,對於事務請求,則會轉發給Leader服務器處理。不會參與任何形式的投票只提供服務,通常用於在不影響集羣事務處理能力的前提下提升集羣的非事務處理能力,用於增加併發的請求。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2 Zookeeper存儲結構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下圖描述了用於內存表示的ZooKeeper文件系統的樹結構。ZooKeeper節點稱爲 znode。每個znode由一個名稱標識,並用路徑(/)序列分隔。在圖中,首先有一個由“/”分隔的znode。在根目錄下有兩個邏輯命名空間 config 和 workers 。config 命名空間用於集中式配置管理,workers 命名空間用於命名。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在config命名空間下,每個znode最多可存儲1MB的數據。這與UNIX文件系統相類似,除了父znode也可以存儲數據。這種結構的主要目的是存儲同步數據並描述znode的元數據。此結構稱爲 ZooKeeper數據模型。ZooKeeper命名空間中的每個節點都由路徑標識。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c6/c6fcaa758b73896b0556fb5de0ed1d01.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"znode兼具文件和目錄兩種特點。既像文件一樣維護着數據長度、元信息、ACL、時間戳等數據結構,又像目錄一樣可以作爲路徑標識的一部分:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"版本號 - 每個znode都有版本號,這意味着每當與znode相關聯的數據發生變化時,其對應的版本號也會增加。當多個zookeeper客戶端嘗試在同一znode上執行操作時,版本號的使用就很重要。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作控制列表(ACL) - ACL基本上是訪問znode的認證機制。它管理所有znode讀取和寫入操作。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時間戳 - 時間戳表示創建和修改znode所經過的時間。它通常以毫秒爲單位。ZooKeeper從“事務ID\"(zxid)標識znode的每個更改。Zxid 是唯一的,並且爲每個事務保留時間,以便你可以輕鬆地確定從一個請求到另一個請求所經過的時間。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"數據長度 - 存儲在znode中的數據總量是數據長度。最多可以存儲1MB的數據。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZooKeeper還具有短暫節點的概念。只要創建znode的會話處於活動狀態,這些znode就存在。會話結束時,將刪除znode。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.3 Zookeeper監視功能","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZooKeeper支持watch的概念,客戶端可以在znode上設置觀察。znode更改時,將觸發並刪除監視。觸發監視後,客戶端會收到一個數據包,說明znode已更改。如果客戶端和其中一個ZooKeeper服務器之間的連接斷開,則客戶端將收到本地通知。3.6.0中的新增功能:客戶端還可以在znode上設置永久性的遞歸監視,這些監視在觸發時不會刪除,並且會以遞歸方式觸發註冊znode以及所有子znode的更改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.4 Zookeeper選舉過程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/70/70f0230a38f2019e4180cc281ccf7dc6.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZooKeeper至少需要三個節點才能工作,Zookeeper節點狀態一般認爲有4個:  ","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LOOKING:表示正在進行選舉的節點,處於該狀態需要進入選舉流程","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LEADING:領導者狀態,處於該狀態的節點說明是角色已經是Leader","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"FOLLOWING:跟隨者狀態,表示Leader已經選舉出來,當前節點角色是follower","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OBSERVER:觀察者狀態,表明當前節點角色是observer,observer表示不會進入選舉,僅僅只是接受選舉結果,也就是說不會成爲Leader節點,但是是follower節點一樣提供服務。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推選Leader過程如下圖所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d8/d84b7e62b625b0a248593ba43c17b7c1.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在集羣初始化階段,當有一臺服務器 ZK1 啓動時,無法單獨進行和完成 Leader 選舉,當第二臺服務器 ZK2 啓動時,此時兩臺機器可以相互通信,每臺機器都試圖找到 Leader,於是進入 Leader 選舉過程。選舉過程開始,過程如下:  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(1) 每個Server發出一個投票。由於是初始情況,ZK1 和 ZK2 都會將自己作爲 Leader 服務器來進行投票,每次投票會包含所推舉的服務器的 ID 和 ZXID(事務ID),使用(ID, ZXID)來表示,此時ZK1的投票爲(1, 0),ZK2的投票爲(2, 0),然後各自將這個投票發給集羣中其他機器。  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(2) 接受來自各個服務器的投票。集羣的每個服務器收到投票後,首先判斷該投票的有效性,如檢查是否是本輪投票、是否來自LOOKING狀態的服務器。  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(3) 處理投票。針對每一個投票,服務器都需要將別人的投票和自己的投票進行比較,規則如下:    ","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"優先檢查 ZXID。ZXID 比較大的服務器優先作爲 Leader。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果 ZXID 相同,那麼就比較服務器 ID 。ID 較大的服務器作爲Leader服務器。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"    對於 ZK1 而言,它的投票是(1, 0),接收 ZK2 的投票爲(2, 0),首先會比較兩者的 ZXID,均爲 0,再比較 ID,此時 ZK2 的 ID更大,於是 ZK2 勝。ZK1 更新自己的投票爲(2, 0),並將投票重新發送給 ZK2。  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(4) 統計投票。每次投票後,服務器都會統計投票信息,判斷是否已經有過半機器接受到相同的投票信息,對於 ZK1、ZK2 而言,都統計出集羣中已經有兩臺機器接受了(2, 0)的投票信息,此時便認爲已經選出 ZK2 作爲Leader。  ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(5) 改變服務器狀態。一旦確定了 Leader,每個服務器就會更新自己的狀態,如果是Follower,那麼就變更爲FOLLOWING,如果是Leader,就變更爲LEADING。當新的 Zookeeper節點ZK3啓動時,發現已經有Leader了,不再選舉,直接將狀態從LOOKING 改爲FOLLOWING。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"三.Consul","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1 Consul整體框架","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/10/10ca322b9d76a77d61c7cf98bd18e3ab.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Consul 支持多數據中心,在上圖中有兩個Data Center,他們通過WAN GOSSIP在 Internet 互聯,同時爲了提高通信效率,只有 Server 節點才加入跨數據中心的通信。因此,consul是可以支持多個數據中心之間基於WAN來做同步的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在單個數據中心中,Consul 分爲 Client 和 Server 兩種節點(所有的節點也被稱爲 Agent)。","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Server節點:參與共識仲裁、存儲羣集狀態(日誌存儲)、處理查詢、維護與周邊(LAN/WAN)各節點關係","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Agent節點:負責通過該節點註冊到consul的微服務的健康檢查、將客戶端註冊請求以及查詢轉化爲對server的RPC請求、維護與周邊(LAN/WAN)各節點關係","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它們之間通過GRPC通信。除此之外,Server和Client之間,還有一條LAN GOSSIP通信,這是用於當LAN內部發生了拓撲變化時,存活的節點們能夠及時感知,比如Server節點down掉後,Client就會觸發將對應Server節點從可用列表中剝離出去。所有的Server節點共同組成了一個集羣,他們之間運行raft協議,通過共識仲裁選舉出leader。所有的業務數據都通過leader寫入到集羣中做持久化,當有半數以上的節點存儲了該數據後,server集羣纔會返回ACK,從而保障了數據的強一致性。當然,Server數量大了之後,也會影響寫數據的效率。所有的follower會跟隨leader的腳步,保障其有最新的數據副本。集羣內的 Consul 節點通過 gossip 協議維護成員關係,如集羣內現在還有哪些節點,這些節點是 Client 還是 Server。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單個數據中心的流言協議同時使用 TCP 和 UDP 通信,並且都使用 8301 端口。跨數據中心的流言協議也同時使用 TCP 和 UDP 通信,端口使用 8302。集羣內數據的讀寫請求既可以直接發到 Server,也可以通過 Client 使用 RPC 轉發到 Server,請求最終會到達 Leader 節點。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"四.Nacos","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"4.1 Nacos整體框架","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/15/15590e00daf7a67331020fcbc7b43f97.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務註冊時在服務端本地會通過輪詢註冊中心集羣節點地址進行服務得註冊,在註冊中心上,即Nacos Server上採用了Map保存實例信息,配置了持久化的服務會被保存到數據庫中,在服務的調用方,爲了保證本地服務實例列表的動態感知,Nacos與其他註冊中心不同的是,採用了 Pull/Push同時運作的方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"4.2 Nacos選舉","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos的集羣類似於zookeeper, 它分爲leader角色和follower角色, 那麼從這個角色的名字可以看出來,這個集羣存在選舉的機制。因爲如果自己不具備選舉功能,角色的命名可能就是master/slave了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"選舉算法 :","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"  Nacos集羣採用 raft 算法來實現,它是相對zookeeper的選舉算法較爲簡單的一種。選舉算法的核心在 RaftCore 中,包括數據的處理和數據同步。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"  在Raft中,節點有三種角色:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Leader:負責接收客戶端的請求","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Candidate:用於選舉Leader的一種角色(競選狀態)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Follower:負責響應來自Leader或者Candidate的請求","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"  所有節點啓動的時候,都是follower狀態。 如果在一段時間內如果沒有收到leader的心跳(可能是沒有leader,也可能是leader掛了),那麼follower會變成Candidate。然後發起選舉,選舉之前,會增加term,這個term和zookeeper 中的 epoch 的道理是一樣的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"  follower會投自己一票,並且給其他節點發送票據信息,等到其他節點回覆在這個過程中,可能出現幾種情況:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"收到過半的票數通過,則成爲leader","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"被告知其他節點已經成爲leader,則自己切換爲follower","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一段時間內沒有收到過半的投票,則重新發起選舉。約束條件在任一term中,單個節點最多隻能投一票","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一種情況,贏得選舉之後,leader會給所有節點發送消息,避免其他節點觸發新的選舉。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二種情況,比如有三個節點A B C。A B同時發起選舉,而A的選舉消息先到達C,C給A投了一票,當B的消息到達C時,已經不能滿足上面提到的約束條件,即C不會給B投票,而A和B顯然都不會給對方投票。A勝出之後,會給B,C發心跳消息,節點B發現節點A的term不低於自己的term,知道有已經有Leader了,於是轉換成follower。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三種情況,沒有任何節點獲得大多數投票,可能是平票的情況。加入總共有四個節點(A/B/C/D),Node C、Node D同時成爲了candidate,但Node A投了Node D一票,Node B投了Node C一票,這就出現了平票 的情況。這個時候大家都在等待,直到超時後重新發起選舉。如果出現平票的情況,那麼就延長了系統不可用的時間,因此raft引入了randomizedelection timeouts來儘量避免平票情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"4.3 Nacos服務註冊流程源碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos源碼是在","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/alibaba/nacos","title":null,"type":null},"content":[{"type":"text","text":"https://github.com/alibaba/nacos","attrs":{}}]},{"type":"text","text":"下載的最新版本2.0.0-bugfix (Mar 30th, 2021)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當需要註冊時,Spring-Cloud會注入實例NacosServiceRegistry。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@Override\n public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {\n NamingUtils.checkInstanceIsLegal(instance);\n String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);\n //添加心跳信息\n if (instance.isEphemeral()) {\n BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance);\n beatReactor.addBeatInfo(groupedServiceName, beatInfo);\n }\n //調用服務代理類進行註冊\n serverProxy.registerService(groupedServiceName, groupName, instance);\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後調用 registerService方法進行註冊,構建請求參數,發起請求。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {\n\n NAMING_LOGGER.info(\"[REGISTER-SERVICE] {} registering service {} with instance: {}\", namespaceId, serviceName,\n instance);\n\n final Map params = new HashMap(16);\n params.put(CommonParams.NAMESPACE_ID, namespaceId);\n params.put(CommonParams.SERVICE_NAME, serviceName);\n params.put(CommonParams.GROUP_NAME, groupName);\n params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());\n params.put(\"ip\", instance.getIp());\n params.put(\"port\", String.valueOf(instance.getPort()));\n params.put(\"weight\", String.valueOf(instance.getWeight()));\n params.put(\"enable\", String.valueOf(instance.isEnabled()));\n params.put(\"healthy\", String.valueOf(instance.isHealthy()));\n params.put(\"ephemeral\", String.valueOf(instance.isEphemeral()));\n params.put(\"metadata\", JacksonUtils.toJson(instance.getMetadata()));\n\n reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);\n\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進入reqApi方法,我們可以看到服務在進行註冊的時候會輪詢配置好的註冊中心的地址:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public String reqApi(String api, Map params, Map body, List servers,\n String method) throws NacosException {\n\n params.put(CommonParams.NAMESPACE_ID, getNamespaceId());\n\n if (CollectionUtils.isEmpty(servers) && StringUtils.isBlank(nacosDomain)) {\n throw new NacosException(NacosException.INVALID_PARAM, \"no server available\");\n }\n\n NacosException exception = new NacosException();\n //service只有一個的情況\n if (StringUtils.isNotBlank(nacosDomain)) {\n for (int i = 0; i < maxRetry; i++) {\n try {\n return callServer(api, params, body, nacosDomain, method);\n } catch (NacosException e) {\n exception = e;\n if (NAMING_LOGGER.isDebugEnabled()) {\n NAMING_LOGGER.debug(\"request {} failed.\", nacosDomain, e);\n }\n }\n }\n } else {\n Random random = new Random(System.currentTimeMillis());\n int index = random.nextInt(servers.size());\n\n for (int i = 0; i < servers.size(); i++) {\n String server = servers.get(index);\n try {\n return callServer(api, params, body, server, method);\n } catch (NacosException e) {\n exception = e;\n if (NAMING_LOGGER.isDebugEnabled()) {\n NAMING_LOGGER.debug(\"request {} failed.\", server, e);\n }\n }\n //輪詢\n index = (index + 1) % servers.size();\n }\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後通過 callServer(api, params, server, method) 發起調用","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public String callServer(String api, Map params, Map body, String curServer,\n String method) throws NacosException {\n long start = System.currentTimeMillis();\n long end = 0;\n injectSecurityInfo(params);\n Header header = builderHeader();\n\n String url;\n //發送http請求\n if (curServer.startsWith(UtilAndComs.HTTPS) || curServer.startsWith(UtilAndComs.HTTP)) {\n url = curServer + api;\n } else {\n if (!IPUtil.containsPort(curServer)) {\n curServer = curServer + IPUtil.IP_PORT_SPLITER + serverPort;\n }\n url = NamingHttpClientManager.getInstance().getPrefix() + curServer + api;\n }\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos服務端的處理:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務端提供了一個InstanceController類,在這個類中提供了服務註冊相關的API","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"@CanDistro\n @PostMapping\n @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)\n public String register(HttpServletRequest request) throws Exception {\n\n final String namespaceId = WebUtils\n .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);\n final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);\n NamingUtils.checkServiceNameFormat(serviceName);\n // 從請求中解析出instance實例\n final Instance instance = parseInstance(request);\n\n serviceManager.registerInstance(namespaceId, serviceName, instance);\n return \"ok\";\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後調用 ServiceManager 進行服務的註冊","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {\n //創建一個空服務,在Nacos控制檯服務列表展示的服務信息,實際上是初始化一個serviceMap,它是一個ConcurrentHashMap集合\n createEmptyService(namespaceId, serviceName, instance.isEphemeral());\n //從serviceMap中,根據namespaceId和serviceName得到一個服務對象\n Service service = getService(namespaceId, serviceName);\n\n if (service == null) {\n throw new NacosException(NacosException.INVALID_PARAM,\n \"service not found, namespace: \" + namespaceId + \", service: \" + serviceName);\n }\n //調用addInstance創建一個服務實例\n addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"創建空服務實例時","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)\n throws NacosException {\n //從serviceMap中獲取服務對象\n Service service = getService(namespaceId, serviceName);\n //如果爲空。則初始化\n if (service == null) {\n Loggers.SRV_LOG.info(\"creating empty service {}:{}\", namespaceId, serviceName);\n service = new Service();\n service.setName(serviceName);\n service.setNamespaceId(namespaceId);\n service.setGroupName(NamingUtils.getGroupName(serviceName));\n // now validate the service. if failed, exception will be thrown\n service.setLastModifiedMillis(System.currentTimeMillis());\n service.recalculateChecksum();\n if (cluster != null) {\n cluster.setService(service);\n service.getClusterMap().put(cluster.getName(), cluster);\n }\n service.validate();\n\n putServiceAndInit(service);\n if (!local) {\n addOrReplaceService(service);\n }\n }\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getService 方法中用到了Map進行存儲:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private final Map> serviceMap = new ConcurrentHashMap<>();","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos是通過不同的 namespace 來維護服務的,而每個namespace下有不同的group,不同的group下才有對應的Service ,再通過這個 serviceName 來確定服務實例。第一次進來則會進入初始化,初始化完會調用 putServiceAndInit。","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private void putServiceAndInit(Service service) throws NacosException {\n //把服務信息保存到serviceMap集合\n putService(service);\n service = getService(service.getNamespaceId(), service.getName());\n //建立心跳檢測機制\n service.init();\n //實現數據一致性監聽,ephemeral(標識服務是否爲臨時服務,默認是持久化的,也就是true)=true表示採用raft協議,false表示採用Distro\n consistencyService\n .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);\n consistencyService\n .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);\n Loggers.SRV_LOG.info(\"[NEW-SERVICE] {}\", service.toJson());\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲取到服務以後把服務實例添加到集合中,然後基於一致性協議進行數據的同步。然後調用 addInstance","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips)\n throws NacosException {\n // 組裝key\n String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral);\n // 獲取剛剛組裝的服務\n Service service = getService(namespaceId, serviceName);\n\n synchronized (service) {\n List instanceList = addIpAddresses(service, ephemeral, ips);\n\n Instances instances = new Instances();\n instances.setInstanceList(instanceList);\n // 也就是上一步實現監聽的類裏添加註冊服務\n consistencyService.put(key, instances);\n }\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"4.4 Nacos服務訂閱源碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"節點的訂閱在不同的註冊中心中都有不同的實現,一般分爲拉取和推送兩種。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推送是指當訂閱的節點發生更新的時候會主動向訂閱方進行推送,ZK就是推送的實現方式,客戶端和服務端會建立一個TCP長連接,客戶端會註冊一個watcher,然後當有數據更新的時候,服務端會通過長連接進行推送。通過這種建立長連接的模式,會嚴重消耗服務端的資源,所以當watcher比較多,並且當更新頻繁的時候,Zookeeper的性能會非常低,甚至掛掉。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"拉取是指訂閱的節點主動定時獲取服務端節點的信息,然後再本地去做一個比對,如果有改變就會做一些更新。在Consul中也有一個watcher機制,但和ZK不一樣的是,他是通過Http長輪詢去實現的,Consul服務端會對請求的url中是否包含wait參數進行立即返回,還是先掛起等待指定wait時間內如果服務有變化在返回。使用該方法的性能可能較高但是實時性可能不高。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Nacos中,結合了這兩個思想,既提供了拉取又提供了主動推送。","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"拉取的部分,從 hostReactor 獲取 serviceInfo的具體操作如下:","attrs":{}}]}]}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {\n\n NAMING_LOGGER.debug(\"failover-mode: \" + failoverReactor.isFailoverSwitch());\n //拼接服務名稱+集羣名稱(默認爲空)\n String key = ServiceInfo.getKey(serviceName, clusters);\n if (failoverReactor.isFailoverSwitch()) {\n return failoverReactor.getService(key);\n }\n //從ServiceInfoMap中根據key來查找服務提供者列表,ServiceInfoMap是客戶端的服務地址的本地緩存\n ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);\n //如果爲空,表示本地緩存不存在\n if (null == serviceObj) {\n serviceObj = new ServiceInfo(serviceName, clusters);\n //如果找不到則創建一個新的然後放入serviceInfoMap,同時放入updatingMap,執行updateServiceNow,再從updatingMap移除;\n serviceInfoMap.put(serviceObj.getKey(), serviceObj);\n\n updatingMap.put(serviceName, new Object());\n // 立馬從Nacos server中去加載服務地址信息\n updateServiceNow(serviceName, clusters);\n updatingMap.remove(serviceName);\n\n } else if (updatingMap.containsKey(serviceName)) {\n //如果從serviceInfoMap找出來的serviceObj在updatingMap中則等待UPDATE_HOLD_INTERVAL\n if (UPDATE_HOLD_INTERVAL > 0) {\n // hold a moment waiting for update finish\n synchronized (serviceObj) {\n try {\n serviceObj.wait(UPDATE_HOLD_INTERVAL);\n } catch (InterruptedException e) {\n NAMING_LOGGER\n .error(\"[getServiceInfo] serviceName:\" + serviceName + \", clusters:\" + clusters, e);\n }\n }\n }\n }\n // 開啓定時調度,每10s去查詢一次服務地址\n //如果本地緩存中存在,則通過scheduleUpdateIfAbsent開啓定時任務,再從serviceInfoMap取出serviceInfo\n scheduleUpdateIfAbsent(serviceName, clusters);\n return serviceInfoMap.get(serviceObj.getKey());\n }","attrs":{}}]},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos推送功能,Nacos會記錄上面我們的訂閱者到我們的PushService","attrs":{}}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/62/62e4870d8714c9e9e9ff4854429ace1f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 PushService 類實現了 ApplicationListener 所以本身又會取監聽該事件,監聽服務狀態變更事件,然後遍歷所有的客戶端,通過udp協議進行消息的廣播通知:","attrs":{}}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void onApplicationEvent(ServiceChangeEvent event) {\n Service service = event.getService();//獲取到服務\n String serviceName = service.getName();//服務名\n String namespaceId = service.getNamespaceId();//命名空間\n //執行任務\n Future future = GlobalExecutor.scheduleUdpSender(() -> {\n try {\n Loggers.PUSH.info(serviceName + \" is changed, add it to push queue.\");\n ConcurrentMap clients = clientMap\n .get(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));\n if (MapUtils.isEmpty(clients)) {\n return;\n }\n Map cache = new HashMap<>(16);\n long lastRefTime = System.nanoTime();\n for (PushClient client : clients.values()) {\n if (client.zombie()) {\n Loggers.PUSH.debug(\"client is zombie: \" + client.toString());\n clients.remove(client.toString());\n Loggers.PUSH.debug(\"client is zombie: \" + client.toString());\n continue;\n }\n Receiver.AckEntry ackEntry;\n Loggers.PUSH.debug(\"push serviceName: {} to client: {}\", serviceName, client.toString());\n String key = getPushCacheKey(serviceName, client.getIp(), client.getAgent());\n byte[] compressData = null;\n Map data = null;\n if (switchDomain.getDefaultPushCacheMillis() >= 20000 && cache.containsKey(key)) {\n org.javatuples.Pair pair = (org.javatuples.Pair) cache.get(key);\n compressData = (byte[]) (pair.getValue0());\n data = (Map) pair.getValue1();\n Loggers.PUSH.debug(\"[PUSH-CACHE] cache hit: {}:{}\", serviceName, client.getAddrStr());\n }\n if (compressData != null) {\n ackEntry = prepareAckEntry(client, compressData, data, lastRefTime);\n } else {\n ackEntry = prepareAckEntry(client, prepareHostsData(client), lastRefTime);\n if (ackEntry != null) {\n cache.put(key, new org.javatuples.Pair<>(ackEntry.origin.getData(), ackEntry.data));\n }\n }\n Loggers.PUSH.info(\"serviceName: {} changed, schedule push for: {}, agent: {}, key: {}\",\n client.getServiceName(), client.getAddrStr(), client.getAgent(),\n (ackEntry == null ? null : ackEntry.key));\n //執行 UDP 推送\n udpPush(ackEntry);\n }\n } catch (Exception e) {\n Loggers.PUSH.error(\"[NACOS-PUSH] failed to push serviceName: {} to client, error: {}\", serviceName, e);\n\n } finally {\n futureMap.remove(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName));\n }\n\n }, 1000, TimeUnit.MILLISECONDS);\n\n futureMap.put(UtilsAndCommons.assembleFullServiceName(namespaceId, serviceName), future);\n\n }","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務消費者此時需建立一個udp服務的監聽,否則服務端無法進行數據的推送。這個監聽是在HostReactor的構造方法中初始化的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos這種推送模式,對於Zookeeper那種通過tcp長連接來說會節約很多資源,就算大量的節點更新也不會讓Nacos出現太多的性能瓶頸,在Nacos中客戶端如果接受到了udp消息會返回一個ACK,如果一定時間Nacos-Server沒有收到ACK,那麼還會進行重發,當超過一定重發時間之後,就不在重發了,雖然通過udp並不能保證能真正的送到訂閱者,但是Nacos還有定時輪訓作爲兜底,不需要擔心數據不會更新的情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Nacos通過這兩種手段,既保證了實時性,又保證了數據更新不會漏掉。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"五.四種註冊中心比較","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"四種註冊中心有着各自的特點,通過以下列表可以比較清晰地對比他們的不同點:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f5/f54c7bc265439fe4b5cdd357316d7c3a.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"

比較項

Eureka 

zookeeper 

Nacos 

Consul

集羣結構

平級 

主從

支持平級和主從

主從

集羣角色

主人

Leader、followerobserver

leader、follower、candidate

server-leader、server以及client

及時感知服務變化

不能及時知道

會及時知道

不能及時知道

不能及時知道

一致性協議(CAP)

注重可用性(AP)

注重一致性(CP)

支持CP和AP

注重一致性(CP)

雪崩保護

沒有

沒有

負載均衡策略

使用ribbon實現

一般可以直接採用RPC的負載均衡

權重/metadata/Selector

Fabio

權限控制

使用ACL實現節點權限控制

RBAC-用戶、角色、權限

ACL

健康檢查

Client Beat

Keep Alive

TCP/HTTP/MYSQL/Client Beat

TCP/HTTP/gRPC/Cmd

自動註銷實例

支持

支持

支持

不支持

訪問協議

HTTP

TCP

HTTP/DNS

HTTP/DNS

配置中心

多數據中心

不支持

不支持

不支持

支持

跨註冊中心同步

不支持

不支持

支持

支持

"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文/hz","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關注得物技術,攜手走向技術的雲端","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章