Zookeeper其實是一種為分布式應用所設計的高可用、高性能且一致的開源協調服務。提供分布式鎖服務這基本的服務(類似google chubby),同時也支持許多其他的服務,例如配置維護、命名管理、集群管理、組服務、分布式消息隊列、分布式通知/協調。
ZooKeeper所提供的服務主要是通過:數據結構(Znode)+原語+watcher機制,三個部分來實現的。
使用ZooKeeper來進行分布式通知和協調能夠大大降低系統之間的耦合(使用到了發布/訂閱模式)。
數據模型
Zookeeper 會維護一個具有層次關系的數據結構,它非常類似於一個標准的文件系統
具有如下特點:
- 每個子目錄項都被稱作為 znode,這個 znode 是被它所在的路徑唯一標識,如 Server1 這個 znode 的標識為 /NameService/Server1
- znode 可以有子節點目錄,並且每個 znode 可以存儲數據,注意 EPHEMERAL 類型的目錄節點不能有子節點目錄
- znode 是有版本的,每個 znode 中存儲的數據可以有多個版本,也就是一個訪問路徑中可以存儲多份數據
- znode 可以是臨時節點,一旦創建這個 znode 的客戶端與服務器失去聯系,這個 znode 也將自動刪除,Zookeeper 的客戶端和服務器通信采用長連接方式,每個客戶端和服務器通過心跳來保持連接,這個連接狀態稱為 session,如果 znode 是臨時節點,這個 session 失效,znode 也就刪除了
- znode 的目錄名可以自動編號,如 App1 已經存在,再創建的話,將會自動命名為 App2
- znode 可以被監控,包括這個目錄節點中存儲的數據的修改,子節點目錄的變化等,一旦變化可以通知設置監控的客戶端,這個是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基於這個特性實現的,后面在典型的應用場景中會有實例介紹
節點的類型:
ZooKeeper中的節點有兩種,分別為臨時節點和永久節點。節點的類型在創建時即被確定,並且不能改變。
- 臨時節點:該節點的生命周期依賴於創建它們的會話。一旦會話(Session)結束,臨時節點將被自動刪除,當然可以也可以手動刪除。雖然每個臨時的Znode都會綁定到一個客戶端會話,但他們對所有的客戶端還是可見的。另外,ZooKeeper的臨時節點不允許擁有子節點。
- 永久節點:該節點的生命周期不依賴於會話,並且只有在客戶端顯示執行刪除操作的時候,他們才能被刪除。
順序節點
當創建Znode的時候,用戶可以請求在ZooKeeper的路徑結尾添加一個遞增的計數。這個計數對於此節點的父節點來說是唯一的,它的格式為"%10d"(10位數字,沒有數值的數位用0補充,例如"0000000001")。當計數值大於232-1時,計數器將溢出。
監控
客戶端可以在節點上設置watch,我們稱之為監視器。當節點狀態發生改變時(Znode的增、刪、改)將會觸發watch所對應的操作。當watch被觸發時,ZooKeeper將會向客戶端發送且僅發送一條通知,因為watch只能被觸發一次,這樣可以減少網絡流量。
基本操作與實例
在ZooKeeper中有9個基本操作,如下圖所示:
共享鎖:
需要獲得鎖的 Server 創建一個 EPHEMERAL_SEQUENTIAL 目錄節點,然后調用 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是不是就是自己創建的目錄節點,如果正是自己創建的,那么它就獲得了這個鎖,如果不是那么它就調用 exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到自己創建的節點是列表中最小編號的目錄節點,從而獲得鎖,釋放鎖很簡單,只要刪除前面它自己所創建的目錄節點就行了。
void getLock() throws KeeperException, InterruptedException{ List<String> list = zk.getChildren(root, false); String[] nodes = list.toArray(new String[list.size()]); Arrays.sort(nodes); if(myZnode.equals(root+"/"+nodes[0])){ doAction(); } else{ waitForLock(nodes[0]); } } void waitForLock(String lower) throws InterruptedException, KeeperException { Stat stat = zk.exists(root + "/" + lower,true); if(stat != null){ mutex.wait(); } else{ getLock(); } }
隊列管理
Zookeeper 可以處理兩種類型的隊列:
- 當一個隊列的成員都聚齊時,這個隊列才可用,否則一直等待所有成員到達,這種是同步隊列。
- 隊列按照 FIFO 方式進行入隊和出隊操作,例如實現生產者和消費者模型。
同步隊列用 Zookeeper 實現的實現思路如下:
創建一個父目錄 /synchronizing,每個成員都監控標志(Set Watch)位目錄 /synchronizing/start 是否存在,然后每個成員都加入這個隊列,加入隊列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然后每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i。判斷 i 的值是否已經是成員的個數,如果小於成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start。
void addQueue() throws KeeperException, InterruptedException{ zk.exists(root + "/start",true); zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); synchronized (mutex) { List<String> list = zk.getChildren(root, false); if (list.size() < size) { mutex.wait(); } else { zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } }
當隊列沒滿是進入 wait(),然后會一直等待 Watch 的通知,Watch 的代碼如下
public void process(WatchedEvent event) { if(event.getPath().equals(root + "/start") && event.getType() == Event.EventType.NodeCreated){ System.out.println("得到通知"); super.process(event); doAction(); } }
參考:https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/
---------------------------------------------------------------------------------------------------------------------
zab協議
最基本的一致性算法是Paxos,但是存在如下的一些問題:
- 活鎖問題:由於不存在leader,則存在P1和P2都提交了proposal,但是其中1個的n較小會被拒絕,所以立馬提出更大n的proposal,所以兩邊不斷提出更大n的proposal,始終不能commit;
- 復雜度問題:base-paxos協議中還存在這樣那樣的問題,於是各種變種paxos出現了,比如為了解決活鎖問題,出現了multi-paxos;為了解決通信次數較多的問題,出現了fast-paxos;為了盡量減少沖突,出現了epaxos。可以看到,工業級實現需要考慮更多的方面,諸如性能,異常等等。這也是為啥許多分布式的一致性框架並非真正基於paxos來實現的原因
- 全序問題:對於paxos算法來說,不能保證兩次提交最終的順序,而zookeeper需要做到這點(保證所有的包之間嚴格的FIFO順序)。
所以采用zab協議(zookeeper atomic broadcast),ZAB在Paxos算法上做了重要改造,和Paxos有着明顯的不同。
- 可靠傳輸:如果消息m被一台服務器送達,它最終會被送達到所有服務器
- 全序:如果一台服務器上消息a在消息b前送達,那么在所有服務器上a將比b先送達。如果a和b是已傳輸過的消息,那么要么a在b前送達,要么b在a前送達(即不可能有同時發生的情況)
- 因果序:如果一個發送者在消息a送達后再發送消息b,那么a必須排在b之前。如果發送者在送達b后再發送消息c,那么c必須排在b之后
- 有序傳輸:數據發出和數據送達的順序嚴格一致,即消息m被送達當且僅當m前發送的所有消息都已被送達。(推論:如果m丟失,m后的所有消息必須丟棄)
- 關閉后沒有消息:一旦FIFO通道關閉,不會再從它收到消息。
唯一id保證:
ZooKeeper事務id(zxid)來標記整體順序,zxid由周期(epoch)和計數器(counter)組成,各為32位整數,因此zxid也可以記為一個整數對(epoch, count)。epoch的值代表leader的改變,leader對每個提案只是簡單地遞增zxid以得到一個唯一的zxid值。Leader激活算法會保證只有一個leader使用一個特定的epoch,因此這個簡單的算法可以保證每個提案都有一個唯一的id。
法定人數設定:
“法定人數”代表一組服務器,必須滿足任意兩個法定人數對之間至少有一個共同的服務器。因此典型情況下,任意一個法定人數至少有(n/2+1)台服務器即可滿足要求,這里n是ZooKeeper服務中的總服務器數。也有其它的構成法定人數的方法,例如PBFT中是2n/3+1,或者對每台服務器分配投票的權重,最后只需要加權的服務器投票數和超過1/2即可。
follower對於提案的認可:認可意味着服務器已將提案保存到持久化存儲上,並且發送ACK。
- Leader激活:在這個階段,leader建立起正確的狀態並准備發起提案
- 消息激活:在這個階段,leader接受消息以發起或協調消息傳輸follower對於提案的認可:認可意味着服務器已將提案保存到持久化存儲上,並且發送ACK。
Leader激活滿足的條件:
當且僅當followers中的法定人數(leader也算)與該leader達成同步,即它們有相同的狀態。這個狀態包含leader認為所有已提交的提案,以及讓followers跟隨本leader的提案(NEW_LEADER提案)。具體使用什么leader選舉算法不關心,只要保證如下兩點:
- leader已經獲知所有followers的最大的zxid
- 一個服務器間的法定人數已經確認會跟隨leader
Leader選舉完成后一台服務器會被指定為leader並等待followers的連接,其他服務器嘗試連接到leader。Leader將和followers同步,通過發送followers缺失的提案(DIFF),但如果followers缺失太多提案,將發送一個完整的快照(SNAP)。
新leader通過獲知的最大zxid來確定新的zxid,如前最大zxid的epoch位是e,則leader使用(e+1, 0)作為新的zxid。在leader和follower同步后,leader會發出一個NEW_LEADER提案。一旦NEW_LEADER提案被提交,leader就算完全激活並開始收發其他提案。
具體的策略如下:
- 選舉擁有 proposal 最大值(即 zxid 最大) 的節點作為新的 leader:由於所有提案被 COMMIT 之前必須有合法數量的 follower ACK,即必須有合法數量的服務器的事務日志上有該提案的 proposal,因此,只要有合法數量的節點正常工作,就必然有一個節點保存了所有被 COMMIT 消息的 proposal 狀態。
- 新的 leader 將自己事務日志中 proposal 但未 COMMIT 的消息處理。
- 新的 leader 與 follower 建立先進先出的隊列, 先將自身有而 follower 沒有的 proposal 發送給 follower,再將這些 proposal 的 COMMIT 命令發送給 follower,以保證所有的 follower 都保存了所有的 proposal、所有的 follower 都處理了所有的消息。
消息激活階段(廣播):十分類似於二階段的提交過程
- leader以相同的順序向所有followers發送提案,且這一順序和收到請求的順序保持一致。因為使用了FIFO通道,於是保證followers也按此順序收到提案。
- followers以收到提案的順序處理消息。這意味着消息將被有序地ACK且leader按此順序收到ACK,仍然是由FIFO通道保證。這也意味着如果某提案上的消息m被寫到非易失性存儲(硬盤)上,所有在m前提出的提案上的消息也已被寫到非易失性存儲上。
- 當法定人數的followers全部ACK某消息后,leader會發出一個COMMIT提案。因為所有消息是按序ACK的,leader發出COMMIT且followers收到該提案也是按序的。
- COMMIT按序被處理。提案被提交后意味着followers可以分發提案上的消息了(發送給客戶端)。
與Paxos的區別:主要在於需要保證所有的proposal的有序
zab與raft的相同點:
- 都使用timeout來重新選擇leader.
- 采用quorum來確定整個系統的一致性(也就是對某一個值的認可),這個quorum一般實現是集群中半數以上的服務器,zookeeper里還提供了帶權重的quorum實現.
- 都由leader來發起寫操作.
- 都采用心跳檢測存活性.
- leader election都采用先到先得的投票方式.
- zab用的是epoch和count的組合來唯一表示一個值, 而raft用的是term和index.
- zab的follower在投票給一個leader之前必須和leader的日志達成一致,而raft的follower則簡單地說是誰的term高就投票給誰.
- raft協議的心跳是從leader到follower, 而zab協議則相反.
- raft協議數據只有單向地從leader到follower(成為leader的條件之一就是擁有最新的log), 而zab協議在discovery階段, 一個prospective leader需要將自己的log更新為quorum里面最新的log,然后才好在synchronization階段將quorum里的其他機器的log都同步到一致.
參考:https://blog.csdn.net/mayp1/article/details/51871761 https://www.jianshu.com/p/fb527a64deee
--------------------------------------------------------------------------------------