首頁>技術>

一、ZooKeeper產生背景

1.1、分散式的發展

分散式這個概念大家都比較熟悉,但是真正大規模系統的使用還得從Google說起,很早以前分散式被提出來的時候,對於計算機行業的人來說還是比較複雜和難學的技術,因此大規模的商業應用一直未出現。自從Google釋出了MapReduce、DFS和Bigtable的論文之後,分散式開始大規模使用,從架構上解決了分散式的難題,並且成熟的運用在了海量資料儲存和計算上。

以DFS為基礎的分散式計算框架和key、value資料高效的解決運算瓶頸。開發人員只需要較少的程式碼就可以完成分散式程式的開發,這使得開發人員只需要關注業務邏輯即可。最近幾年中分散式成為了海量資料儲存以及計算、高併發、高可靠性、高可用性的解決方案。

1.2、ZooKeeper的誕生

通常分散式架構都是中心化設計,就是一個主機控制連線多個處理節點。問題就是當主機失效時,整個系統就無法訪問了,因此在保證系統的高可用的同時,需要保證主機的高可用性。分散式鎖就是這一問題的較好方案,多主機搶一把鎖。這就涉及到了我們要講的ZooKeeper。

目前,在分散式協調服務中做得比較好的主要是Google的Chubby和Apache的ZooKeeper。那麼為什麼兩種軟體會同時存在呢,難道需要重複造輪子嗎?原因主要在於Chubby是Google公司專用的一款非開源軟體;而ZooKeeper作為一款開源軟體,由雅虎模仿Chubby開發並捐獻給Apache。ZooKeeper問世以來,其在分散式領域的可靠性和可用性都經受住了理論和實踐的檢驗。

ZooKeeper是一個針對大型分散式系統的可靠協調系統,主要用來解決分散式應用中遇到的一些資料管理問題,可以高可靠的維護元資料。提供的功能包括:配置維護、命名服務、分散式同步、組服務等。ZooKeeper的設計目標就是封裝好複雜易出錯的關鍵服務,將簡單易用的介面和效能高效穩定的系統提供給使用者。

1.3、ZooKeeper的使用

ZooKeeper作為一個分散式叢集框架,主要是用來解決分散式系統中的應用系統的一致性問題,它提供類似與Unix系統的檔案目錄節點樹方式的資料儲存,但是ZooKeeper可不是用來真正儲存資料的。它的主要作用是用來管理和監控儲存的資料的狀態變化。通過監控這些資料狀態的變化,從而達到基於資料的叢集管理,後續將會詳細介紹ZooKeeper能夠解決的實際問題。

(1)從資料大小上看:ZooKeeper的資料儲存在一個叫

ReplicatedDatabase的資料庫中,這是一個記憶體資料庫,既然是記憶體資料庫,那麼它的資料量就不會很大。這一點和Hadoop的HDFS有很大的區別,HDFS的資料主要儲存在磁碟上,因此資料儲存主要是HDFS的事情,而ZooKeeper主要是協調作用。

(2)從資料型別上看:由於記憶體空間的限制,因此我們不能隨心所欲的在ZooKeeper中儲存資料,我們只能儲存一些關鍵的資料並且不能太大。ZooKeeper主要應用在下面幾個方面:

叢集管理:利用臨時節點的特性,節點關聯的是機器的主機名、IP地址等相關資訊,叢集單點故障也屬於這個範疇。統一命名:主要利用節點的唯一性和節點目錄樹結構。配置管理:節點關聯的是配置資訊。分散式鎖:節點關聯的是要競爭的資源。二、ZooKeeper應用場景

ZooKeeper是一個高可用的分散式資料管理與資料協調框架。基於對Paxos演算法的實現,使得該框架保證了分散式環境的一致性,基於這個特點,使得ZooKeeper能夠應用於很多場景。

ZooKeeper實現的任何功能都離不開ZooKeeper的資料結構,任何功能的實現都是利用”Znode結構特性+節點關聯的資料“來實現的,ZooKeeper資料模型的結構與Unix檔案系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode。 每一個ZNode預設能夠儲存1MB的資料,每個ZNode都可以通過其路徑唯一標識。

ZooKeeper這種資料結構有如下特點

每個子目錄項如leaf都被稱作為znode,這個 znode 是被它所在的路徑唯一標識,如 leaf這個 znode 的標識為 /znode2/leaf。znode 可以有子節點目錄,並且每個 znode 可以儲存資料,注意 EPHEMERAL 型別的目錄節點不能有子節點目錄。znode 是有版本的,每個 znode 中儲存的資料可以有多個版本,也就是一個訪問路徑中可以儲存多份資料。znode 可以是臨時節點,一旦建立這個 znode 的客戶端與伺服器失去聯絡,這個 znode 也將自動刪除,Zookeeper 的客戶端和伺服器通訊採用長連線方式,每個客戶端和伺服器通過心跳來保持連線,這個連線狀態稱為 session,如果 znode 是臨時節點,這個 session 失效,znode 也就刪除了。znode 的目錄名可以自動編號,如 leaf1 已經存在,再建立的話,將會自動命名為 leaf2。znode 可以被監控,包括這個目錄節點中儲存的資料的修改,子節點目錄的變化等,一旦變化可以通知設定監控的客戶端,這個是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基於這個特性實現的。2.1、統一配置管理

典型場景描述

統一配置管理就是將資料釋出到ZK節點上,供訂閱者動態獲取資料,實現配置資訊的集中式管理和動態更新。例如全域性的配置資訊,地址列表等就非常的適合使用。一般的商業公司都會實現一套集中的配置管理中心,應對不同的應用叢集對於共享各自配置的需求,並且在配置變更時能夠通知到叢集中的每臺機器。

應用

叢集中機器節點狀態存放在ZK的一些指定節點,供各個客戶端訂閱使用。應用中用到的一些配置資訊集中管理,在應用啟動的時候主動來獲取一次,並且在節點上註冊一個watcher,以後每次配置有更新,實時通知應用系統以獲得最新配置資訊。業務邏輯中需要用到的一些全域性變數,比如一些訊息中介軟體的訊息佇列通常有個offset,這個offset放在ZK上,這樣叢集中每個傳送者都能知道當前的傳送進度。系統中有些資訊需要動態獲取,並且還會存在人工手動去修改這個資訊。以前通常是暴露出介面,例如JMX介面,有了ZK之後,只要將這些資訊存在ZK節點上即可。

應用舉例

1)分散式環境中,配置檔案同步非常常見

ZooKeeper的配置資訊如下圖所示:

ZooKeeper很容易實現這種集中式的配置管理,比如將所需的配置資訊放到/Configuration節點上,叢集中所有機器一啟動就會通過Client對/Configuration這個節點的監控zk.exist("/Configuration",true),並且實現Watcher回撥方法process(),那麼在ZooKeeper上/Configuration節點下資料發生變化的時候,每臺機器都會收到通知,Watcher回撥方法將會被執行,那麼應用再取下資料即可zk.getData("/Configuration",false,null)。

2.2、統一命名服務

在分散式應用中,通常需要一整套的命名規則,既能夠產生唯一的名稱又便於識別,通常情況下用樹型的名稱結構比較合適,樹型結構的層次目錄,既對人友好又不會重複。大家很容易聯想到JNDI,ZooKeeper的命名服務與JNDI能夠完成的功能差不多,但是ZooKeeper的命名服務是更加廣泛意義上的關聯,也許不需要將名稱關聯到特定的資源上,可能只需要一個不重複的名稱。

通過使用命名服務,客戶端能夠根據指定的名稱獲取資源的伺服器地址,提供者資訊等。被命名的實體通常可以是叢集中的機器,提供的服務地址,程序物件等等,這些我們都可以統稱他們為名字(Name)。其中較為常見的就是一些分散式服務框架中的服務地址列表。通過呼叫ZK提供的建立節點的API,能夠很容易建立一個全域性唯一的path,這個path就可以作為一個名稱。Name Service 已經是Zookeeper 內建的功能,你只要呼叫 Zookeeper 的 API 就能實現。如呼叫 create 介面就可以很容易建立一個目錄節點。

2.3、分散式鎖

場景描述

分散式鎖,這個主要得益於ZooKeeper為我們保證了資料的強一致性,即使用者只要完全相信每時每刻,zk叢集中任意節點(一個zk server)上的相同znode的資料是一定是相同的。鎖服務可以分為兩類,一個是保持獨佔,另一個是控制時序。

保持獨佔,就是所有試圖來獲取這個鎖的客戶端,最終只有一個可以成功獲得這把鎖。通常的做法是把ZK上的一個znode看作是一把鎖,通過create znode的方式來實現。所有客戶端都去建立 /distribute_lock 節點,最終成功建立的那個客戶端也即擁有了這把鎖。

控制時序,就是所有試圖來獲取這個鎖的客戶端,最終都是會被安排執行,只是有個全域性時序了。做法和上面基本類似,只是這裡 /distribute_lock 已經預先存在,客戶端在它下面建立臨時有序節點。Zk的父節點(/distribute_lock)維持一份sequence,保證子節點建立的時序性,從而也形成了每個客戶端的全域性時序。

應用

共享鎖在同一個程序中很容易實現,但是在跨程序或者在不同 Server 之間就不好實現了。Zookeeper 卻很容易實現這個功能,實現方式也是需要獲得鎖的 Server 建立一個 EPHEMERAL_SEQUENTIAL 目錄節點,然後呼叫 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是不是就是自己建立的目錄節點,如果正是自己建立的,那麼它就獲得了這個鎖,如果不是那麼它就呼叫 exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到自己建立的節點是列表中最小編號的目錄節點,從而獲得鎖,釋放鎖很簡單,只要刪除前面它自己所建立的目錄節點就行了。

演算法思路

對於加鎖操作,可以讓所有客戶端都去/locks目錄下建立臨時、順序節點,如果建立的客戶端發現自身建立節點序列號是/locks/目錄下最小的節點,則獲得鎖。否則,監視比自己建立節點的序列號小的節點(當前序列在自己前面一個的節點),進入等待。解鎖操作,只需要將自身建立的節點刪除即可。具體演算法流程如下圖所示:

package com.qinghe.zookeeper;​import java.io.IOException;​import org.apache.zookeeper.WatchedEvent;import org.apache.zookeeper.Watcher;import org.apache.zookeeper.ZooKeeper;​public class ConnectZKClient implements Watcher {​    private static final int sessionTimeout = 20000;    protected static ZooKeeper zk = null;    protected static Integer mutex;    protected String rootPath = null;    ​    public ConnectZKClient(String connectString) {        if (zk == null) {            try {                System.out.println("建立一個新的連線:");                zk = new ZooKeeper(connectString, sessionTimeout, this);                                mutex = new Integer(-1);            } catch (IOException e) {​                e.printStackTrace();            }        }​    }​    @Override    synchronized public void process(WatchedEvent event) {        synchronized (mutex) {            mutex.notify();        }​    }​}​
package com.qinghe.zookeeper;​import java.util.Arrays;import java.util.List;​import org.apache.zookeeper.CreateMode;import org.apache.zookeeper.KeeperException;import org.apache.zookeeper.WatchedEvent;import org.apache.zookeeper.ZooDefs.Ids;import org.apache.zookeeper.data.Stat;​public class SharedLocksClient extends ConnectZKClient {        private String myZnode = null;​    public static void main(String[] args) throws KeeperException, InterruptedException {        String connectString = "bigdata101:2181,bigdata102:2181,bigdata103:2181";        SharedLocksClient lock = new SharedLocksClient(connectString, "/locks");                lock.checkLock();    }​    private void checkLock() throws KeeperException, InterruptedException {​        myZnode  = zk.create(this.rootPath + "/_lock_", new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);                getLock();    }            private void getLock() throws KeeperException, InterruptedException {                List<String> children = zk.getChildren(this.rootPath, false);                String[] nodes = children.toArray(new String[children.size()]);        Arrays.sort(nodes);        if (myZnode.equals(this.rootPath + "/" + nodes[0])) {            business();        } else {            waitForLock(nodes[0]);        }    }​    private void waitForLock(String node) throws KeeperException, InterruptedException {        Stat stat = zk.exists(this.rootPath + "/" + node, true);                if (stat != null) {            synchronized (mutex) {                mutex.wait();            }                    } else {            getLock();        }    }        @Override    public void process(WatchedEvent event) {        if (event.getType() == Event.EventType.NodeDeleted) {            System.out.println("得到通知");            super.process(event);                business();        }            }​    /**     * 執行具體的業務     */    private void business() {        System.out.println("同步佇列已經得到同步,可以開始執行後面的任務了");            }        public SharedLocksClient(String connectString, String rootPath) throws KeeperException, InterruptedException {        super(connectString);        this.rootPath = rootPath;​        if (ConnectZKClient.zk != null) {            Stat s = zk.exists(rootPath, false);                        if (s == null) {                zk.create(rootPath, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);            }        }            }    ​}​
2.4、叢集管理

這通常用於那種對叢集中機器狀態,機器線上率有較高要求的場景,能夠快速對叢集中機器變化作出響應。這樣的場景中,往往有一個監控系統,實時檢測叢集機器是否存活。過去的做法通常是:監控系統通過某種手段(比如ping)定時檢測每個機器,或者每個機器自己定時向監控系統彙報"我還活著"。這種做法可行,但是存在兩個比較明顯的問題:

叢集中機器有變動的時候,牽連修改的東西比較多。有一定的延時。

利用ZooKeeper中兩個特性,就可以實施另一種叢集機器存活性監控系統:

客戶端在節點 x 上註冊一個Watcher,那麼如果 x 的子節點變化了,會通知該客戶端。建立EPHEMERAL型別的節點,一旦客戶端和伺服器的會話結束或過期,那麼該節點就會消失。

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 寫給 Android 應用工程師的 Binder 原理剖析