昨天晚上看了一篇博客,作者實現了一個分佈式的調度框架,其中支持兩種集羣模式,其中一種就是主備模式,是基於 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
這裏只配置一個即可:
重新啓動 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
歡迎關注公衆號