1. 背景
對于zookeeper,大家都比較熟悉,在Kafka、HBase、Dubbo中都有看到過他的身影,那zookeeper到底是什么,都能做些什么呢? 今天我們一起來了解下。
2. ZooKeeper簡介
2.1 概述
簡單理解,ZooKeeper 是分布式應(yīng)用程序的分布式開源協(xié)調(diào)服務(wù),封裝了分布式架構(gòu)中核心和主流的需求和功能,并提供一系列簡單易用的接口提供給用戶使用。 分布式應(yīng)用程序可以根據(jù)zookeeper實現(xiàn)分布式鎖、分布式集群的集中式元數(shù)據(jù)存儲、Master選舉、分布式協(xié)調(diào)和通知等。下圖是官網(wǎng)上zookeeper的架構(gòu)簡圖:
從zk的架構(gòu)圖中也可以了解到,zk本身就是一個分布式的、高可用的系統(tǒng)。有多個server節(jié)點,其中有一個是leader節(jié)點,其他的是follower節(jié)點,他們會從leader節(jié)點中復制數(shù)據(jù)。客戶端節(jié)點連接單個zk服務(wù)器,并維護一個TCP連接,通過該連接發(fā)送請求、獲取響應(yīng)以及監(jiān)聽事件并發(fā)送心跳,如果與服務(wù)器的連接中斷,就會連接到另外的服務(wù)器。
2.2 技術(shù)特點
zookeeper有以下幾個特點:
- 集群部署:一般是3~5臺機器組成一個集群,每臺機器都在內(nèi)存保存了zk的全部數(shù)據(jù),機器之間互相通信同步數(shù)據(jù),客戶端連接任何一臺機器都可以。
- 順序一致性:所有的寫請求都是有序的;集群中只有l(wèi)eader機器可以寫,所有機器都可以讀,所有寫請求都會分配一個zk集群全局的唯一遞增編號:zxid,用來保證各種客戶端發(fā)起的寫請求都是有順序的。
- 原子性:要么全部機器成功,要么全部機器都不成功。
- 數(shù)據(jù)一致性:無論客戶端連接到哪臺節(jié)點,讀取到的數(shù)據(jù)都是一致的;leader收到了寫請求之后都會同步給其他機器,保證數(shù)據(jù)的強一致,你連接到任何一臺zk機器看到的數(shù)據(jù)都是一致的。
- 高可用:如果某臺機器宕機,會保證數(shù)據(jù)不丟失。集群中掛掉不超過一半的機器,都能保證集群可用。比如3臺機器可以掛1臺,5臺機器可以掛2臺。
- 實時性:一旦數(shù)據(jù)發(fā)生變更,其他節(jié)點會實時感知到。
- 高性能:每臺zk機器都在內(nèi)存維護數(shù)據(jù),所以zk集群絕對是高并發(fā)高性能的,如果將zk部署在高配置物理機上,一個3臺機器的zk集群抗下每秒幾萬請求是沒有問題的。
- 高并發(fā):高性能決定的,主要是基于純內(nèi)存數(shù)據(jù)結(jié)構(gòu)來處理,并發(fā)能力是很高的,只有一臺機器進行寫,但是高配置的物理機,比如16核32G,可以支撐幾萬的寫入QPS。所有機器都可以讀,選用3臺高配機器的話,可以支撐十萬+的QPS。可參考官網(wǎng)「基于3.2版本做的性能壓測【1】」
2.3 技術(shù)本質(zhì)
zookeeper的技術(shù)本質(zhì)決定了,一些開源項目為什么要使用zk來實現(xiàn)自己系統(tǒng)的高可用。zk是基于zab協(xié)議來實現(xiàn)的分布式一致性的,他的核心就是圖中的Atomic Broadcast,通過原子廣播的能力,保持所有服務(wù)器的同步,來實現(xiàn)了分布式一致性。
zookeeper算法即zab算法或者叫zab協(xié)議,需注意zab并不是paxos,zab的核心是為了實現(xiàn)primary-back systems這種架構(gòu)模式,而paxos實際上是叫 state machine replication,就是狀態(tài)復制機。這里簡單對比下。
- primary-back systems:leader節(jié)點接受寫請求,先寫到自己的本地機器,然后通過原子協(xié)議或其他的方式,將結(jié)果復制到其他的系統(tǒng)。
- state machine replication:沒有一個明顯的leader節(jié)點。寫的時候,把接收到的命令記錄下來,然后把命令復制給各個節(jié)點,然后各個節(jié)點再去執(zhí)行命令,應(yīng)用到自己的狀態(tài)機里面。
關(guān)于一致性算法這里不做具體對比,后續(xù)會詳細說下常見的分布式集群算法。
2.3.1 ZAB工作流程
ZAB協(xié)議,即ZooKeeper Atomic Broadcas。 集群啟動自動選舉一個Leader出來,只有Leader是可以寫的,F(xiàn)ollower是只能同步數(shù)據(jù)和提供數(shù)據(jù)的讀取,Leader掛了,F(xiàn)ollower可以繼續(xù)選舉出來Leader。這里可以看下zab的工作流程:
Leader收到寫請求,就把請求廣播給所有的follower,每一個消息廣播的時候,都是按照2PC思想,先是發(fā)起事務(wù)Proposal的廣播,就是事務(wù)提議,在發(fā)起一個事務(wù)proposal之前,leader會分配一個全局唯一遞增的事務(wù)id,zxid,通過這個可以嚴格保證順序。每個follower收到一個事務(wù)proposal之后,就立即寫入本地磁盤日志中,寫入成功之后返回一個ack給leader,然后過半的follower都返回了ack之后,leader會推送commit消息給全部follower,Leader自己也會進行commit操作,commit之后,數(shù)據(jù)才會寫入znode,然后這個數(shù)據(jù)可以被讀取到了。
如果突然leader宕機了,會進入恢復模式,重新選舉一個leader,只要過半的機器都承認你是leader,就可以選舉出來一個新的leader,所以zookeeper很重要的一點是宕機的機器數(shù)量小于一半,他就可以正常工作。這里不具體展開詳細的選舉和消息丟棄的邏輯。
從上述的工作過程,可以看出zookeeper并不是強制性的,leader并不是保證一條數(shù)據(jù)被全部follower都commit了才會讓客戶端讀到,過程中可能會在不同的follower上讀取到不一致的數(shù)據(jù),但是最終所有節(jié)點都commit完成后會一致性的。zk官方給自己的定義是:順序一致性。
從上述的這些技術(shù)特點上,我們也可以看出,為什么zookeeper只能是小集群部署,而且是適合讀多寫少的場景。想象一下,如果集群中有幾十個節(jié)點,其中1臺是leader,每次寫請求,都要跟所有節(jié)點同步,并等過半的機器ack之后才能提交成功,這個性能肯定是很差的。舉個例子,如果使用zookeeper做注冊中心,大量的服務(wù)上線、注冊、心跳的壓力,如果達到了每秒幾萬甚至上十萬的場景,zookeeper的單節(jié)點寫入是扛不住那么大壓力的。如果這是kafka或者其他中間件共用了一個zookeeper集群,那基本就都癱瘓了。
2.3.2 ZooKeeper角色
上述 的場景中已經(jīng)提到了leader和follower,既然提到了性能問題,就額外說下,除了leader和follower,zookeeper還有一個角色是:observer。
observer節(jié)點是不參與leader選舉的;他也不參與zab協(xié)議同步時,過半follower ack的環(huán)節(jié)。他只是單存的接收數(shù)據(jù),同步數(shù)據(jù),提供讀服務(wù)。這樣zookeeper集群,可以通過擴展observer節(jié)點,線性提升高并發(fā)查詢的能力 。
2.4 Znode數(shù)據(jù)模型
如 果想使用好zk的話,必須要了解下他的數(shù)據(jù)模型。雖然zookeeper的實現(xiàn)比較復雜,但是數(shù)據(jù)結(jié)構(gòu)還是比較簡單的。如下圖所示,zookeeper的數(shù)據(jù)結(jié)構(gòu)是一個類型文件系統(tǒng)的樹形目錄分層結(jié)構(gòu),它是純內(nèi)存保存的。基于這個目錄結(jié)構(gòu),可以根據(jù)自己的需要,設(shè)計的具體的概念含義。ZooKeeper 的層次模型稱作 Data Tree,Data Tree 的每個節(jié)點叫作 znode。如下圖所示:
ZNode在我們應(yīng)用中是主要訪問的實體,有些特點這里提出來,說一下:
- 【W(wǎng)atches】 監(jiān)聽
zookeeper支持 watch 的概念。客戶端可以在 znode 上設(shè)置監(jiān)聽,當 znode 發(fā)生變化時,watch 將被觸發(fā)并移除。當 watch 被觸發(fā)時,客戶端會收到一個數(shù)據(jù)包,說明 znode 已更改,這個在分布式系統(tǒng)的協(xié)調(diào)中是很有用的一個功能。
- 【Data Access】 數(shù)據(jù)訪問
存儲在命名空間中每個 znode 的數(shù)據(jù)的讀取和寫入都是原子的。讀取操作獲取與 znode 關(guān)聯(lián)的所有數(shù)據(jù),寫入操作會替換所有數(shù)據(jù)。每個節(jié)點都有一個訪問控制列表 (ACL),用于限制誰可以做什么。
- 【Ephemeral Nodes】臨時節(jié)點
ZooKeeper 也有臨時節(jié)點的概念。只要創(chuàng)建 znode 的會話處于活動狀態(tài),這些 znode 就存在。當會話結(jié)束時,znode 被刪除。臨時 znode 不允許有子節(jié)點。
- 持久節(jié)點
zooKeeper除了臨時節(jié)點還有持久節(jié)點,客戶端斷開連接,或者集群宕機,節(jié)點也會一直存在。
- 【Sequence Nodes】 順序節(jié)點
創(chuàng)建 znode 時,可以 在path路徑末尾附加一個單調(diào)遞增的計數(shù)器。這個計數(shù)器對于父 znode 是唯一的,是全局遞增的序號。
3. ZooKeeper的應(yīng)用
對zookeeper有了一定的了解之后,我們看下他有哪些應(yīng)用場景。前面的背景中也提到過,在一些常見的開源項目中,都看到過zk的身影,那zk在這些開源項目中是如何使用的呢?
3.1 典型的應(yīng)用場景
(1)元數(shù)據(jù)管理:Kafka、Canal,本身都是分布式架構(gòu),分布式集群在運行,本身他需要一個地方集中式的存儲和管理分布式集群的核心元數(shù)據(jù),所以他們都選擇把核心元數(shù)據(jù)放在zookeeper中。
Dubbo:使用zookeeper作為注冊中心、分布式集群的集中式元數(shù)據(jù)存儲
HBase:使用zookeeper做分布式集群的集中式元數(shù)據(jù)存儲
(2)分布式協(xié)調(diào):如果有節(jié)點對zookeeper中的數(shù)據(jù)做了變更,然后zookeeper會反過來去通知其他監(jiān)聽這個數(shù)據(jù)的節(jié)點,告訴它這個數(shù)據(jù)變更了。
kafka: 通過zookeeper解決controller的競爭問題。kafka有多個broker,多個broker會競爭成為一個controller的角色。如果作為controller的broker掛掉了,此時他在zk里注冊的一個節(jié)點會消失,其他broker瞬間會被zookeeper反向通知這個事情,繼續(xù)競爭成為新的controller,這個就是非常經(jīng)典的一個分布式協(xié)調(diào)的場景。
(3)Master選舉 -> HA架構(gòu)
Canal:通過zookeeper解決Master選舉問題,來實現(xiàn)HA架構(gòu)
HDFS:Master選舉實現(xiàn)HA架構(gòu),NameNode HA架構(gòu),部署主備兩個NameNode,只有一個人可以通過zookeeper選舉成為Master,另外一個作為backup。
(4)分布式鎖
一般用于分布式的業(yè)務(wù)系統(tǒng)中,控制并發(fā)寫操作。不過實際使用中,使用zookeeper做分布式鎖的案例比較少,大部分都是使用的redis.
3.2 開源產(chǎn)品使用zk的情況
下圖是有關(guān)Paxos Systems Demystified的論文中,常見的開源產(chǎn)品使用的一致性算法系統(tǒng),可以看到除了google系的產(chǎn)品用paxos算法或他們自己的chubby服務(wù)外,開源的系統(tǒng)大部分都使用zookeeper來實現(xiàn)高可用的。
4. 實踐-通過ZooKeeper來實現(xiàn)高可用
了解了zookeeper的一些技術(shù)特性及常見的使用場景后,考慮一個問題:我們平時在工作中,大部分是使用一些開源的成熟的產(chǎn)品,如果出現(xiàn)部分產(chǎn)品是不能滿足我們的業(yè)務(wù)需求時,我們是需要根據(jù)自己的業(yè)務(wù)場景去改造或重新設(shè)計的,但一般不會從頭開始,也就是我們說的不會重復造輪子。
自
己實現(xiàn)一個產(chǎn)品時,一般都是分布式集群部署的,那就要考慮,是否需要一個地方集中式存儲分布式集群的元數(shù)據(jù)?
是否需要進行Master選舉實現(xiàn)HA架構(gòu)?是否需要進行分布式協(xié)調(diào)通知?如果有這些需求,zookeeper作為一個久經(jīng)考驗的產(chǎn)品,是可以考慮直接拿來使用的,這大大提高我們的效率以及提升產(chǎn)品的穩(wěn)定性。
基于以上對zookeeper技術(shù)特點的了解,如果使用zookeeper來實現(xiàn)系統(tǒng)的高可用時,一般需要考慮4個問題,或者理解為4個步驟:
- 如何設(shè)計znode的path
- znode的類型如何選擇?比如是臨時節(jié)點,還是順序節(jié)點?
- znode中存儲什么數(shù)據(jù)?如何表達自己的業(yè)務(wù)含義
- 如何設(shè)計watch,客戶端需要關(guān)注什么事件,事件發(fā)生后需要如何處理?
下面我們通過兩個案例,看下zookeeper是如何實現(xiàn)主備切換、集群選舉的。
4.1 主備切換
主備切換在我們?nèi)粘S玫降姆植际较到y(tǒng)很常見,那我們自己如何通過zookeeper的接口來實現(xiàn)呢?
簡單回顧下主備切換架構(gòu):
主備架構(gòu)的工作流程:
正常階段的話,業(yè)務(wù)數(shù)據(jù)讀寫在主機,數(shù)據(jù)會復制到備機。當主機故障了,數(shù)據(jù)沒辦法復制到備機,原來的備機自動升級為主機,業(yè)務(wù)請求到新的主機。原來的主機恢復后,成為新的備機,將數(shù)據(jù)從新的主機同步到備機上
4.1.1 實現(xiàn)方式
(1)設(shè)計 Path
由于只有2個角色,因此直接設(shè)置兩個 znode 即可,master和slave,樣例:
-
- /com/dewu/order/operate/master
- /com/dewu/order/operate/slave。
(2)選擇節(jié)點類型
當 master 節(jié)點掛掉的時候,原來的 slave 升級為 master 節(jié)點,因此用 ephemeral 類型的 znode。
因為當一個節(jié)點成為master時,需要zk創(chuàng)建master節(jié)點,一旦這臺主機掛掉了,它到zk的連接就斷掉了,這個master節(jié)點會在超時之后,被zk自動刪除,這樣的話,就知道原來的主機宕機了,所以選擇使用ephemeral類型的節(jié)點。
(3)設(shè)計 節(jié)點數(shù)據(jù)
由于 slave 成為 master 后,會成為新的復制源,可能出現(xiàn)數(shù)據(jù)沖突,因此 slave 成為 master 后,節(jié)點需要寫入成為 master 的時間,這樣方便修復沖突數(shù)據(jù)。還可以寫入slave上最新的事務(wù)id,這里可以根據(jù)自己的業(yè)務(wù)靈活設(shè)計,znode節(jié)點中應(yīng)該寫入什么數(shù)據(jù)。
(4)設(shè)計 Watch
節(jié)點啟動的時候,嘗試創(chuàng)建 master znode,創(chuàng)建成功則切換為master,否則創(chuàng)建 slave znode,成為 slave,并監(jiān)聽master節(jié)點; 如果 slave 節(jié)點收到 master znode 刪除的事件,就自己去嘗試創(chuàng)建 master znode,創(chuàng)建成功,則自己成為 master,刪除自己創(chuàng)建的slave znode。
4.1.2 示意代碼
public class Node {
public static final Node INSTANCE = new Node();
private volatile String role = "slave";
private CuratorFramework curatorFramework;
private Node(){
}
public void start(String connectString) throws Exception{
if(connectString == null || connectString.isEmpty()){
throw new Exception("connectString is null or empty");
}
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(connectString).sessionTimeoutMs(5000).connectionTimeoutMs(3000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
String groupNodePath = "/com/dewu/order/operate";
String masterNodePath = groupNodePath + "/master";
String slaveNodePath = groupNodePath + "/slave";
//watch master node
PathChildrenCache pathChildrenCache = new PathChildrenCache(client, groupNodePath, true);
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
if(event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)){
String childPath = event.getData().getPath();
System.out.println("child removed: " + childPath);
//如果是master節(jié)點刪除了,則自己嘗試變成master
if(masterNodePath.equals(childPath)){
switchMaster(client, masterNodePath, slaveNodePath);
}
} else if(event.getType().equals(PathChildrenCacheEvent.Type.CONNECTION_LOST)) {
System.out.println("connection lost, become slave");
role = "slave";
} else if(event.getType().equals(PathChildrenCacheEvent.Type.CONNECTION_RECONNECTED)) {
System.out.println("connection connected……");
if(!becomeMaster(client, masterNodePath)){
becomeSlave(client, slaveNodePath);
}
}
else{
System.out.println("path changed: " + event.getData().getPath());
}
}
});
client.start();
pathChildrenCache.start();
}
public String getRole(){
return role;
}
private boolean becomeMaster(CuratorFramework client, String masterNodePath){
//try to become master
try {
client.create().creatingParentContainersIfNeeded().withMode(CreateMode.EPHEMERAL)
.forPath(masterNodePath, Longs.toByteArray(System.currentTimeMillis()));
System.out.println("succeeded to become master");
role = "master";
return true;
} catch (Exception e) {
System.out.println("failed to become master: " + e.getMessage());
return false;
}
}
private boolean becomeSlave(CuratorFramework client, String slaveNodePath) throws Exception {
//try to become slave
try {
client.create().creatingParentContainersIfNeeded().withMode(CreateMode.EPHEMERAL)
.forPath(slaveNodePath, Longs.toByteArray(System.currentTimeMillis()));
System.out.println("succeeded to become slave");
role = "slave";
return true;
} catch (Exception e) {
System.out.println("failed to become slave: " + e.getMessage());
throw e;
}
}
private void switchMaster(CuratorFramework client, String masterNodePath, String slaveNodePath){
if(becomeMaster(client, masterNodePath)){
try {
client.delete().forPath(slaveNodePath);
} catch (Exception e) {
System.out.println("failed to delete slave node when switch master: " + slaveNodePath);
}
}
}
}
參考 【代碼樣例】 【2】
4.2 實現(xiàn)集群選舉
集群選舉的方式比較多,主要是根據(jù)自己的業(yè)務(wù)場景,考慮選舉時的一些具體算法。
4.2.1 最小節(jié)點獲勝
就是在共同的父節(jié)點下創(chuàng)建znode,誰的編號最小,誰是leader。
(1)設(shè)計 Path
集群共用父節(jié)點 parent znode,也就是上圖中的operate,集群中的每個節(jié)點在 parent 目錄下創(chuàng)建自己的 znode。如上圖中,假如有5個節(jié)點,編號是從
node0000000001~node0000000005。
(2)選擇節(jié)點類型
當 Leader 節(jié)點掛掉的時候,持有最小編號 znode 的集群節(jié)點成為新的 Leader,因此用ephemeral_sequential 類型 znode。使用ephemeral類型的目的是,leader掛掉的時候,節(jié)點能自動刪除,使用sequential類型的目的是,讓這些節(jié)點都是有序的,我們選擇最小節(jié)點的時候就比較簡單。
(3)設(shè)計 節(jié)點數(shù)據(jù)
可以根據(jù)業(yè)務(wù)需要靈活寫入各種數(shù)據(jù)。
(4)設(shè)計 Watch
-
- 節(jié)點啟動或者重連后,在 parent 目錄下創(chuàng)建自己的 ephemeral_sequntial znode;
- 創(chuàng)建成功后掃描 parent 目錄下所有 znode,如果自己的 znode 編號是最小的,則成為 Leader,否則 監(jiān)聽 parent整個目錄;
- 當 parent 目錄有節(jié)點刪除的時候,首先判斷其是否是 Leader 節(jié)點,然后再看其編號是否正好比自己小1,如果是則自己成為 Leader,如果不是繼續(xù) watch。
Curator 的 LeaderLatch、LeaderSelector 采用這種策略,可以參考【Curator】【3】看下對應(yīng)的代碼 。
4.2.2 搶建唯一節(jié)點
集群共用父節(jié)點 parent znode,也就是operate,集群中的每個節(jié)點在 parent 目錄下創(chuàng)建自己的 znode。也就是集群中只有一個節(jié)點,誰創(chuàng)建成功誰就是leader。
(1)設(shè)計 Path
集群所有節(jié)點只有一個 leader znode,其實本質(zhì)上就是一個分布式鎖。
(2)選擇 znode 類型
當 Leader 節(jié)點掛掉的時候,剩余節(jié)點都來創(chuàng)建 leader znode,看誰能最終搶到 leader znode,因此用ephemeral 類型。
(3)設(shè)計 節(jié)點數(shù)據(jù)
可以根據(jù)業(yè)務(wù)需要靈活寫入各種數(shù)據(jù)。
(4)設(shè)計 Watch
-
- 節(jié)點啟動或者重連后,嘗試創(chuàng)建 leader znode,嘗試失敗則監(jiān)聽 leader znode;
- 當收到 leader znode 被刪除的事件通知后,再次嘗試創(chuàng)建leader znode,嘗試成功則成為leader ,失敗則繼續(xù)監(jiān)聽leader znode。
4.2.3 法官判決
整體實現(xiàn)比較復雜的一個方案,通過創(chuàng)建節(jié)點,判斷誰是法官節(jié)點。 法官節(jié)點可以根據(jù)一定的邏輯算法來判斷誰是leader,比如看誰的數(shù)據(jù)最新等等。
(1) 設(shè)計 Path
集群共用父節(jié)點 parent znode,集群中的每個節(jié)點在 parent 目錄下創(chuàng)建自己的 znode。
-
- parent znode:圖中的 operate,代表一個集群,選舉結(jié)果寫入到operate節(jié)點,比如寫入的內(nèi)容可以是:leader=server6。
- 法官 znode:圖中的橙色 znode,最小的 znode,持有這個 znode 的節(jié)點負責選舉算法/規(guī)則。
-
-
- 例如:實現(xiàn)Redis 存儲集群的選舉,各個 slave 節(jié)點可以將自己存儲的數(shù)據(jù)最新的 trxId寫入到 znode,然后法官節(jié)點將 trxID 最大的節(jié)點選為新的 Leader。
-
- 成員 znode:圖中的綠色 znode,每個集群節(jié)點對應(yīng)一個,在選舉期間將選舉需要的信息寫入到自己的 znode。
- Leader znode:圖中的紅色 znode,集群里面只有一個,由法官選出來。
(2)選擇節(jié)點類型
當 Leader 節(jié)點掛掉的時候,持有最小編號 znode 的集群節(jié)點成為“法官”,因此用 ephemeral_sequential 類型 znode。
(3)設(shè)計 節(jié)點數(shù)據(jù)
可以根據(jù)業(yè)務(wù)需要靈活寫入各種數(shù)據(jù),比如寫入當前存儲的最新的數(shù)據(jù)對應(yīng)的事務(wù) ID。
(4)設(shè)計 Watch
-
- 節(jié)點啟動或者重連后,在 parent 目錄下創(chuàng)建自己的 ephemeral_sequntial znode,并 監(jiān)聽 parent 目錄;
- 當 parent 目錄有節(jié)點刪除的時候,所有節(jié)點更新自己的 znode 里面和選舉相關(guān)的數(shù)據(jù);
- “法官”節(jié)點讀取所有 znode的數(shù)據(jù),根據(jù)規(guī)則或者算法選舉新的 Leader,將選舉結(jié)果寫入parent znode;
- 所有節(jié)點監(jiān)聽 parent znode,收到變更通知的時候讀取 parent znode 的數(shù)據(jù),判斷自己是否成為 Leader。
4.2.4 集群選舉模式對比
實現(xiàn)方式 |
實現(xiàn)復雜度 |
選舉靈活性 |
應(yīng)用場景 |
最小節(jié)點獲勝 |
低 |
低 |
計算集群 |
搶建唯一節(jié)點 |
低 |
低 |
計算集群 |
法官判決 |
高 |
高,可以設(shè)計滿足業(yè)務(wù)需求的復雜選舉算法和規(guī)則 |
存儲集群 |
這里簡單說下計算集群和存儲集群的應(yīng)用場景:
計算集群
無狀態(tài),不存在存儲集群里所有的數(shù)據(jù)覆蓋問題,計算集群只要選出一個leader或主節(jié)點就行,對業(yè)務(wù)沒什么影響。
存儲集群
需要考慮比較復雜的選舉邏輯,考慮數(shù)據(jù)一致性,考慮數(shù)據(jù)盡量不要丟失不要沖突等等,所以需要一個復雜的選舉邏輯。
這里也可以看到,并不是功能越強大越好,實際上需要考慮不同的應(yīng)用場景,特性,基于不同的業(yè)務(wù)要求選擇合適的方案。一切脫離業(yè)務(wù)場景的方案都是耍流氓。
5. Etcd
這里再跟大家簡答提一下etcd,主要是zookeeper與etcd的應(yīng)用場景類似,在實際的落地選型時也會拿來對比,如何選擇。
5.1 etcd概述
除了zookeeper,etcd也是最近幾年比較火的一個分布式協(xié)調(diào)框架。
etcd是一種強一致性的分布式鍵值存儲,它提供了一種可靠的方式來存儲需要由分布式系統(tǒng)或機器集群訪問的數(shù)據(jù)。 它優(yōu)雅地處理網(wǎng)絡(luò)分區(qū)期間的leader選舉,并且可以容忍機器故障。 etcd是go寫的一個分布式協(xié)調(diào)組件,尤其是在云原生的技術(shù)領(lǐng)域里,目前已經(jīng)成為了云原生和分布式系統(tǒng)的存儲基石。 下圖是etcd的請求示意圖:
通常,一個用戶的請求發(fā)送過來,會經(jīng)由 HTTP Server 轉(zhuǎn)發(fā)給 邏輯層進行具體的事務(wù)處理,如果涉及到節(jié)點的修改,則交給 Raft 模塊進行狀態(tài)的變更、日志的記錄,然后再同步給別的 etcd 節(jié)點以確認數(shù)據(jù)提交,最后進行數(shù)據(jù)的提交,再次同步。
5.2 應(yīng)用場景
與zookeeper一樣,有類似的應(yīng)用場景,包括:
- 服務(wù)發(fā)現(xiàn)
- 配置管理
- 分布式協(xié)調(diào)
- Master選舉
- 分布式鎖
- 負載均衡
比如openstack 使用etcd做配置管理和分布式鎖,ROOK使用etcd研發(fā)編排引擎。
5.3 簡單對比
|
zookeeper |
etcd |
語言 |
JAVA |
go |
協(xié)議 |
TCP |
grpc |
接口調(diào)用 |
必須要使用自己的client進行調(diào)用 |
可通過http傳輸,即可通過curl等命令實現(xiàn)調(diào)用 |
一致性算法 |
Zab; Zab 協(xié)議則由 Leader 選舉、發(fā)現(xiàn)、同步、廣播組成 |
Raft ;Raft 算法由 Leader 選舉、日志同步、安全性組成 |
watch功能 |
較局限,一次性觸發(fā)器 |
一次watch可以監(jiān)聽所有的事件 |
數(shù)據(jù)模型 |
基于目錄的層次模式 |
參考了zk的數(shù)據(jù)模型,是個扁平的kv模型 |
存儲 |
kv存儲,使用的是 Concurrent HashMap,內(nèi)存存儲,一般不建議存儲較多數(shù)據(jù) |
kv存儲,使用bbolt存儲引擎,可以處理幾個GB的數(shù)據(jù)。 |
支持mvcc |
不支持 |
etcd支持mvcc,通過兩個b+tree進行版本控制 |
權(quán)限校驗 |
實現(xiàn)的 ACL |
etcd 實現(xiàn)了 RBAC 的權(quán)限檢驗 |
事務(wù)能力 |
提供了簡易的事務(wù)能力 |
只提供了版本號的檢查能力 |
在實際的業(yè)務(wù)場景中具體選擇哪個產(chǎn)品,要考慮自己的業(yè)務(wù)場景,考慮具體的特性,開發(fā)語言等等。目前zookeeper 是用 java 語言的,被 Apache 很多項目采用,etcd 是用 go 開發(fā)的,在云原生的領(lǐng)域使用比較多。不過從實現(xiàn)上看,etcd提供了更穩(wěn)定的高負載穩(wěn)定讀寫能力。
結(jié)尾
綜上所述,zookeeper是一個比較成熟的,經(jīng)過市場驗證的分布式協(xié)調(diào)框架,可以協(xié)助我們快速地解決分布式系統(tǒng)中遇到的一些難題。另從上面的介紹中發(fā)現(xiàn),zookeeper的核心是zab,etcd的核心是raft,那可以思考下,還有哪些一致性算法?在分布式存儲的架構(gòu)里中又有哪些關(guān)聯(lián)呢呢?這篇文章不做詳細介紹了,后面會針對這塊做個詳細講解。