ZooKeeper 使用 Java API

JiangYue發表於2018-05-03

zkCli 工具適用於除錯,不推薦使用 zkCli 工具來搭建系統。

實際開發時一般也不直接使用 ZooKeeper 的 Java API,而是使用更高層次的封裝庫 Curator,不過對 Java API 的學習仍然有很多益處。

本篇文章介紹通過 ZooKeeper 的 Java API 來實現建立會話、實現監視點等功能,演示主從模式。

新增依賴

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.9</version>
</dependency>
複製程式碼

建立會話

啟動 ZooKeeper 服務端,通過 Java API 建立會話。

ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
複製程式碼

其中 connectString 包含主機名及埠號,sessionTimeout 為會話超時時間,watcher 物件用於接收會話事件。

Watcher 為一個介面,實現 Watcher 需要重寫 void process(WatchedEvent event) 方法。

當遇到網路故障時,如果連線斷開,ZooKeeper 客戶端會自動重新連線。

獲取管理權

下面通過 ZooKeeper Java API 來實現簡單的群首選舉演算法,確保同一時間只有一個主節點程式處於活動狀態。

package com.ulyssesss.zookeeper;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Random;

public class Master implements Watcher {
    
    private ZooKeeper zk;
    private String serviceId = Integer.toString(new Random().nextInt());
    private boolean isLeader = false;

    private void startZk() throws IOException {
        zk = new ZooKeeper("localhost:2181", 5000, this);
    }

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

    public void process(WatchedEvent watchedEvent) {
        System.out.println("event: " + watchedEvent);
    }

    public static void main(String[] args) throws Exception {
		Master master = new Master();
		master.startZk();

		master.runForMaster();

        System.out.println("serviceId: " + master.serviceId);

		if (master.isLeader) {
            System.out.println("master");
            Thread.sleep(10000);
        } else {
            System.out.println("not master");
        }

		master.stopZk();
    }

    private boolean checkMaster() throws InterruptedException {
        while (true) {
            try {
                Stat stat = new Stat();
                byte data[] = zk.getData("/master", false, stat);
                isLeader = new String(data).equals(serviceId);
                return true;
            } catch (KeeperException.NoNodeException e) {
                return false;
            } catch (KeeperException e) {
                e.printStackTrace();
            }
        }
    }

    private void runForMaster() throws InterruptedException {
        while (true) {
            try {
                zk.create("/master", serviceId.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
                isLeader = true;
                break;
            } catch (KeeperException.NodeExistsException e) {
                isLeader = false;
                break;
            } catch (KeeperException e) {
                e.printStackTrace();
            }
            if (checkMaster()) {
                break;
            }
        }
    }
}
複製程式碼

主函式執行建立一個演示例項,例項會分配一個隨機整數作為 id,建立 ZooKeeper 連線後嘗試建立主節點 master。

如果 master 主節點建立成功,則該例項為群首 leader;如果節點已經存在則其他例項為 leader;發生斷開連線等異常時,響應資訊丟失,無法確定當前程式是否為主節點,需要通過 checkMaster() 方法重新檢查主節點狀態。

多次執行主函式,其中第一次執行時會列印 master,在 master 斷開連線前的 10 秒鐘內,再次執行會列印 not master,當第一次執行的 master 斷開連線後,再次執行主函式,列印 master。

非同步建立需要的目錄

在 ZooKeeper 中所有的同步操作都有對應的非同步操作,非同步呼叫不會阻塞應用程式,還能簡化應用的實現方式。

主從模型的設計需要用到 /tasks、/assign 和 /workers 3 個目錄,可以通過某些系統配置來建立這些目錄。下面的程式碼示例會通過非同步的方式建立出所需要的目錄。

private void bootstrap() {
    createParent("/workers", new byte[0]);
    createParent("/assign", new byte[0]);
    createParent("/tasks", new byte[0]);
}

private void createParent(String path, byte[] data) {
    zk.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT, createParentCallback, data);
}

AsyncCallback.StringCallback createParentCallback = new AsyncCallback.StringCallback() {
    public void processResult(int rc, String path, Object ctx, String name) {
        switch (KeeperException.Code.get(rc)) {
            case OK:
                System.out.println("parent " + path + " created");
                break;
            case NODEEXISTS:
                System.out.println("parent " + path + " already registered");
                break;
            case CONNECTIONLOSS:
                createParent(path, (byte[]) ctx);
                break;
            default:
                System.out.println("create " + path + " error");
        }
    }
};
複製程式碼

註冊從節點

前面的部分已經有了主節點,為了使主節點可以發號施令,現在要配置從節點,在 /workers 下建立臨時節點。

package com.ulyssesss.zookeeper;

import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.Random;

public class Worker implements Watcher {

    private ZooKeeper zk;
    private String serviceId = Integer.toString(new Random().nextInt());

    private void startZk() throws IOException {
        zk = new ZooKeeper("localhost:2181", 5000, this);
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("event: " + event);
    }

    private void register() {
        zk.create("/workers/worker-" + serviceId, "Idle".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE
                , CreateMode.EPHEMERAL, createWorkerCallback, null);
    }

    private AsyncCallback.StringCallback createWorkerCallback = new AsyncCallback.StringCallback() {
        @Override
        public void processResult(int rc, String path, Object ctx, String name) {
            switch (KeeperException.Code.get(rc)) {
                case OK:
                    System.out.println("registered successfully: " + serviceId);
                    break;
                case NODEEXISTS:
                    System.out.println("already registered: " + serviceId);
                    break;
                case CONNECTIONLOSS:
                    register();
                    break;
                default:
                    System.out.println("error");
            }
        }
    };

    private String status;

    public void setStatus(String status) {
        this.status = status;
        updateStatus(status);
    }

    synchronized private void updateStatus(String status) {
        if (status.equals(this.status)) {
            zk.setData("/workers/worker-" + serviceId, status.getBytes(), -1, statusUpdateCallback, status);
        }
    }

    AsyncCallback.StatCallback statusUpdateCallback = new AsyncCallback.StatCallback() {
        @Override
        public void processResult(int rc, String path, Object ctx, Stat stat) {
            switch (KeeperException.Code.get(rc)) {
                case CONNECTIONLOSS:
                    updateStatus((String) ctx);
                    break;
                default:
            }
        }
    };
    
    public static void main(String[] args) throws Exception {
		Worker worker = new Worker();
		worker.startZk();
		worker.register();
		Thread.sleep(30000);
    }
}
複製程式碼

主函式建立 worker 例項,開啟會話,執行註冊邏輯,建立節點時如發生連線丟失則再次執行註冊邏輯,註冊所建立的節點為臨時節點。

從節點開始處理某些任務時,需要通過 setStatus() 方法更新節點狀態。

任務佇列

系統中 client 元件用於新增任務,以便從節點執行任務。以下為 client 程式碼:

package com.ulyssesss.zookeeper;

import org.apache.zookeeper.*;
import java.io.IOException;

public class Client implements Watcher {

    private ZooKeeper zk;

    private void startZk() throws IOException {
        zk = new ZooKeeper("localhost:2181", 5000, this);
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("event: " + event);
    }

    private String queueCommand(String command) {
        while (true) {
            try {
                String name = zk.create("/tasks/task-", command.getBytes()
                        , ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
                return name;
            } catch (Exception e) {
                System.out.println("error");
            }
        }
    }


    public static void main(String[] args) throws Exception {
		Client client = new Client();
		client.startZk();
		String name = client.queueCommand("command-1");
        System.out.println("created " + name);
    }
}
複製程式碼

Client 使用有序節點 task- 標示任務,task- 後面會跟隨一個遞增整數,在執行 create 時如發生連線丟失,則重試 create 操作,適用於【至少執行一次】策略的應用。如要採用【至多執行一次】策略,可以將任務的唯一標識新增到節點名中。

管理客戶端

管理客戶端 AdminClient 用於展示系統執行狀態,程式碼如下:

package com.ulyssesss.zookeeper;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.Date;

public class AdminClient implements Watcher {

    private ZooKeeper zk;

    private void startZk() throws IOException {
        zk = new ZooKeeper("localhost:2181", 5000, this);
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("event: " + event);
    }

    private void listState() throws KeeperException, InterruptedException {
        try {
            Stat stat = new Stat();
            byte[] masterData = zk.getData("/master", false, stat);
            Date startDate = new Date(stat.getCtime());
            System.out.println("master: " + new String(masterData) + " since " + startDate);
        } catch (KeeperException.NoNodeException e) {
            System.out.println("no master");
        }

        System.out.println("workers: \n");
        for (String worker : zk.getChildren("/workers", false)) {
            byte[] data = zk.getData("/workers/" + worker, false, null);
            String state = new String(data);
            System.out.println("worker: " + state);
        }

        // ...
    }

    public static void main(String[] args) throws Exception {
		AdminClient adminClient = new AdminClient();
		adminClient.startZk();
		adminClient.listState();
    }
}
複製程式碼

以上程式碼會簡單的列出各個節點的資訊。

通過 Java API 程式設計與 zkCli 命令非常接近,不同的是 zkCli 常用於除錯,一般會在一個相對穩定的環境下使用。通過 Java API 編寫的程式,需要考慮到異常情況,尤其是 ConnectionLossException 異常,需要檢查狀態併合理恢復。

原文地址

相關文章