天天看點

Zookeeper的叢集配置和Java測試程式

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/catoop/article/details/50848555

概述

Zookeeper是Apache下的項目之一,傾向于對大型應用的協同維護管理工作。IBM則給出了IBM對ZooKeeper的認知: Zookeeper 分布式服務架構是 Apache Hadoop 的一個子項目,它主要是用來解決分布式應用中經常遇到的一些資料管理問題,如:統一命名服務、狀态同步服務、叢集管理、分布式應用配置項的管理等。總之,可以用“協調”這個核心的詞來形容它的作用。關于它能幹嗎,你可以看看 “

Zookeeper能幹什麼?

”。

特征

  1. 我們可以把Zookeeper了解為一個精簡的檔案系統(和Linux檔案系統結構非常相似),其每一個節點稱為znode,znode下可以存放子節點,也可以直接對節點進行指派存值。
  2. Zookeeper被應用與一些叢集上,提高叢集的高可用。它可以幫助你避免單點故障,使你的系統更加可靠。
  3. Zookeeper的叢集我們可以通俗的了解為,一個有Leader的團隊,團隊中各個成員的資料都是一緻的。團隊中的Leader采用選舉算法推舉,是以可以保證在Leader出現問題的時候,又會選舉出新的Leader。(fast paxos 選舉算法大家可以深入了解下)
  4. Zookeeper使用路徑來描述節點,節點可以被看做是一個目錄,也可以被看做是一個檔案,它同時具有兩者的特點。
  5. Zookeeper的Watch機制也是它的最大被應用的原因。當我們有很多用戶端連接配接到Zookeeper時,當被設定了Watch的資料發生了改變的時候,則伺服器将這個改變發送給設定了Watch的用戶端,通知它們。是以我們經常用它來做業務系統的統一配置管理。使用zk的Watch要特别注意一點就是它的“一次性觸發器”(最後的Java例子中有模拟這點)。

叢集部署

1. 下載下傳

官網:

http://zookeeper.apache.org/releases.html

下載下傳:zookeeper-3.4.8.tar.gz

2. 安裝

因為資源有限,是以我在同一個伺服器上面建立3個目錄 server1、server2、server3 來模拟3台伺服器叢集。

cd server1

tar -zxvf zookeeper-3.4.8.tar.gz

mkdir data

mkdir dataLog

data 為資料目錄,dataLog 為日志目錄。

3. 配置

cd zookeeper-3.4.8/conf

建立檔案 zoo.cfg,内容如下:

tickTime=2000 
initLimit=5
syncLimit=2
dataDir=/opt/zookeeper/server1/data
dataLogDir=/opt/zookeeper/server1/dataLog
clientPort=2181
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890           

tickTime:zookeeper中使用的基本時間機關, 毫秒值。

initLimit:這個配置項是用來配置 Zookeeper 接受用戶端(這裡所說的用戶端不是使用者連接配接 Zookeeper 伺服器的用戶端,而是 Zookeeper 伺服器叢集中連接配接到 Leader 的 Follower 伺服器)初始化連接配接時最長能忍受多少個 tickTime 時間間隔數。這裡設定為5表名最長容忍時間為 5 * 2000 = 10 秒。

syncLimit:這個配置辨別 Leader 與 Follower 之間發送消息,請求和應答時間長度,最長不能超過多少個 tickTime 的時間長度,總的時間長度就是 2 * 2000 = 4 秒。

dataDir 和 dataLogDir 看配置就知道幹嗎的了,不用解釋。

clientPort:監聽client連接配接的端口号,這裡說的client就是連接配接到Zookeeper的代碼程式。

server.{myid}={ip}:{leader伺服器交換資訊的端口}:{當leader伺服器挂了後, 選舉leader的端口}

maxClientCnxns:對于一個用戶端的連接配接數限制,預設是60,這在大部分時候是足夠了。但是在我們實際使用中發現,在測試環境經常超過這個數,經過調查發現有的團隊将幾十個應用全部部署到一台機器上,以友善測試,于是這個數字就超過了。

修改zoo.cfg非常簡單,然後還需要建立myid檔案:

cd server1

echo 1 > myid

然後拷貝server1為server2和server3,并修改其中的zoo.cfg配置,當然也要修改myid的内容為2和3。

下面給出3個server的zoo.cfg 内容:

# server1
tickTime=2000
initLimit=5
syncLimit=2
dataDir=/opt/zookeeper/server1/data
dataLogDir=/opt/zookeeper/server1/dataLog
clientPort=2181
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890           
# server2
tickTime=2000
initLimit=5
syncLimit=2
dataDir=/opt/zookeeper/server2/data
dataLogDir=/opt/zookeeper/server2/dataLog
clientPort=2182
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890           
# server3
tickTime=2000
initLimit=5
syncLimit=2
dataDir=/opt/zookeeper/server3/data
dataLogDir=/opt/zookeeper/server3/dataLog
clientPort=2183
server.1=127.0.0.1:2888:3888
server.2=127.0.0.1:2889:3889
server.3=127.0.0.1:2890:3890           

這裡做下說明:因為我們是在同一個機器上模拟的叢集,是以要注意server端口号和clientPort不要重複了,不然會出現端口沖突。是以,如果我們是3個不同的機器上做的3個server,那麼我們的zoo.cfg配置都是一樣的(注意server.{myid}=後面的IP位址使用具體的IP位址,如192.168.0.88)。還有就是,每一個server的myid内容都不能一樣,這也可以了解為不同server的辨別。

4. 啟動

進入 zookeeper-3.4.8/bin 目錄,使用 ./zkServer.sh start 啟動zk服務。(你也可以使用 ./zkServer.sh start myzoo.cfg 指定配置檔案啟動,這在自動化運維的時候很有用)

使用 tail -f zookeeper.out 檢視日志。

要說的是:在啟動第一個的時候,日志中會出現一堆錯誤,仔細一看就能明白,是因為另外2個server還沒有啟動它連接配接不上的錯誤。然後當我們啟動第二個server的時候,日志中的錯誤将會減少。最後我們把所有server都啟動起來後,日志中便沒有錯誤了。

5. 測試

随便進入一個zk目錄,連接配接一個server測試。

cd zookeeper-3.4.8/bin

zkCli.sh -server 127.0.0.1:2181

如果你要連接配接别的伺服器,請指定具體的IP位址。

幾個基本指令說明:

ls 檢視指定節點中包含的子節點(如:ls / 或 ls /app1/server1)

create 建立節點并指派

get 讀取節點内容

set 改變節點内容

delete 删除節點

注意zk中所有節點都基于路徑确定,如你要删除 /app1/server1/nodeA 的指令為:

delete /app1/server1/nodeA

下面是基本操作截圖:

Java程式Demo

建立一個Maven工程

打開pom.xml檔案添加zookeeper依賴

<dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.6</version>
        </dependency>           

建立Demo.java,代碼如下:

package com.shanhy.demo.zookeeper;

import java.io.IOException;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

/**
 * Zookeeper測試
 *
 * @create    2016年3月10日
 */
public class Test {

    // 會話逾時時間,設定為與系統預設時間一緻
    private static final int SESSION_TIMEOUT = 30 * 1000;

    // 建立 ZooKeeper 執行個體
    private ZooKeeper zk;

    // 建立 Watcher 執行個體
    private Watcher wh = new Watcher() {
        /**
         * Watched事件
         */
        public void process(WatchedEvent event) {
            System.out.println("WatchedEvent >>> " + event.toString());
        }
    };

    // 初始化 ZooKeeper 執行個體
    private void createZKInstance() throws IOException {
        // 連接配接到ZK服務,多個可以用逗号分割寫
        zk = new ZooKeeper("192.168.19.130:2181,192.168.19.130:2182,192.168.19.130:2183", Test.SESSION_TIMEOUT, this.wh);

    }

    private void ZKOperations() throws IOException, InterruptedException, KeeperException {
        System.out.println("\n1. 建立 ZooKeeper 節點 (znode : zoo2, 資料: myData2 ,權限: OPEN_ACL_UNSAFE ,節點類型: Persistent");
        zk.create("/zoo2", "myData2".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);

        System.out.println("\n2. 檢視是否建立成功: ");
        System.out.println(new String(zk.getData("/zoo2", this.wh, null)));// 添加Watch

        // 前面一行我們添加了對/zoo2節點的監視,是以這裡對/zoo2進行修改的時候,會觸發Watch事件。
        System.out.println("\n3. 修改節點資料 ");
        zk.setData("/zoo2", "shanhy20160310".getBytes(), -1);

        // 這裡再次進行修改,則不會觸發Watch事件,這就是我們驗證ZK的一個特性“一次性觸發”,也就是說設定一次監視,隻會對下次操作起一次作用。
        System.out.println("\n3-1. 再次修改節點資料 ");
        zk.setData("/zoo2", "shanhy20160310-ABCD".getBytes(), -1);

        System.out.println("\n4. 檢視是否修改成功: ");
        System.out.println(new String(zk.getData("/zoo2", false, null)));

        System.out.println("\n5. 删除節點 ");
        zk.delete("/zoo2", -1);

        System.out.println("\n6. 檢視節點是否被删除: ");
        System.out.println(" 節點狀态: [" + zk.exists("/zoo2", false) + "]");
    }

    private void ZKClose() throws InterruptedException {
        zk.close();
    }

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        Test dm = new Test();
        dm.createZKInstance();
        dm.ZKOperations();
        dm.ZKClose();
    }
}           

我想代碼不用解釋了,該注釋的裡面都注釋了。

下面有一種特殊的情況的處理思路:

有server1、server2、server3這三個服務,在client去連接配接zk的時候,指向server1初始化的過程中是沒有問題的,然而剛剛初始化完成,準備去連接配接server1的時候,server1因為網絡等原因挂掉了。

然而對client來說,它會拿server1的配置去請求連接配接,這時肯定會報連接配接被拒絕的異常以緻啟動退出。

是以優雅的解決這個問題的方法思路就是“在連接配接的時候判斷連接配接狀态,如果未連接配接成功,程式自動使用其他連接配接去請求連接配接”,這樣來避開這種罕見的異常問題。

代碼如下:

// 初始化 ZooKeeper 執行個體
    private void createZKInstance() throws IOException {
        // 連接配接到ZK服務,多個可以用逗号分割寫
        zk = new ZooKeeper("192.168.19.130:2181,192.168.19.130:2182,192.168.19.130:2183", Test.SESSION_TIMEOUT, this.wh);
        if(!zk.getState().equals(States.CONNECTED)){
            while(true){
                if(zk.getState().equals(States.CONNECTED)){
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }           

上面的代碼是基于zk提供的庫的API來你使用的,為了更易于使用,有人寫了開源的zkclient,我們可以直接使用它來操作zk。

zkclient 開源位址:

https://github.com/sgroschupf/zkclient

maven 依賴配置:

<!--zkclient -->
        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
            <version>0.7</version>
        </dependency>           

zkClient 針對 zk 的一次性watcher,做了重新封裝,然後定義了 stateChanged、znodeChanged、dataChanged 三種監聽器。

  1. 監聽children變化
  2. 監聽節點資料變化
  3. 監聽連接配接狀态變化
package com.shanhy.demo.zookeeper;

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.I0Itec.zkclient.DataUpdater;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.IZkStateListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.Watcher.Event.KeeperState;

/**
 * ZkClient的使用測試
 *
 * @author   單紅宇(365384722)
 * @myblog  http://blog.csdn.net/catoop/
 * @create    2016年3月11日
 */
public class ZkClientTest {

    public static void main(String[] args) {
        ZkClient zkClient = new ZkClient("192.168.19.130:2181,192.168.19.130:2182,192.168.19.130:2183");
        String node = "/myapp";

        // 訂閱監聽事件
        childChangesListener(zkClient, node);
        dataChangesListener(zkClient, node);
        stateChangesListener(zkClient);

        if (!zkClient.exists(node)) {
            zkClient.createPersistent(node, "hello zookeeper");
        }
        System.out.println(zkClient.readData(node));

        zkClient.updateDataSerialized(node, new DataUpdater<String>() {

            public String update(String currentData) {
                return currentData + "-123";
            }
        });
        System.out.println(zkClient.readData(node));

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 訂閱children變化
     *
     * @param zkClient
     * @param path
     * @author SHANHY
     * @create  2016年3月11日
     */
    public static void childChangesListener(ZkClient zkClient, final String path) {
        zkClient.subscribeChildChanges(path, new IZkChildListener() {

            public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
                System.out.println("clildren of path " + parentPath + ":" + currentChilds);
            }

        });
    }

    /**
     * 訂閱節點資料變化
     *
     * @param zkClient
     * @param path
     * @author SHANHY
     * @create  2016年3月11日
     */
    public static void dataChangesListener(ZkClient zkClient, final String path){
        zkClient.subscribeDataChanges(path, new IZkDataListener(){

            public void handleDataChange(String dataPath, Object data) throws Exception {
                System.out.println("Data of " + dataPath + " has changed.");
            }

            public void handleDataDeleted(String dataPath) throws Exception {
                System.out.println("Data of " + dataPath + " has changed.");
            }

        });
    }

    /**
     * 訂閱狀态變化
     *
     * @param zkClient
     * @author SHANHY
     * @create  2016年3月11日
     */
    public static void stateChangesListener(ZkClient zkClient){
        zkClient.subscribeStateChanges(new IZkStateListener() {

            public void handleStateChanged(KeeperState state) throws Exception {
                System.out.println("handleStateChanged");
            }

            public void handleSessionEstablishmentError(Throwable error) throws Exception {
                System.out.println("handleSessionEstablishmentError");
            }

            public void handleNewSession() throws Exception {
                System.out.println("handleNewSession");
            }
        });
    }
}

           

ZkClient 做了便捷的包裝,對Watch做了增強處理。

subscribeChildChanges實際上是通過exists和getChildren關注了兩個事件。這樣當create(“/path”)時,對應path上通過getChildren注冊的listener也會被調用。另外subscribeDataChanges實際上隻是通過exists注冊了事件。因為從上表可以看到,對于一個更新,通過exists和getData注冊的watcher要麼都會觸發,要麼都不會觸發。

關于session逾時的問題,ZkClient 貌似還是有對 Session Expired 處理的,在ZkClient.processStateChanged方法中。雖然能重新連接配接,但是連接配接上是一個新的 session,原有建立的ephemeral znode和watch會被删除,程式上你可能需要處理這個問題。

最後說幾點關于ZkClient的注意事項:

1. 建立節點的時候一定要先判斷節點是否存在,如果直接使用zkclient建立一個已經存在的節點,則會抛出異常。

2. 使用zkclient建立節點的時候,path描述的路徑,預新增的最終節點之前的所有父節點都必須要存在,否則會抛出異常。是以根據自己需要做好處理。