zookeeper 快速入門

janlle發表於2019-04-25

zookeeper

zookeeper是什麼

Apache ZooKeeper是Apache軟體基金會的一個軟體專案,他為大型分散式計算提供開源的分散式配置服務、同步服務和命名註冊。ZooKeeper曾經是Hadoop的一個子專案,但現在是一個獨立的頂級專案。

ZooKeeper的架構通過冗餘服務實現高可用性。因此,如果第一次無應答,客戶端就可以詢問另一臺ZooKeeper主機。ZooKeeper節點將它們的資料儲存於一個分層的名稱空間,非常類似於一個檔案系統或一個字首樹結構。客戶端可以在節點讀寫,從而以這種方式擁有一個共享的配置服務。

使用ZooKeeper的公司包括Rackspace、雅虎和eBay,以及類似於像Solr這樣的開源企業級搜尋系統。

zookeeper提供了什麼

  • 檔案系統:zookeeper維護一個類似檔案系統的資料結構,每個子目錄項如 NameService 都被稱作為 znode,和檔案系統一樣,自由增加及刪除,唯一不同其可儲存資料。Znode分為四種型別

    • PERSISTENT-持久化目錄節點。(客戶端與zookeeper斷開連線後,該節點依舊存在)。

    • PERSISTENT_SEQUENTIAL-持久化順序編號目錄節點。(客戶端與zookeeper斷開連線後,該節點依舊存在,只是Zookeeper給該節點名稱進行順序編號)

    • EPHEMERAL-臨時目錄節點(客戶端與zookeeper斷開連線後,該節點被刪除)

    • EPHEMERAL_SEQUENTIAL-臨時順序編號目錄節點。(客戶端與zookeeper斷開連線後,該節點被刪除,只是Zookeeper給該節點名稱進行順序編號)

  • 通知機制:客戶端註冊監聽它關心的目錄節點,當目錄節點發生變化(資料改變、被刪除、子目錄節點增加刪除)時,zookeeper會通知客戶端。

zookeeper能為我們做什麼?

  • 命名服務:在zookeeper的檔案系統裡建立一個目錄,即有唯一的path。在我們使用tborg無法確定上游程式的部署機器時即可與下游程式約定好path,通過path即能互相探索發現。

  • 配置管理:把應用配置放置zookeeper上去,儲存在 Zookeeper 的某個目錄節點中,然後所有相關應用程式對這個目錄節點進行監聽,一旦配置資訊發生變化,每個應用程式就會收到 Zookeeper 的通知,然後從 Zookeeper 獲取新的配置資訊應用到系統中就好。

  • 叢集管理:節點(機器)增刪及Master選取。節點增刪:所有機器約定在父目錄GroupMembers下建立臨時目錄節點,然後監聽父目錄節點的子節點變化訊息。一旦有機器掛掉,該機器與 zookeeper的連線斷開,其所建立的臨時目錄節點被刪除,所有其他機器都收到通知:某個兄弟目錄被刪除,於是,所有人都知道:它上船了。新機器加入 也是類似,所有機器收到通知:新兄弟目錄加入,highcount又有了。Master選取:所有機器建立臨時順序編號目錄節點,每次選取編號最小的機器作為master就好。

  • 分散式鎖:基於zookeeper一致性檔案系統,實現鎖服務。鎖服務分為儲存獨佔及時序控制兩類。儲存獨佔:將zookeeper上的一個znode看作是一把鎖,通過createznode的方式來實現。所有客戶端都去建立 /distribute_lock 節點,最終成功建立的那個客戶端也即擁有了這把鎖。用完刪除自己建立的distribute_lock 節點就釋放鎖。時序控制:基於/distribute_lock鎖,所有客戶端在它下面建立臨時順序編號目錄節點,和選master一樣,編號最小的獲得鎖,用完刪除,依次方便。

  • 佇列管理:分同步佇列,FIFO佇列(入隊與出隊),同步佇列:當一個佇列的成員都聚齊時,這個佇列才可用,否則一直等待所有成員到達。在約定目錄下建立臨時目錄節點,監聽節點數目是否是我們要求的數目。FIFO佇列:和分散式鎖服務中的控制時序場景基本原理一致,入列有編號,出列按編號。

  • 分散式與資料複製:Zookeeper作為一個叢集提供一致的資料服務,必然在所有機器間做資料複製。資料複製好處:(1)容錯:一個節點出錯,不致於讓整個系統停止工作,別的節點可以接管它的工作。(2)提高系統的擴充套件能力:把負載分佈到多個節點上,或者增加節點來提高系統的負載能力;(3)效能提升:讓客戶端本地訪問就近節點,提高使用者訪問速度。

zookeeper基本概念

角色簡介

Zookeeper角色分為三類,領導者:負責進行投票的發起和決議,更新系統狀態。跟隨者:Follower用於接收客戶請求並向客戶端返回結果,在選中過程中參與投票。觀察者:Observer可以接收客戶端連線,將寫請求轉發給leader節點。但不參加投票過程,只同步leader狀態。Observer目的在於擴充套件系統,提高讀取速度。

設計目的

  • 一致性:client不論連線到哪個Server,展示給它都是同一個檢視,這是zookeeper最重要的效能。

  • 可靠性:具有簡單、健壯、良好的效能,如果訊息m被到一臺伺服器接受,那麼它將被所有的伺服器接受。

  • 實時性:Zookeeper保證客戶端將在一個時間間隔範圍內獲得伺服器的更新資訊,或者伺服器失效的資訊。但由於網路延時等原因,Zookeeper不能保證兩個客戶端能同時得到剛更新的資料,如果需要最新資料,應該在讀資料之前呼叫sync()介面。

  • 等待無關(wait-free):慢的或者失效的client不得干預快速的client的請求,使得每個client都能有效的等待。

  • 原子性:更新只能成功或者失敗,沒有中間狀態。

  • 順序性:包括全域性有序和偏序兩種:全域性有序是指如果在一臺伺服器上訊息a在訊息b前釋出,則在所有Server上訊息a都將在訊息b前被髮布;偏序是指如果一個訊息b在訊息a後被同一個傳送者釋出,a必將排在b前面。

選主流程

當leader崩潰或者leader失去大多數的follower,這時候zk進入恢復模式,恢復模式需要重新選舉出一個新的leader,讓所有的Server都恢復到一個正確的狀態。Zk的選舉演算法有兩種:一種是基於basic paxos實現的,另外一種是基於fast paxos演算法實現的。系統預設的選舉演算法為fast paxos。先介紹basic paxos流程:

選舉執行緒由當前Server發起選舉的執行緒擔任,其主要功能是對投票結果進行統計,並選出推薦的Server; 選舉執行緒首先向所有Server發起一次詢問(包括自己); 選舉執行緒收到回覆後,驗證是否是自己發起的詢問(驗證zxid是否一致),然後獲取對方的id(myid),並儲存到當前詢問物件列表中,最後獲取對方提議的leader相關資訊(id,zxid),並將這些資訊儲存到當次選舉的投票記錄表中; 收到所有Server回覆以後,就計算出zxid最大的那個Server,並將這個Server相關資訊設定成下一次要投票的Server; 執行緒將當前zxid最大的Server設定為當前Server要推薦的Leader,如果此時獲勝的Server獲得n/2 + 1的Server票數, 設定當前推薦的leader為獲勝的Server,將根據獲勝的Server相關資訊設定自己的狀態,否則,繼續這個過程,直到leader被選舉出來。 通過流程分析我們可以得出:要使Leader獲得多數Server的支援,則Server總數必須是奇數2n+1,且存活的Server的數目不得少於n+1.

每個Server啟動後都會重複以上流程。在恢復模式下,如果是剛從崩潰狀態恢復的或者剛啟動的server還會從磁碟快照中恢復資料和會話資訊,zk會記錄事務日誌並定期進行快照,方便在恢復時進行狀態恢復。

fast paxos流程是在選舉過程中,某Server首先向所有Server提議自己要成為leader,當其它Server收到提議以後,解決epoch和zxid的衝突,並接受對方的提議,然後向對方傳送接受提議完成的訊息,重複這個流程,最後一定能選舉出Leader。

zookeeper的安裝使用

wget http://mirrors.cnnic.cn/apache/zookeeper/zookeeper-3.4.8/zookeeper-3.4.8.tar.gz

tar zxvf zookeeper-3.4.8.tar.gz -C /usr/local/

cd $ZOOKEEPER_HOME

cp conf/zoo_sample.cfg conf/zoo.cfg

# 叢集需要在zoo.cfg配置

server.1=192.168.1.148:2888:3888
server.2=192.168.1.149:2888:3888
server.3=192.168.1.150:2888:3888


# 在zookeeper的臨時目錄建立myid
mkdir -p /tmp/zookeeper

# 分別在不同節點建立myid檔案裡面的數字對應節點的編號比如server.1就對應1,server.2就對應2
echo 1 > /tmp/zookeeper/myid

# 最後分別啟動叢集上的節點
$ZOOKEEPER_HOME/bin/zkServer.sh start

# 檢視zookeeper的狀態
$ZOOKEEPER_HOME/bin/zkServer.sh status

# 停止zookeeper服務
$ZOOKEEPER_HOME/bin/zkServer.sh stop 

複製程式碼

zookeeper命令列操作

啟動zookeeper服務後到bin目錄啟動zookeeper的客戶端$ZOOKEEPER_HOME/bin/zkCli.sh

# 輸入help

[zk: localhost:2181(CONNECTED) 0] help
ZooKeeper -server host:port cmd args
        stat path [watch]
        set path data [version]
        ls path [watch]
        delquota [-n|-b] path
        ls2 path [watch]
        setAcl path acl
        setquota -n|-b val path
        history 
        redo cmdno
        printwatches on|off
        delete path [version]
        sync path
        listquota path
        rmr path
        get path [watch]
        create [-s] [-e] path data acl
        addauth scheme auth
        quit 
        getAcl path
        close 
        connect host:port

複製程式碼

建立節點

[zk: localhost:2181(CONNECTED) 14] create /test test-data
Created /test
複製程式碼

檢視節點

[zk: localhost:2181(CONNECTED) 1] ls /
[abc, zookeeper, eclipse]
複製程式碼

獲取節點

[zk: localhost:2181(CONNECTED) 10] get /test
test-update
cZxid = 0x38
ctime = Sat Dec 22 10:11:46 CST 2018
mZxid = 0x39
mtime = Sat Dec 22 10:12:05 CST 2018
pZxid = 0x38
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 0
複製程式碼

修改節點

[zk: localhost:2181(CONNECTED) 11] set /test test-update
cZxid = 0x38
ctime = Sat Dec 22 10:11:46 CST 2018
mZxid = 0x3a
mtime = Sat Dec 22 10:15:04 CST 2018
pZxid = 0x38
cversion = 0
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 0
複製程式碼

刪除節點

[zk: localhost:2181(CONNECTED) 13] delete /test
複製程式碼

zookeeper java 客戶端操作



/**
 * @author leone
 * @since 2018-06-16
 **/
public class ZkClient {

    private final static Logger logger = LoggerFactory.getLogger(ZkClient.class);

    private final static String ZK_URL = "xxx.xxx.xxx.xxx:2181";

    private final static int TIME_OUT = 5000;

    private static ZooKeeper zkClient = null;


    @Before
    public void init() throws Exception {
        zkClient = new ZooKeeper(ZK_URL, TIME_OUT, (WatchedEvent event) -> {
            // 收到事件通知後的回撥函式(應該是我們自己的事件處理邏輯)
            logger.info(event.getType() + "---" + event.getPath());
            try {
                zkClient.getChildren("/", true);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 設定值
     *
     * @throws Exception
     */
    @Test
    public void testSetData() throws Exception {
        zkClient.setData("/eclipse", "world".getBytes(), -1);
        byte[] data = zkClient.getData("/eclipse", false, null);
        System.out.println(new String(data));
    }

    /**
     * 建立節點
     *
     * @throws Exception
     */
    @Test
    public void testCreate() throws Exception {
        // 引數1:要建立的節點的路徑 引數2:節點資料 引數3:節點的許可權 引數4:節點的型別
        zkClient.create("/eclipse/aaa", "aaaData".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }


    /**
     * 測試某節點是否存在
     *
     * @throws Exception
     */
    @Test
    public void testExists() throws Exception {
        Stat stat = zkClient.exists("/eclipse", false);
        System.out.println(stat == null ? "not exist" : "exist");
    }

    /**
     * 獲取子節點
     *
     * @throws Exception
     */
    @Test
    public void testGetChild() throws Exception {
        List<String> children = zkClient.getChildren("/", true);
        for (String child : children) {
            System.out.println(child);
        }
    }

    /**
     * 刪除節點
     *
     * @throws Exception
     */
    @Test
    public void testDelete() throws Exception {
        // 引數2:指定要刪除的版本,-1表示刪除所有版本
        zkClient.delete("/abc", -1);
    }


    /**
     * 獲取節點的資料
     *
     * @throws Exception
     */
    @Test
    public void testGetDate() throws Exception {
        byte[] data = zkClient.getData("/eclipse", false, null);
        System.out.println(new String(data));
    }

}

複製程式碼