发布于 

通过zookeeper实现主从(Master-slave)竞选

背景

为了提高系统的健壮性,我们常常做出多节点负载均衡的设计,通过zookeeper注册和发现可用服务,调用端通过一定的负载均衡策略决定请求哪一个可用服务节点。

然后,在某些情况下,服务的调用并非由客户端发起,而是由这个服务自身发起。比如,一个服务可能存在一些定时任务,每分钟去操作一下数据库之类的。当系统只有一个容器时,不用考虑主从的问题,只管到时间了就执行。但如果系统是分布式的,一个服务可能同时运行在多个容器中,查询类的定时任务没有影响,但是某些定时任务每次只需要执行一次,没有区分主从的情况下,每个容器下的服务都会企图去执行,很可能会造成不可预料的结果。

所以,我们需要达到的目标是,服务能够判断自己是否是Master,如果是,则执行,如果不是,则不执行。同时,如果Master服务掉线(比如宕机了),那么某个容器里的slave服务能够自动升级为Master,并执行Master执行的任务。

基础

  • Zookeeper客户端可以创建临时节点并保持长连接,当客户端断开连接时,临时节点会被删除
  • Zookeeper客户端可以监听节点变化

实现

  1. 定义一个持久化节点/gzcb/master/services,此节点下的子节点为临时节点,分别代表不同的Master服务
  2. Container_1中的服务AccountService,在启动时,在zookeeper中创建临时节点/gzcb/master/services/AccountService:1.0.0,节点的数据为192.168.99.100:9090。这代表,192.168.99.100:9090这个容器中的AccountService(版本为1.0.0)成功竞选为Master服务。Container_1中维护一个缓存,如果竞选成功,对应service:version置为true,否则置为false;
  3. Container_2中的服务AccountService,在启动时,也试图创建临时节点/gzcb/master/services/AccountService:1.0.0,但是会创建失败,返回结果码显示该节点已经存在。所以服务就知道已经有一个Master的AccountService(1.0.0)存在,它竞选失败。
  4. Container_2会保持对该临时节点的监听,如果监听到该零时节点被删除,则试图再次创建(创建临时节点的过程就是竞选master的过程),创建成功,则更新缓存对应service:version为true,否则继续保持监听。

优化

不管竞选成功还是失败,可以维护一份Master缓存信息,并保持监听,实时更新。这样,不仅能够自动竞选master,还能够通过修改临时节点数据的方式,手动指定Master。

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* 竞选Master
* <p/>
* /gzcb/master/services/**.**.**.AccountService:1.0.0 data [192.168.99.100:9090]
*/
public void runForMaster(String key) {
zk.create(PATH + key, currentContainerAddr.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL, masterCreateCb, key);
}

private AsyncCallback.StringCallback masterCreateCb = (rc, path, ctx, name) -> {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
//检查master状态
checkMaster((String) ctx);
break;
case OK:
//被选为master
isMaster.put((String) ctx, true);
LOGGER.info("{}竞选master成功, data为[{}]", (String) ctx, currentContainerAddr);
break;
case NODEEXISTS:
//master节点上已存在相同的service:version,自己没选上
isMaster.put((String) ctx, false);
LOGGER.info("{}竞选master失败, data为[{}]", (String) ctx, currentContainerAddr);
//保持监听
masterExists((String) ctx);
break;
case NONODE:
LOGGER.error("{}的父节点不存在,创建失败", path);
break;
default:
LOGGER.error("创建{}异常:{}", path, KeeperException.Code.get(rc));
}
};

/**
* 监听master是否存在
*/
private void masterExists(String key) {

zk.exists(PATH + key, event -> {
//若master节点已被删除,则竞争master
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
String serviceKey = event.getPath().replace(PATH, "");
runForMaster(serviceKey);
}

}, (rc, path, ctx, stat) -> {

switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
masterExists((String) ctx);
break;
case NONODE:
runForMaster((String) ctx);
break;
case OK:
if (stat == null) {
runForMaster((String) ctx);
} else {
checkMaster((String) ctx);
}
break;
default:
checkMaster((String) ctx);
break;
}

}, key);
}

/**
* 检查master
*
* @param serviceKey
*/
private void checkMaster(String serviceKey) {

zk.getData(PATH + serviceKey, false, (rc, path, ctx, data, stat) -> {
switch (KeeperException.Code.get(rc)) {
case CONNECTIONLOSS:
checkMaster((String) ctx);
return;
case NONODE: // 没有master节点存在,则尝试获取领导权
runForMaster((String) ctx);
return;
case OK:
String value = new String(data);
if (value.equals(currentContainerAddr))
isMaster.put((String) ctx, true);
else
isMaster.put((String) ctx, false);
return;
}

}, serviceKey);
}

问题

上面的代码,看起来似乎没什么问题,但是,仔细梳理一下流程,再结合zookeeper的应用,发现有个隐藏的bug。

当当当当,仔细思考。

我在上面的代码中,当服务去创建临时节点时,如果节点存在,就会去拿节点里面的数据,将数据与自身的ip端口对比,如果一致,则认为自己是主节点,否则,认为自己是从节点。一般情况下,不会有问题啦,但是,加入主节点重启呢?并且重启时间非常短呢?

由于zookeeper的心跳包间隔问题,当主服务节点重启时,要大约10秒后,其他节点才会收到主服务节点创建的临时节点被删除的消息,这时候某个从节点成功竞选上了master。然而这10秒内发生了什么呢,原本的主服务节点创建创建节点失败,然后去检查临时节点的值,发现临时节点上的值(此时临时节点还未删除)与自己本地的ip+端口一致,就认为自己是master节点了…这时候,系统中就会存在两个Master节点。

解决方案

  1. 不根据临时节点的值来判断自己是否主节点,仅仅把是否成功创建节点作为是否master的依据。创建成功则是Master,失败则不是,并保持监听。这样仅仅需要做小小的修改,且代码更简单了。
  2. 利用zookeeper的临时有序节点的特性,每次创建临时节点时,判断自己是否有序节点中最小的那个,如果是,那么自己就是master,如果不是,那么当然自己就不是master。并且保持对最小节点的监听,一旦最小节点被删除,就去判断自己是否最小…

最后,我选择了第2种方案。其实都是可行的Master竞选方式。

以上。