簡單使用 ZooKeeper 實現集羣主備切換

昨天晚上看了一篇博客,作者實現了一個分佈式的調度框架,其中支持兩種集羣模式,其中一種就是主備模式,是基於 ZooKeeper 實現的,這也是 ZooKeeper 很常見的應用場景,還沒來得及看具體細節就去處理了一個線上問題,今天一直找不到那個博客鏈接。今天就嘗試自己實現一下,本文會介紹兩種實現方式(總體思想一致,部分細節有所差別)。

這種主備模式首先需要從集羣中選出 Master 節點,然後剩餘節點 Standby,當 Master 節點掛掉之後,剩餘節點爭搶或者根據一定的策略指定出 Master 節點,還有一個特點是這種模式幹活的只有 Master 節點。

本文使用 Curator 操作 ZooKeeper,引入相關依賴:

<dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.0.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.0</version>
        </dependency>

環境準備:

好久沒用 ZooKeeper 了,之前環境配置的都忘記了,還好不算折騰,這裏簡單記錄下:

啓動 ZooKeeper:

[root@localhost bin]# pwd
/usr/local/develope/zookeeper/zookeeper/bin
[root@localhost bin]# sh zkServer.sh start 

客戶端連接:

[root@localhost bin]# sh zkCli.sh -server 127.0.0.1:2181

發現一直不停輸出:

Unable to read additional data from server sessionid 0x0

這是因爲我之前配置的是三臺 ZooKeeper 集羣,我這裏只啓動了一臺機器,最少要啓動半數以上的機器,所以要改一下 zoo.cfg 文件:

[root@localhost bin]# cd ..
[root@localhost bin]# cd conf
[root@localhost bin]# vim zoo.cfg 

這裏只配置一個即可:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8efFySDV-1588688548208)(./1.png)]

重新啓動 ZooKeeper 服務,再進入客戶端:

[root@localhost bin]# sh zkServer.sh stop 
[root@localhost bin]# sh zkServer.sh start
[root@localhost bin]# sh zkCli.sh -server 127.0.0.1:2181
[zk: 127.0.0.1:2181(CONNECTED) 0] ls
[zk: 127.0.0.1:2181(CONNECTED) 1] ls /
[zookeeper]

配置類:

@Configuration
public class ZooKeeperConfiguration {

    private static final String ZOOKEEPER_URL = "192.168.43.6:2181";

    @Bean
    public CuratorFramework getCuratorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
        CuratorFramework client = CuratorFrameworkFactory.newClient(ZOOKEEPER_URL,retryPolicy);
        client.start();
        return client;
    }
}

第一種實現

這種實現方式就是基於 ZooKeeper 的臨時節點,集羣每個節點啓動的時候都會去創建一個臨時節點,創建成功了的節點被選爲 Master,然後其餘節點監控這個臨時節點即可。

搶佔 Master:

package com.example.simplespringboot.configuration;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.zookeeper.CreateMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-05-05 18:54
 */
@Component
public class MasterRegister implements CommandLineRunner {

    private static final String ROOT_PATH = "/test";

    private static final Long WAIT_SECONDS = 3L;

    public volatile boolean master = false;

    @Autowired
    private CuratorFramework zkClient;

    @Value("${server.port}")
    private String port;

    @Override
    public void run(String... args) throws Exception {
        //Spring 容器啓動後創建臨時節點
        //由於在ZooKeeper中規定了所有非葉子節點必須爲持久節點,調用上面這個API之後,只有path參數對應的數據節點是臨時節點,其父節點均爲持久節點
        regist();
        PathChildrenCache childrenCache = new PathChildrenCache(zkClient, ROOT_PATH , true);
        childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
        // 節點數據change事件的通知方法
        childrenCache.getListenable().addListener((curatorFramework, event) -> {
            if(event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)){
                System.out.println("節點變更,開始重新選舉");
                //todo:防止腦裂/防止網絡抖動/數據同步
                try {
                    TimeUnit.SECONDS.sleep(WAIT_SECONDS);
                } catch (InterruptedException ignored) {
                }
                regist();
            }
        });

    }

    private void regist() {
        System.out.printf("機器【%s】開始搶佔 Master", port);
        System.out.println();
        try {
            zkClient.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .forPath(ROOT_PATH + "/master", port.getBytes());
            System.out.printf("機器【%s】成爲了 Master", port);
            System.out.println();
            master = true;
        } catch (Exception e) {
            System.out.printf("機器【%s】搶佔 Master 失敗", port);
            System.out.println();
            master = false;
        }
    }
}

Controller:

@RestController
@RequestMapping("point")
public class PointController {

    @Autowired
    private MasterRegister masterRegister;

    @RequestMapping("master")
    public boolean isMaster() {
        return masterRegister.master;
    }
}

將項目打包,分別以不同的端口號 8080、8081、8082 啓動:

➜  simple-spring-boot mvn clean package -Dmaven.test.skip=true
➜  ~ java -jar -Dserver.port=8080 /Users/dongguabai/Desktop/temp/simple-spring-boot/target/simple-spring-boot-0.0.1-SNAPSHOT.jar
➜  ~ java -jar -Dserver.port=8081 /Users/dongguabai/Desktop/temp/simple-spring-boot/target/simple-spring-boot-0.0.1-SNAPSHOT.jar
➜  ~ java -jar -Dserver.port=8082 /Users/dongguabai/Desktop/temp/simple-spring-boot/target/simple-spring-boot-0.0.1-SNAPSHOT.jar

控制檯輸出:

機器【8080】開始搶佔 Master
機器【8080】成爲了 Master

機器【8082】開始搶佔 Master
機器【8082】搶佔 Master 失敗

機器【8081】開始搶佔 Master
機器【8081】搶佔 Master 失敗

查看 ZooKeeper 節點情況:

[zk: 127.0.0.1:2181(CONNECTED) 10] ls /
[zookeeper, test]
[zk: 127.0.0.1:2181(CONNECTED) 11] ls /test
[master]
[zk: 127.0.0.1:2181(CONNECTED) 12] get /test/master
8080
cZxid = 0x19
ctime = Tue May 05 05:01:20 PDT 2020
mZxid = 0x19
mtime = Tue May 05 05:01:20 PDT 2020
pZxid = 0x19
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x1000011f3160007
dataLength = 4
numChildren = 0
[zk: 127.0.0.1:2181(CONNECTED) 13] 

當前 Master 節點所在進程的端口號爲 8080。

接下來關閉 8080 端口所在的進程,8081 和 8082 控制檯分別輸出:

節點變更,開始重新選舉
機器【8081】開始搶佔 Master
機器【8081】成爲了 Master

節點變更,開始重新選舉
機器【8082】開始搶佔 Master
機器【8082】搶佔 Master 失敗

再看 ZooKeeper 節點信息:

[zk: 127.0.0.1:2181(CONNECTED) 54] get /test/master
8081
cZxid = 0x75
ctime = Tue May 05 05:36:50 PDT 2020
mZxid = 0x75
mtime = Tue May 05 05:36:50 PDT 2020
pZxid = 0x75
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x1000011f3160020
dataLength = 4
numChildren = 0

可以看到當前 Master 節點所在進程的端口號爲 8081。

第二種實現方式

第一種實現方式有一個問題就是會產生“驚羣效應”,在併發量較高的情況下,也就是說短時間之內會有大量的客戶端去爭搶註冊爲 Master,短時間內會發生大量的事件上下文變更,但是實際上只有一個客戶端可以註冊得到,相當於出現了大量的無效的系統調度、上下文切換,系統系能大打折扣。爲了解決這個問題,可以利用 ZooKeeper 中有序節點的特性。系統啓動會在 /test/master 下注冊臨時有序節點,根據一定的策略去選定 Master 即可。

註冊:

package com.example.simplespringboot.configuration;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.zookeeper.CreateMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-05-05 18:54
 */
@Component
public class MasterRegister2 implements CommandLineRunner {

    private static final String ROOT_PATH = "/test";

    private static final Long WAIT_SECONDS = 3L;

    public volatile boolean master = false;

    public String masterPort;

    @Autowired
    private CuratorFramework zkClient;

    @Value("${server.port}")
    private String port;

    @Override
    public void run(String... args) throws Exception {
        //Spring 容器啓動後創建臨時節點
        regist();
    }

    private void regist() {
        try {
            zkClient.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(ROOT_PATH + "/master", port.getBytes());
        } catch (Exception ignored) {
        }
    }
}

將項目打包,分別以不同的端口號 8080、8081、8082 啓動:

➜  simple-spring-boot mvn clean package -Dmaven.test.skip=true~ java -jar -Dserver.port=8080 /Users/dongguabai/Desktop/temp/simple-spring-boot/target/simple-spring-boot-0.0.1-SNAPSHOT.jar
➜  ~ java -jar -Dserver.port=8081 /Users/dongguabai/Desktop/temp/simple-spring-boot/target/simple-spring-boot-0.0.1-SNAPSHOT.jar
➜  ~ java -jar -Dserver.port=8082 /Users/dongguabai/Desktop/temp/simple-spring-boot/target/simple-spring-boot-0.0.1-SNAPSHOT.jar

查看 ZooKeeper 節點:

[zk: 127.0.0.1:2181(CONNECTED) 57] ls /test
[master0000000019, master0000000017, master0000000018]

調用:

package com.example.simplespringboot.controller;

import com.example.simplespringboot.configuration.MasterRegister;
import com.example.simplespringboot.configuration.MasterRegister2;
import org.apache.curator.framework.CuratorFramework;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Comparator;
import java.util.List;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-05-05 19:44
 */
@RestController
@RequestMapping("point")
public class PointController {

    @Autowired
    private CuratorFramework zkClient;

    @RequestMapping("master")
    public Object isMaster() throws Exception {
        //todo 緩存優化-監聽變化
        List<String> list = zkClient.getChildren().forPath("/test");
        String s = list.stream().sorted(String.CASE_INSENSITIVE_ORDER).findFirst().get();
        return s;
    }
}

響應結果爲:

master0000000017

關閉最早註冊的機器後,再次訪問,響應結果爲:

master0000000018

References

  • https://blog.csdn.net/qq_39833418/article/details/78316898
  • https://blog.csdn.net/Dongguabai/article/details/82901686

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

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