Zookeeper研究和應用

jopen 10年前發布 | 22K 次閱讀 分布式/云計算/大數據 ZooKeeper

zookeeper簡介

zookeeper是一個開源分布式的服務,它提供了分布式協作,分布式同步,配置管理等功能. 其實現的功能與google的chubby基本一致.zookeeper的官方網站已經寫了一篇非常經典的概述性文章,請大家參閱:ZooKeeper: A Distributed Coordination Service for Distributed Applications
在此我僅花少量筆墨介紹下本文相關的內容。
在zookeeper的集群中,各個節點共有下面3種角色和4種狀態:

  • 角色:leader,follower,observer
  • 狀態:leading,following,observing,looking

除了observer和observing之外,其它的角色和狀態與下面將要介紹的Paoxs算法中的角色與狀態一一對應,我們將在下文中具體描述.
observer是zookeeper-3.3版本新添加的一個角色,在這里有相關的介紹. 他們的引入是為了解決zookeeper集群擴大后,由于網絡可靠性下降可能導致的拜占庭將軍問題. observer的行為在大多數情況下與follower完全一致, 但是他們不參加選舉和投票, 而僅僅接受(observing)選舉和投票的結果.

zookeeper實現了一個層次名字空間(hierarchal name space)的數據模型, 它特別象一個文件系統, 每個文件被稱為znode, 一個znode除了自己包含一些數據外,還能擁有孩子節點.
存在下述的3種類型znode:

  • Persistent Nodes: 永久有效地節點,除非client顯式的刪除,否則一直存在
  • Ephemeral Nodes: 臨時節點,僅在創建該節點client保持連接期間有效,一旦連接丟失,zookeeper會自動刪除該節點
  • Sequence Nodes: 順序節點,client申請創建該節點時,zk會自動在節點路徑末尾添加遞增序號,這種類型是實現分布式鎖,分布式queue等特殊功能的關鍵

Zookeeper Watch 定義如下:

A watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes.

在我看來,watch可以理解為一個分布式的回調,當client關心的znodes發生變化時,zookeeper將會把消息傳回到client,并導致client的消息處理函數得到調用.zk的任何一個讀操作都能夠設置watch,例如:getData(), getChildren(), and exists()
可以watch的event包括如下的二種:

  • KeeperState:Disconnected,SyncConnected,Expired
  • EventType:None,NodeCreated,NodeDeleted,NodeDataChanged,NodeChildrenChanged

這些狀態是很容易理解的. watch的實現只言片語沒法說清楚,后面我可能會專門寫一篇文章講述這個實現.

Paoxs算法

說到zookeeper,我們不得不提起Paoxs算法Lesile Lamport.
Paoxs 算法是zookeeper的靈魂,這個算法是Leslie Lamport在1990年提出的一種基于消息傳遞的一致性算法.Paxos 算法解決的問題是一個分布式系統如何就某個值(決議)達成一致。一個典型的場景就是:”在zookeeper cluster中誰是leader?”。
該算法由Leslie于1990年在文章The Part-Time Parliament中首次提出,但是這篇文章相當的晦澀難懂(也有一些軼事,可以看文章鏈接中Leslie自己寫的內容),于是,Lesilie在2001年寫下了Paxos Made Simple.他對此解釋道:

At the PODC 2001 conference, I got tired of everyone saying how difficult it was to understand the Paxos algorithm, published in [122]. Although people got so hung up in the pseudo-Greek names that they found the paper hard to understand, the algorithm itself is very simple. So, I cornered a couple of people at the conference and explained the algorithm to them orally, with no paper. When I got home, I wrote down the explanation as a short note, which I later revised based on comments from Fred Schneider and Butler Lampson. The current version is 13 pages long, and contains no formula more complicated than n1 > n2.

Paxos Made Simple的abstract只有一句話:

The Paxos algorithm, when presented in plain English, is very simple.

可見這位Lamport老兄是多么的有意思. 順便說一句,這位老哥就是LaTex中的”La”.
在上文中是這樣描述Paoxs算法執行過程的:

Phase 1.
(a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered proposal (if any) that it has accepted.
Phase 2.
(a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v, where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.
(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.

這幾乎就是Paxos的全部了.具體的執行過程舉例可以在Zookeeper全解析――Paxos作為靈魂中找到,在此不再贅述.
Zookeeper 完全實現了Paoxs算法,zk cluster中每個節點都保持了一份完整的數據模型,當任何一個client通過某集群節點向集群發起讀寫請求時,該節點會向Leader節點發出投票 請求,如果投票通過(超過一半節點同意)則該請求被執行,否則該請求被駁回. 通過paoxs算法,zookeeper的保持了數據模型的一致性,同時保持了任何操作的原子性.

分布式選舉

介紹完了Paoxs算法, 分布式選舉幾乎是順理成章的, 因為分布式選舉不過是Paoxs算法的一次或者若干次執行, 所不同的只是proposal內容為:”誰是Leader”.下面這兩個圖解釋了zookeeper集群在正常工作和選舉時各個節點狀態的異同:

zookeeper狀態示意圖

zookeeper狀態示意圖

zookeeper采用org.apache.zookeeper.server.quorum.FastLeaderElection作為其缺省選舉算法,關于這個算法的具體執行流程可以參考淘寶核心系統段飛同學的文章“paxos 實現”.或者也可以直接閱讀源代碼. zookeeper源代碼量不大,結構清晰,注釋充分,閱讀體驗超好~ 我就不在這里越俎代庖了.

zookeeper應用

擁有了zookeeper如此強大的分布式協作系統后,我們可以很容易的實現大量的分布式應用,包括了分布式鎖,分布式隊列,分布式Barrier,雙階段提交等等. 這些應用可以幫我們改進很多復雜系統的協作方式,將這些系統的實現變得更加優雅而高效.
鑒于篇幅,本文僅介紹分布式鎖的實現.
利用了前文提到的sequence nodes可以非常容易的實現分布式鎖. 實現分布式鎖的基本步驟如下(這些步驟需要在所有需要鎖的客戶端執行):

  1. client調用create()創建名為”_locknode_/lock-”的節點,注意需要設置sequence和ephemeral屬性
  2. client調用getChildren(“_locknode_”),注意不能設置watch,這樣才能避免羊群效應
  3. 如果步驟1中創建的節點序號最低,則該client獲得鎖,開始執行其它程序
  4. client對lock-xxx中序號僅次于自己創建節點的那個節點調用exists(),并設置watch
  5. 如果exist()返回false(節點不存在)則回到步驟2,否則等待步驟4中的watch被觸發并返回步驟2

分布式鎖在zookeeper的源代碼中已經有實現,可以參考org.apache.zookeeper.recipes.lock

下面是一個使用分布式鎖的樣例,這段程序摘自一個hadoop reduce的configure函數, 使用分布式鎖的目的是確保一臺機器上的所有reduce進程中,只有一個reduce進程會執行某些初始化代碼. 同時其它reduce在總和初始化完成之前不會繼續執行.


以下是代碼片段:
class zkWatcher implements Watcher {
     //watch回調函數
    public void process(WatchedEvent event) {
         if (event.getType() == EventType.NodeCreated) {
            if (event.getPath() == "balbalbal.init_done"
            //如果回調信息是節點創建,且創建的節點是init成功節點,則觸發latch
                  gcihInitLatch.countDown();
        } else if (event.getState() == KeeperState.SyncConnected) {
            //server連接成功,觸發連接成功latch
            zkConnectedLatch.countDown();
         }
    }
}
public void configure(String conf) {
    try {
        //zookeeper服務器列表,節點間用,分隔
        String keepers = "zk_server0:port,zk_server1:port,zk_server2:port";
        String Init_Done = "/full-dump-gcih/"
                + InetAddress.getLocalHost().getHostName() + ".init_done";
        String HostName = InetAddress.getLocalHost().getHostName();
        // 初始化一個Watch
        zkWatcher zkw = new zkWatcher();
        //異步創建連接, 并設置zkw為watch回調
        ZooKeeper zk = new ZooKeeper(keepers, 5000, zkw);
        //等待zookeeper創建連接成功
        zkConnectedLatch.await();
        //創建分布式鎖
        WriteLock gcih_lock = new WriteLock(zk, "/full-dump-gcih/" + HostName, null);
        //檢測初始化成功標識是否存在,并設置watch
        if (null == zk.exists(Init_Done, true)) {
            // if the init_done node not exists we try to init
            if (gcih_lock.lock()) {
                //獲取鎖成功,初始化數據
                initializeData(conf);
                //創建初始化成功標識,注意這個標志是永久節點
                zk.create(Init_Done, null, Ids.OPEN_ACL_UNSAFE,  CreateMode.PERSISTENT);
                //工作完成,釋放鎖
                gcih_lock.unlock();
            } else {
                //未獲取鎖,說明已經有reduce在做初始化了,釋放鎖
                gcih_lock.unlock();
                if (!gcihInitLatch.await(30, TimeUnit.MINUTES))
                    throw new IOException(
                            "Init UDP time out, critical error");
                else {
                    //latch成功返回,說明the one 初始化成功了
                    initializeData(null);
                }
            }
        } else {// if init_done exists we simply load data from gcih
            initializeData(null);
        }
     } catch (Exception e) {
        .....
    }
  }

多個reduce分別獲取鎖后,加鎖節點的子節點信息如下所示


以下是引用片段:
[zk: localhost:2181(CONNECTED) 31] ls /full-dump-gcih/xxxxx.cm2
[x-84692699318388014-0000000001, x-84692699318387993-0000000000]

這些節點全部是Sequence+Ephemeral 屬性的節點, 其中


以下是引用片段:
x-84692699318388014-000000000
name-zk_session_id-sequence_number

這個節點名稱是org.apache.zookeeper.recipes.lock中使用的名稱,可以根據需要自己重新實現相關代碼,進而設計一個專用的鎖.
關于Zookeeper更多的應用請參閱ZooKeeper Recipes and Solutions

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!