分佈式系統筆記:利用zookeeper實現分佈式leader節點選舉

利用zookeeper實現分佈式leader節點選舉

依賴原理

  • 在ZK中添加基本節點,路徑程序定義,節點類型爲持久節點(PERSISTENT)。
  • 對需要競選leader的每個進程,在ZK中分別添加基本節點的子節點,類型爲臨時自編號節點(EPHEMERAL_SEQUENTIAL),並保存創建返回的實際節點路徑。
  • 通過delete方式刪除本進程創建的子節點,可以作爲退出leader狀態的方式。
  • 基本節點的子節點類型爲臨時自編號節點(EPHEMERAL_SEQUENTIAL),當進程與ZK連接中斷後,ZK會自動將該節點刪除,確保了斷連之後其他進程對leader的選舉。
  • 由於ZK自編號產生的路徑是遞增的,因此可以通過判斷基本節點的子節點中最小路徑數字編號的節點是否是本進程新建的節點來判斷是否獲得leader地位。

原理圖示

利用zk實現的分佈式leader節點選舉實現原理如下:

若干進程分別嘗試競選leader,情況如下:
- (1)8個進程分別在ZK基本節點下創建臨時自編號節點,獲取創建成功後的實際路徑
- (2)在基本節點子節點列表中,判斷本進程創建節點編號是否爲最小
- (3)最小編號進程獲得leader地位
這裏寫圖片描述

leader程序異常退出或者服務器異常導致leader進程無法執行leader功能:
- (1)進程將ZK中對應的臨時節點刪除,此時基本節點下路徑最小的子節點將獲得leader地位
- (2)進程由於網絡或其他原因與ZK斷開了連接,ZK自動將其對應的臨時節點刪除
- (3)新出現的進程加入leader競選,在ZK下創建臨時節點,排隊等待
這裏寫圖片描述

方案一 :父節點監聽方式

實現原理

程序流程圖如下:

這裏寫圖片描述

實現代碼

package xuyihao.zktest.server.zk.leader;

import org.apache.zookeeper.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * 基於zk的分佈式leader節點選舉
 * <pre>
 *     方案一:父節點監聽方式
 *
 *     實現思路:監聽父節點狀態
 *     1.在父節點(持久化)下創建臨時節點,實際創建的節點路徑會根據數量進行自增(ZK自編號方式創建節點)。
 *     2.創建節點成功後,獲取父節點下的子節點列表,判斷本線程的路徑後綴編號是否是所有子節點中最小的,若是則成爲leader,反之監聽父節點變動狀態(通過getChildren()方法註冊watcher)
 *     3.當父節點狀態變動(主要是子節點列表變動)後watcher會接收到通知,這時判斷父節點下的子節點的排序狀態,若滿足本線程的路徑後綴編號最小則成爲leader,反之繼續註冊watcher監聽父節點狀態
 * </pre>
 * <p>
 * Created by xuyh at 2017/11/24 9:19.
 */
public class ZKLeader {
    private static ZKLeader zkLeader;
    private Logger logger = LoggerFactory.getLogger(ZKLeader.class);
    private final static String BASE_NODE_PATH = "/ZKLeader_Leader";
    private final static String NODE_PATH = "host_process_no_";
    private String finalNodePath;

    //是否是主節點標誌位
    private boolean leader = false;

    private String host = "127.0.0.1";
    private String port = "2181";
    private ZooKeeper zooKeeper;
    private FatherWatcher fatherWatcher;

    //是否連接成功標誌位
    private boolean connected = false;

    public static ZKLeader create(String host, String port) {
        ZKLeader zkLeader = new ZKLeader(host, port);
        zkLeader.connectZookeeper();
        return zkLeader;
    }

    public boolean leader() {
        return leader;
    }

    public void close() {
        disconnectZooKeeper();
    }

    private ZKLeader(String host, String port) {
        this.host = host;
        this.port = port;
        this.fatherWatcher = new FatherWatcher(this);
    }

    private boolean connectZookeeper() {
        try {
            zooKeeper = new ZooKeeper(host + ":" + port, 60000, event -> {
                if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                    leader = false;
                } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                    leader = false;
                } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                    leader = false;
                } else {
                    if (event.getType() == Watcher.Event.EventType.None) {//說明連接成功了
                        connected = true;
                    }
                }
            });

            int i = 1;
            while (!connected) {//等待異步連接成功,超過時間30s則退出等待
                if (i == 100)
                    break;
                Thread.sleep(300);
                i++;
            }

            if (connected) {
                if (zooKeeper.exists(BASE_NODE_PATH, false) == null) {//創建父節點
                    zooKeeper.create(BASE_NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }
                //創建子節點
                finalNodePath = zooKeeper.create(BASE_NODE_PATH + "/" + NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

                //檢查一次是否是主節點
                checkLeader();
            } else {
                logger.warn("Connect zookeeper failed. Time consumes 30 s");
                return false;
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            return false;
        }
        return true;
    }

    private boolean disconnectZooKeeper() {
        if (zooKeeper == null)
            return false;
        try {
            connected = false;
            leader = false;
            zooKeeper.close();
        } catch (Exception e) {
            logger.warn(String.format("ZK disconnect failed. [%s]", e.getMessage()), e);
        }
        return true;
    }

    private void checkLeader() {
        if (!connected)
            return;
        try {
            //獲取子節點列表同時再次註冊監聽
            List<String> childrenList = zooKeeper.getChildren(BASE_NODE_PATH, fatherWatcher);

            if (judgePathNumMin(childrenList)) {
                leader = true;
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
    }

    private boolean judgePathNumMin(List<String> paths) {
        if (paths.isEmpty())
            return true;
        if (paths.size() >= 2) {
            //對無序狀態的path列表按照編號升序排序
            paths.sort((str1, str2) -> {
                int num1;
                int num2;
                String string1 = str1.substring(NODE_PATH.length(), str1.length());
                String string2 = str2.substring(NODE_PATH.length(), str2.length());
                num1 = Integer.parseInt(string1);
                num2 = Integer.parseInt(string2);
                if (num1 > num2) {
                    return 1;
                } else if (num1 < num2) {
                    return -1;
                } else {
                    return 0;
                }
            });
        }

        String minId = paths.get(0);
        return finalNodePath.equals(BASE_NODE_PATH + "/" + minId);
    }


    private class FatherWatcher implements Watcher {
        private ZKLeader context;

        FatherWatcher(ZKLeader context) {
            this.context = context;
        }

        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeChildrenChanged) {//子節點有變化
                context.checkLeader();
            }
        }
    }
}

測試

測試程序

private void zkLeaderOneTestWithMultiThread() throws Exception {
    List<LeaderOneThread> leaderOneThreads = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        leaderOneThreads.add(new LeaderOneThread(ZKLeader.create("127.0.0.1", "2181"), i));
    }
    leaderOneThreads.forEach(LeaderOneThread::start);

    //線程0斷連
    Thread.sleep(20000);
    leaderOneThreads.get(0).getZkLeader().close();
    Thread.sleep(2000);
    System.out.println(String.format("線程: [%s] 斷開連接", 0));

    //線程1斷連
    Thread.sleep(20000);
    leaderOneThreads.get(1).getZkLeader().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 1));

    //線程3斷連
    Thread.sleep(20000);
    leaderOneThreads.get(3).getZkLeader().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 3));

    //線程4斷連
    Thread.sleep(20000);
    leaderOneThreads.get(4).getZkLeader().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 4));

    //線程2斷連
    Thread.sleep(20000);
    leaderOneThreads.get(2).getZkLeader().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 2));

    Thread.sleep(60000);
}

private class LeaderOneThread extends Thread {
    private ZKLeader zkLeader;
    private int threadNum;

    public ZKLeader getZkLeader() {
        return zkLeader;
    }

    LeaderOneThread(ZKLeader zkLeader, int threadNum) {
        this.zkLeader = zkLeader;
        this.threadNum = threadNum;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            Date dt = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String currentTime = sdf.format(dt);
            if (zkLeader.leader()) {
                System.out.println(String.format("[%s] 線程: [%s] 是主節點", currentTime, threadNum));
            }
        }
    }
}

結果:

[2017-11-30 17:05:02] 線程: [0] 是主節點
[2017-11-30 17:05:07] 線程: [0] 是主節點
[2017-11-30 17:05:12] 線程: [0] 是主節點
線程: [0] 斷開連接
[2017-11-30 17:05:22] 線程: [1] 是主節點
[2017-11-30 17:05:27] 線程: [1] 是主節點
[2017-11-30 17:05:32] 線程: [1] 是主節點
[2017-11-30 17:05:37] 線程: [1] 是主節點
線程: [1] 斷開連接
[2017-11-30 17:05:42] 線程: [2] 是主節點
[2017-11-30 17:05:47] 線程: [2] 是主節點
[2017-11-30 17:05:52] 線程: [2] 是主節點
[2017-11-30 17:05:57] 線程: [2] 是主節點
線程: [3] 斷開連接
[2017-11-30 17:06:02] 線程: [2] 是主節點
[2017-11-30 17:06:07] 線程: [2] 是主節點
[2017-11-30 17:06:12] 線程: [2] 是主節點
[2017-11-30 17:06:17] 線程: [2] 是主節點
線程: [4] 斷開連接
[2017-11-30 17:06:22] 線程: [2] 是主節點
[2017-11-30 17:06:27] 線程: [2] 是主節點
[2017-11-30 17:06:32] 線程: [2] 是主節點
[2017-11-30 17:06:37] 線程: [2] 是主節點
線程: [2] 斷開連接
[2017-11-30 17:06:42] 線程: [5] 是主節點
[2017-11-30 17:06:47] 線程: [5] 是主節點
[2017-11-30 17:06:52] 線程: [5] 是主節點
[2017-11-30 17:06:57] 線程: [5] 是主節點
[2017-11-30 17:07:02] 線程: [5] 是主節點
[2017-11-30 17:07:07] 線程: [5] 是主節點
[2017-11-30 17:07:12] 線程: [5] 是主節點
[2017-11-30 17:07:17] 線程: [5] 是主節點
[2017-11-30 17:07:22] 線程: [5] 是主節點
[2017-11-30 17:07:27] 線程: [5] 是主節點
[2017-11-30 17:07:32] 線程: [5] 是主節點
[2017-11-30 17:07:37] 線程: [5] 是主節點

方案一優劣

優點

  • 實現對父節點變動狀態(主要是子節點列表變化)的監聽
  • 當子節點列表出現變化後,ZK通知監聽的各個進程,各個進程查詢子節點狀態
  • 對父節點進行監聽,實現起來相對簡單

劣勢

  • 每個進程都監聽父節點狀態,即父節點出現變動(主要是子節點列表變化)後,ZK服務器需要通知到所有註冊監聽的進程,網絡消耗和資源浪費比較大

方案三 :子節點監聽方式

實現原理

程序流程圖如下:

這裏寫圖片描述

實現代碼

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Created by xuyh at 2017/11/30 14:40.
 * <p>
 * **最優方案**
 * <pre>
 *     方案三:子節點監聽方式
 *
 *     實現思路:監聽子節點狀態
 *     1.在父節點(持久化)下創建臨時節點,實際創建的節點路徑會根據數量進行自增(ZK自編號方式創建節點)。
 *     2.創建節點成功後,首先獲取父節點下的子節點列表,判斷本線程的路徑後綴編號是否是所有子節點中最小的,若是則成爲leader,反之監聽本節點前一個節點(路徑排序爲本節點路徑數字減一的節點)變動狀態(通過getData()方法註冊watcher)
 *     3.當監聽對象狀態變動(節點刪除狀態)後watcher會接收到通知,這時再次判斷父節點下的子節點的排序狀態,若滿足本線程的路徑後綴編號最小則成爲leader,反之繼續註冊watcher監聽前一個節點狀態
 */
public class ZKLeaderTwo {
    private static ZKLeaderTwo zkLeaderTwo;
    private Logger logger = LoggerFactory.getLogger(ZKLeader.class);
    private final static String BASE_NODE_PATH = "/ZKLeader_Leader";
    private final static String NODE_PATH = "host_process_no_";
    private String finalNodePath;

    //是否是主節點標誌位
    private boolean leader = false;

    private String host = "127.0.0.1";
    private String port = "2181";
    private ZooKeeper zooKeeper;
    private PreviousNodeWatcher previousNodeWatcher;

    //是否連接成功標誌位
    private boolean connected = false;

    public static ZKLeaderTwo create(String host, String port) {
        ZKLeaderTwo zkLeaderTwo = new ZKLeaderTwo(host, port);
        zkLeaderTwo.connectZookeeper();
        return zkLeaderTwo;
    }

    public boolean leader() {
        return leader;
    }

    public void close() {
        disconnectZooKeeper();
    }

    private ZKLeaderTwo(String host, String port) {
        this.host = host;
        this.port = port;
        this.previousNodeWatcher = new PreviousNodeWatcher(this);
    }

    private boolean connectZookeeper() {
        try {
            zooKeeper = new ZooKeeper(host + ":" + port, 60000, event -> {
                if (event.getState() == Watcher.Event.KeeperState.AuthFailed) {
                    leader = false;
                } else if (event.getState() == Watcher.Event.KeeperState.Disconnected) {
                    leader = false;
                } else if (event.getState() == Watcher.Event.KeeperState.Expired) {
                    leader = false;
                } else {
                    if (event.getType() == Watcher.Event.EventType.None) {//說明連接成功了
                        connected = true;
                    }
                }
            });

            int i = 1;
            while (!connected) {//等待異步連接成功,超過時間30s則退出等待
                if (i == 100)
                    break;
                Thread.sleep(300);
                i++;
            }

            if (connected) {
                if (zooKeeper.exists(BASE_NODE_PATH, false) == null) {//創建父節點
                    zooKeeper.create(BASE_NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }
                //創建子節點
                finalNodePath = zooKeeper.create(BASE_NODE_PATH + "/" + NODE_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

                //檢查一次是否是主節點
                checkLeader();
            } else {
                logger.warn("Connect zookeeper failed. Time consumes 30 s");
                return false;
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
            return false;
        }
        return true;
    }

    private boolean disconnectZooKeeper() {
        if (zooKeeper == null)
            return false;
        try {
            zooKeeper.close();
            connected = false;
            leader = false;
        } catch (Exception e) {
            logger.warn(String.format("ZK disconnect failed. [%s]", e.getMessage()), e);
        }
        return true;
    }

    private void checkLeader() {
        if (!connected)
            return;
        try {
            //獲取子節點列表,若沒有成爲leader,註冊監聽,監聽對象應當是比本節點路徑編號小一(或者排在前面一位)的節點
            List<String> childrenList = zooKeeper.getChildren(BASE_NODE_PATH, false);

            if (judgePathNumMin(childrenList)) {
                leader = true;//成爲leader
            } else {
                watchPreviousNode(childrenList);
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
    }

    private boolean judgePathNumMin(List<String> paths) {
        if (paths.isEmpty())
            return true;
        if (paths.size() >= 2) {
            //對無序狀態的path列表按照編號升序排序
            paths.sort((str1, str2) -> {
                int num1;
                int num2;
                String string1 = str1.substring(NODE_PATH.length(), str1.length());
                String string2 = str2.substring(NODE_PATH.length(), str2.length());
                num1 = Integer.parseInt(string1);
                num2 = Integer.parseInt(string2);
                if (num1 > num2) {
                    return 1;
                } else if (num1 < num2) {
                    return -1;
                } else {
                    return 0;
                }
            });
        }

        String minId = paths.get(0);
        return finalNodePath.equals(BASE_NODE_PATH + "/" + minId);
    }

    private void watchPreviousNode(List<String> paths) {
        if (paths.isEmpty() || paths.size() == 1) {
            return;
        }
        int currentNodeIndex = paths.indexOf(finalNodePath.substring((BASE_NODE_PATH + "/").length(), finalNodePath.length()));
        String previousNodePath = BASE_NODE_PATH + "/" + paths.get(currentNodeIndex - 1);
        //通過getData方法再次註冊watcher
        try {
            zooKeeper.getData(previousNodePath, previousNodeWatcher, new Stat());
        } catch (Exception e) {
            logger.warn(String.format("Previous node watcher register failed! message: [%s]", e.getMessage()), e);
        }
    }

    private class PreviousNodeWatcher implements Watcher {
        private ZKLeaderTwo context;

        PreviousNodeWatcher(ZKLeaderTwo context) {
            this.context = context;
        }

        @Override
        public void process(WatchedEvent event) {
            //節點被刪除了,說明這個節點放棄了leader
            if (event.getType() == Event.EventType.NodeDeleted) {
                context.checkLeader();
            }
        }
    }
}

測試

測試程序

private void zkLeaderTwoTestWithMultiThread() throws Exception {
    List<LeaderTwoThread> leaderTwoThreads = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        leaderTwoThreads.add(new LeaderTwoThread(ZKLeaderTwo.create("127.0.0.1", "2181"), i));
    }
    leaderTwoThreads.forEach(LeaderTwoThread::start);

    //線程0斷連
    Thread.sleep(20000);
    leaderTwoThreads.get(0).getZkLeaderTwo().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 0));

    //線程1斷連
    Thread.sleep(20000);
    leaderTwoThreads.get(1).getZkLeaderTwo().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 1));

    //線程3斷連
    Thread.sleep(20000);
    leaderTwoThreads.get(3).getZkLeaderTwo().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 3));

    //線程4斷連
    Thread.sleep(20000);
    leaderTwoThreads.get(4).getZkLeaderTwo().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 4));

    //線程2斷連
    Thread.sleep(20000);
    leaderTwoThreads.get(2).getZkLeaderTwo().close();
    System.out.println(String.format("線程: [%s] 斷開連接", 2));

    Thread.sleep(60000);
}

private class LeaderTwoThread extends Thread {
    private ZKLeaderTwo zkLeaderTwo;
    private int threadNum;

    public ZKLeaderTwo getZkLeaderTwo() {
        return zkLeaderTwo;
    }

    LeaderTwoThread(ZKLeaderTwo zkLeaderTwo, int threadNum) {
        this.zkLeaderTwo = zkLeaderTwo;
        this.threadNum = threadNum;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            Date dt = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String currentTime = sdf.format(dt);
            if (zkLeaderTwo.leader()) {
                System.out.println(String.format("[%s] 線程: [%s] 是主節點", currentTime, threadNum));
            }
        }
    }
}

結果:

[2017-11-30 16:47:41] 線程: [0] 是主節點
[2017-11-30 16:47:46] 線程: [0] 是主節點
[2017-11-30 16:47:51] 線程: [0] 是主節點
[2017-11-30 16:47:56] 線程: [0] 是主節點
線程: [0] 斷開連接
[2017-11-30 16:48:01] 線程: [1] 是主節點
[2017-11-30 16:48:06] 線程: [1] 是主節點
[2017-11-30 16:48:11] 線程: [1] 是主節點
[2017-11-30 16:48:16] 線程: [1] 是主節點
線程: [1] 斷開連接
[2017-11-30 16:48:21] 線程: [2] 是主節點
[2017-11-30 16:48:26] 線程: [2] 是主節點
[2017-11-30 16:48:31] 線程: [2] 是主節點
[2017-11-30 16:48:36] 線程: [2] 是主節點
線程: [3] 斷開連接
[2017-11-30 16:48:41] 線程: [2] 是主節點
[2017-11-30 16:48:46] 線程: [2] 是主節點
[2017-11-30 16:48:51] 線程: [2] 是主節點
[2017-11-30 16:48:56] 線程: [2] 是主節點
線程: [4] 斷開連接
[2017-11-30 16:49:01] 線程: [2] 是主節點
[2017-11-30 16:49:06] 線程: [2] 是主節點
[2017-11-30 16:49:11] 線程: [2] 是主節點
[2017-11-30 16:49:16] 線程: [2] 是主節點
線程: [2] 斷開連接
[2017-11-30 16:49:21] 線程: [5] 是主節點
[2017-11-30 16:49:26] 線程: [5] 是主節點
[2017-11-30 16:49:31] 線程: [5] 是主節點
[2017-11-30 16:49:36] 線程: [5] 是主節點
[2017-11-30 16:49:41] 線程: [5] 是主節點
[2017-11-30 16:49:46] 線程: [5] 是主節點
[2017-11-30 16:49:51] 線程: [5] 是主節點
[2017-11-30 16:49:56] 線程: [5] 是主節點
[2017-11-30 16:50:01] 線程: [5] 是主節點
[2017-11-30 16:50:06] 線程: [5] 是主節點
[2017-11-30 16:50:11] 線程: [5] 是主節點
[2017-11-30 16:50:16] 線程: [5] 是主節點

方案二優劣

優點

  • 實現對子節點變動狀態(排序在本進程對應節點之前的一個節點)的監聽
  • 被監聽子節點變動(刪除)之後,ZK通知本進程執行相應操作,判斷是否成爲leader
  • 相對於父節點監聽方式,子節點監聽方式在每一次鎖釋放(或者節點變動)時,ZK僅通知到一個進程的watcher,節省了大量的網絡消耗和資源佔用

劣勢

  • 實現方式與程序邏輯較父節點監聽來說比較繁瑣

總結比較

  • 程序複雜度:
    父節點監聽方式 < 子節點監聽方式

  • 網絡資源消耗:
    父節點監聽方式 >> 子節點監聽方式

  • 程序可靠性
    父節點監聽方式 < 子節點監聽方式

輕量Zookeeper客戶端實現

github地址:

https://github.com/johnsonmoon/zk-client

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章