声明:此博客为学习笔记,学习自极客学院ZooKeeper相关视频;本文内容是本人照着视频里的前辈所讲知识敲了
一遍的记录,个别地方按照本人理解稍作修改。非常感谢众多大牛们的知识分享。
相关概念:
程序主题流程:
说明:创建master不成功,说明master已存在;
说明:读取master数据时失败,可能刚好在读取数据那一刻,master临时节点被删除了。
注:集群中每个zookeeper服务器启动时,都会走上述流程。
应对网络抖动:
说明:如果发生网络抖动,导致集群中的服务器A创建的master的节点被删除;那么最好保证下次创建master节点的,
仍然是服务器A;
这是因为:当Leader由A变为B时,就相当于发生了资源的迁移,而资源的迁移一般会导致一些不必要的系统开
销;所以我们对于资源的迁移能避免就尽量避免。
群首选举的实现示例:
软硬件环境:Windows10、IntelliJ IDEA、SpringBoot、ZkClient客户端
准备工作:引入zkclient依赖
<!--
https://mvnrepository.com/artifact/com.101tec/zkclient
ZkClient客户端所需依赖
-->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
相关类总体说明:
WorkerServerParamModel:集群中的每个服务器的属性参数。
WorkerServer:集群中的每个服务器的群首选举的实际逻辑封装类。
注:我们也可以将WorkerServerParamModel中的属性直接写进WorkerServer里,这样就不需要WorkerServerParamModel
模型了。
MasterSelectTest:群首选举测试类。
给个各个类的实现细节:
WorkerServerParamModel:
/**
* 服务器模型
* 注:此模型主要用于记录 服务器的基本信息
* 注:由于此信息要被写入节点,所以需要能够序列化,即:需要实现Serializable功能性接口
*
* @author JustryDeng
* @date 2018/11/23 15:42
*/
public class WorkerServerParamModel implements Serializable {
private static final long serialVersionUID = -4983450409669715087L;
/** 服务器id */
private Long serverId;
/** 服务器名字 */
private String serverName;
public Long getServerId() {
return serverId;
}
public void setServerId(Long serverId) {
this.serverId = serverId;
}
public String getServerName() {
return serverName;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
@Override
public String toString() {
return "WorkerServerParamModel{serverId=" + serverId + ", serverName='" + serverName + "'}";
}
}
WorkerServer:
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkException;
import org.I0Itec.zkclient.exception.ZkInterruptedException;
import org.I0Itec.zkclient.exception.ZkNoNodeException;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import org.apache.zookeeper.CreateMode;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 集群中的每一个服务器 启动时,都需要做这些事
* 注:监听master,并保证群首存在
* TODO 去调所有的System.out.println
*
* @author JustryDeng
* @date 2018/11/23 16:19
*/
public class WorkerServer {
/** master临时节点 */
private final String MASTER_NODE_PATH = "/master";
/** 主节点服务器(即:群首) */
private WorkerServerParamModel masterServerParam;
/** 当前服务器 */
private WorkerServerParamModel currentServerParam;
/** 当前服务器是否处于运行中(默认为false) */
private boolean currentServerIsRunning = false;
/** 监听器 */
private IZkDataListener iZkDataListener;
/**
* 应对网络抖动的实现
* 这里使用:定时计划线程池
*/
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
private final int DELAY_TIME = 2;
/** zkClient客户端 */
private ZkClient zkClient;
public ZkClient getZkClient() {
return zkClient;
}
public void setZkClient(ZkClient zkClient) {
this.zkClient = zkClient;
}
public WorkerServer(WorkerServerParamModel currentServerParam, ZkClient zkClient) {
this.zkClient = zkClient;
this.currentServerParam = currentServerParam;
iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
System.out.println(currentServerParam.getServerName() + "监听到目标节点的数据发生了变化!");
//do something即可
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println(currentServerParam.getServerName() + "监听到目标节点被删除了!");
// 如果前一次的群首是自己,那么立即 开始 争抢群首
if (masterServerParam != null && masterServerParam.getServerId().equals(currentServerParam.getServerId())){
tryCreateMasterNode();
} else { // 如果前一次的群首不是自己,那么延迟一定时间后才开始 争抢群首
scheduledExecutorService.schedule(() -> tryCreateMasterNode(), 1, TimeUnit.SECONDS);
}
}
};
}
/**
* 试着去 创建master临时主节点(即:争抢群首)
*
* @author JustryDeng
* @date 2018/11/26 21:53
*/
private void tryCreateMasterNode() {
// 如果当前服务器没有运行着,那么不进行下面的逻辑
if (!currentServerIsRunning) {
return;
}
try {
System.out.println(currentServerParam.getServerName() + "试着去创建master临时主节点!");
// 创建临时主节点, 该节点的数据 要有唯一性(以便后面的区分)
String path = zkClient.create(MASTER_NODE_PATH, currentServerParam, CreateMode.EPHEMERAL);
System.out.println(currentServerParam.getServerName() + "创建" + path + "临时主节点成功!");
//创建成功后,将当前服务器数据赋值给masterServer
masterServerParam = currentServerParam;
System.out.println(masterServerParam.getServerName() + " is master! this server id is "
+ masterServerParam.getServerId());
} catch (ZkNodeExistsException e) { // 创建时,如果出现master节点存在异常,那么试着读取主节点数据
// 第二个参数设为true,当节点不存在时,返回null
WorkerServerParamModel runningServer = zkClient.readData(MASTER_NODE_PATH, true);
if (runningServer == null) { // 当此时主节点不存在时,重试
System.out.println("获取主节点数据为null,则此时主节点不存在时,重试tryCreateMasterNode方法");
// 注:可能出现这种情况:上一步create时,主节点还在;而在下一步readData时,主节点就不在了
tryCreateMasterNode();
} else {
masterServerParam = runningServer;
}
} catch (ZkInterruptedException e) { // 出现打断异常时重试
System.out.println("tryCreateMasterNode()发生ZkInterruptedException异常了!");
tryCreateMasterNode();
} catch (Exception e) {
System.out.println("tryCreateMasterNode()发生Exception异常了!");
}
}
/**
* 判断当前节点是否是master主节点
*
* @author JustryDeng
* @date 2018/11/27 14:59
*/
private boolean currentServerIsMasterServer() {
try {
WorkerServerParamModel runningServer = zkClient.readData(MASTER_NODE_PATH);
masterServerParam = runningServer;
return currentServerParam.getServerId().equals(runningServer.getServerId());
} catch (ZkNoNodeException e) {
return false;
} catch (ZkInterruptedException e) { // 被打断了就进行重试
return currentServerIsMasterServer();
} catch (ZkException e) {
return false;
}
}
/**
* 如果自己是群首,那么删除主节点
* 注:由于该节点是一个没有子节点的节点,所以使用delete或deleteRecursive都行;
*
* @author JustryDeng
* @date 2018/11/27 14:59
*/
private void deleteMasterNode() {
// 主节点最好是被对应的服务器删除,在实际使用时可考虑加上ACL做限制
if (currentServerIsMasterServer()) {
zkClient.delete(MASTER_NODE_PATH);
}
}
/**
* 对外暴露的启动方法
*
* @author JustryDeng
* @date 2018/11/27 15:12
*/
public void start() {
if (currentServerIsRunning) {
System.out.println("当前服务已经在跑着了!");
return;
}
currentServerIsRunning = true;
// 监听主节点
zkClient.subscribeDataChanges(MASTER_NODE_PATH, iZkDataListener);
tryCreateMasterNode();
}
/**
* 对外暴露的停止方法
*
* @author JustryDeng
* @date 2018/11/27 15:12
*/
public void stop() {
if (!currentServerIsRunning) {
System.out.println("当前服务已经停止了!");
return;
}
// 取消监听
zkClient.unsubscribeDataChanges(MASTER_NODE_PATH, iZkDataListener);
currentServerIsRunning = false;
// 删除主节点
deleteMasterNode();
// 关闭线程池
scheduledExecutorService.shutdown();
}
}
MasterSelectTest:
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkInterruptedException;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 群首选举 --- 测试
*
* @author JustryDeng
* @date 2018/11/27 15:30
*/
public class MasterSelectTest {
private static final int SIZE = 10;
private static final String IP_PORT = "10.8.109.60:2181";
private static final int SESSION_TIMEOUT = 10000;
private static final int CONNECT_TIMEOUT = 100000;
private static List<ZkClient> zkClients = new ArrayList<>(16);
private static List<WorkerServer> workerServers = new ArrayList<>(16);
/**
* 程序入口
*/
public static void main(String[] args) throws InterruptedException {
try {
zkClients.clear();
workerServers.clear();
for (int i = 0; i < SIZE; i++){
// 创建WorkerServer
WorkerServerParamModel wsParamModel = new WorkerServerParamModel();
wsParamModel.setServerId((long)i);
wsParamModel.setServerName("worker" + i);
ZkClient zkClient = new ZkClient(IP_PORT, SESSION_TIMEOUT, CONNECT_TIMEOUT, new SerializableSerializer());
zkClients.add(zkClient);
WorkerServer workerServer = new WorkerServer(wsParamModel, zkClient);
workerServers.add(workerServer);
// 启动zookeeper的某个客户端
workerServer.start();
}
} finally {
System.out.println("开始退出!");
try {
WorkerServer workerServer;
for (int i = 0; i < workerServers.size(); i++) {
workerServer = workerServers.get(i);
workerServer.stop();
// 这里线程阻塞几秒钟,以便观察 抢占详情
TimeUnit.SECONDS.sleep(5);
}
for (ZkClient zkClient :zkClients) {
zkClient.close();
}
} catch (ZkInterruptedException e) {
e.printStackTrace();
}
}
}
}
启动对应的ZooKeeper服务器,开放端口(或关闭防火墙),然后运行上述主函数,控制台输出(只截取了部分):
所有zookeeper示例内容有(代码链接见本人末):