前言
ZooKeeper是一個提供高可用,一致性,高性能的保證讀寫順序的存儲系統。ZAB協議為ZooKeeper專門設計的一種支持數據一致性的原子廣播協議。
演示環境
$ uname -a Darwin 18.6.0 Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64 x86_64
安裝
brew cask install java brew install zookeeper
配置
這里演示的是在同一台機器部署3個ZooKeeper進程的偽集群。
$ cat /usr/local/etc/zookeeper/zoo1.cfg tickTime=2000 initLimit=10 syncLimit=5 dataDir=/usr/local/var/run/zookeeper/data1 clientPort=2181 server.1=localhost:2888:3888 server.2=localhost:4888:5888 server.3=localhost:6888:7888
$ echo "1" > /usr/local/var/run/zookeeper/data1/myid
- tickTime ZooKeeper中使用的基本時間單元,以毫秒為單位,默認是2000。它用來調節心跳和超時。
- initLimit 默認值是10,即tickTime屬性值的10倍。表示允許follower連接並同步到leader的最大時間。如果ZooKeeper管理的數據量很大的話可以增加這個值。
- syncLimit 默認值是5,即tickTime屬性值的5倍。表示lead和follower進行心跳檢測的最大延遲時間。如果在設置的時間內follower無法於leader進行通信,那么follower將會被丟棄。
- dataDir ZooKeeper用來存儲內存數據庫快照的目錄,並且除非指定其他目錄,否則數據庫更新的事務日志也會存儲在該目錄。可以配置dataLogDir指定ZooKeeper事務日志的存儲目錄。
- clientPort 服務器監聽客戶端連接的端口,默認是2181。
- server.id = host:port1:port2 集群模式中用於感知其他機器,每一行代表一個ZooKeeper實例配置。id被成為Server ID用來表示實例在集群中的序號。同時需要將每個實例的ID寫入dataDir的myid文件中。port1用於數據同步。port2用於選舉。
集群中第2個實例的配置為:
$ cat /usr/local/etc/zookeeper/zoo2.cfg tickTime=2000 initLimit=10 syncLimit=5 dataDir=/usr/local/var/run/zookeeper/data2 clientPort=2182 server.1=localhost:2888:3888 server.2=localhost:4888:5888 server.3=localhost:6888:7888 $ cat /usr/local/var/run/zookeeper/data2/myid 2
集群中第3個示例的配置為:
$ cat /usr/local/etc/zookeeper/zoo3.cfg tickTime=2000 initLimit=10 syncLimit=5 dataDir=/usr/local/var/run/zookeeper/data3 clientPort=2183 server.1=localhost:2888:3888 server.2=localhost:4888:5888 server.3=localhost:6888:7888 $ cat /usr/local/var/run/zookeeper/data3/myid 3
啟動集群:
$ zkServer start /usr/local/etc/zookeeper/zoo1.cfg ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo1.cfg Starting zookeeper ... STARTED $ zkServer start /usr/local/etc/zookeeper/zoo2.cfg ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo2.cfg Starting zookeeper ... STARTED $ zkServer start /usr/local/etc/zookeeper/zoo3.cfg ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo3.cfg Starting zookeeper ... STARTED $ zkServer status /usr/local/etc/zookeeper/zoo1.cfg ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo1.cfg Mode: follower $ zkServer status /usr/local/etc/zookeeper/zoo2.cfg ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo2.cfg Mode: leader $ zkServer status /usr/local/etc/zookeeper/zoo3.cfg ZooKeeper JMX enabled by default Using config: /usr/local/etc/zookeeper/zoo3.cfg Mode: follower
從上面的狀態檢查可以看出,第二實例是leader,其他兩個實例是follower。
操作
下面我將演示在集群中讀寫節點。
$ zkCli -server localhost:2182 Connecting to localhost:2182 Welcome to ZooKeeper! JLine support is enabled WATCHER:: WatchedEvent state:SyncConnected type:None path:null [zk: localhost:2182(CONNECTED) 2] create /test "i am test" Created /test [zk: localhost:2182(CONNECTED) 3] get /test i am test cZxid = 0x200000002 ctime = Tue Jul 02 16:35:15 CST 2019 mZxid = 0x200000002 mtime = Tue Jul 02 16:35:15 CST 2019 pZxid = 0x200000002 cversion = 0 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 9 numChildren = 0
在實例2中創建/test節點,接下來在實例3中也能讀取到該節點,說明ZooKeeper集群數據的一致性。
ZooKeeper語義保證
ZooKeeper簡單高效,同時提供以下語義保證,從而使我們可以利用這些特性提供復雜的服務:
- 順序性:客戶端發起的更新請求會按發送順序被應用到ZooKeeper上。
- 原子性:更新操作要么成功要么失敗,不會出現中間狀態。
- 可靠性:一個更新操作一旦被接受即不會意外丟失,除非被其他更新操作覆蓋。
- 最終一致性:寫操作最終(而非立即)會對客戶端可見。
ZooKeeper Watch 機制
所有對ZooKeeper的讀操作都可以附帶一個Watch。一旦相應的數據有變化,該Watch即被觸發。
Watch有以下特點:
- 主動推送:Watch被觸發時,由ZooKeeper服務器主動將更新推送給客戶端,而不需要客戶端輪詢。
- 一次性:數據變化時Watch只會被觸發一次。如果客戶端想得到后續更新的通知,必須要在Watch被觸發后重新再注冊一個Watch。
- 順序性:如果多個更新觸發了多個Watch,那Watch被觸發的順序與更新的順序一致。
- 可見性:如果一個客戶端在讀請求中附帶Watch,Watch被觸發的同時再次讀取數據,客戶端在得到Watch消息之前肯定不可能看到更新后的數據。換句話說,更新通知先於更新結果。
ZAB協議
為了保證寫操作的一致性與可用性,ZooKeeper在paxos的基礎上設計了一種名為原子廣播(ZAB)的支持崩潰恢復的一致性協議。基於該協議,ZooKeeper實現了一種主從模式的系統架構來保持集群中各個副本之間的數據一致性。
根據ZAB協議,所有的寫操作都必須通過leader來完成,leader寫入本地日志后再復制到所有的follower節點。如果客戶端對follower/observer發起寫請求,follower/observer會將請求轉發到leader,然后由leader處理完成后再將結果轉發回follower/observer發送給客戶端。
ZAB協議分為廣播模式和崩潰恢復模式
leader處理寫請求(廣播模式)的步驟為:
1.leader為事務請求生成唯一的事務ID(ZXID),ZAB協議會將每個事務Proposal按照ZXID的先后順序來進行排序和處理。
2.將Proposal發送給follower並等待follower回復ACK。
3.leader收到超過半數的ACK(leader默認對自己有一個ACK)后向所有的follower/observer發送commit,同時leader自身也會完成commit。
4.將處理結果返回給客戶端。
上述過程成為ZooKeeper的兩階段提交。
崩潰恢復:
當leader實例宕機崩潰,或者因為網絡原因導致其與過半的follower都失去聯系,那么就會進入崩潰恢復階段。
leader宕機或者與過半的follower失聯都會導致leader重新選舉(選舉算法文章后面會介紹)。選舉結束后會緊着進入數據崩潰恢復,以保證數據一致性,也就是數據同步。需要保證已經commit的事務被所有服務器都提交,同時需要丟棄那些只在leader服務器提交的事務。所以選出來的新leader要擁有集群中最高編號的ZXID。在新的leader選舉出來后就會進行數據同步工作,leader會將那些沒有被follower同步的事務以Proposal消息的形式發送給follower,並在每個Proposal消息后面緊跟着發送一個commit消息,表示該事務已經被提交。然后follower服務器會將同步過來的事務Proposal都成功應用到本地數據庫。
ZXID是個64位的無符號整形,高32位是epoch,代表leader選擇周期,低32位是累加計數,每一輪選舉后該計數會清零。leader服務器沒產生一個事務,ZXID的低32位就會加一,沒完成一次leader選舉,就會將ZXID的高32位加一。這樣做是為了保證新leader生成的ZXID肯定是大於舊leader之前產生的ZXID。
領導選舉算法
服務器狀態:
- LOOKING 不確定Leader狀態。該狀態下的服務器認為當前集群中沒有Leader,會發起Leader選舉。
- FOLLOWING 跟隨者狀態。表明當前服務器角色是Follower,並且它知道Leader是誰。
- LEADING 領導者狀態。表明當前服務器狀態是leader,它會維護與Follower間的心跳。
- OBSERVING 觀察者狀態。表明當前服務器角色是Observer,與Follower不同是不參與選舉,也不參與集群寫操作的投票。
選票數據結構:
每個服務器在進行領導者選舉是,會發送如下關鍵信息:
- logicClock 每個服務器都會維護一個自增的整數,名為logicClock,它表示這是該服務器第幾輪發起的投票。
- state 表示該服務器當前的狀態。
- self_id 當前服務器的myid
- self_zxid 當前服務器上所保存的數據的最大zxid
- vote_id 被推舉的服務器的myid
- vote_zxid 被推舉的服務器上所保存的數據的最大zxid
(快速領導者選舉算法)選票PK:
選票PK是基於(logicClock,self_id, self_zxid) 與 (vote_logicClock, vote_self_id, vote_self_zxid)對比:
先比較logicClock,如果相等再比較zxid,如果zxid相等,再比較myid。最后如果vote大於自身,則改票,也投vote。
總結
文章一開始演示ZooKeeper的部署和操作給讀者一個直觀感受,然后介紹了ZooKeeper的ZAB協議和領導者選舉原理。
參考
https://cwiki.apache.org/confluence/display/ZOOKEEPER/ProjectDescription
https://dbaplus.cn/news-141-1875-1.html