个人博客原文地址:http://www.ltang.me/2016/08/06/run-for-master-with-zookeeper-md/
为了提高系统的健壮性,我们常常做出多节点负载均衡的设计,通过zookeeper注册和发现可用服务,调用端通过一定的负载均衡策略决定请求哪一个可用服务节点。
然后,在某些情况下,服务的调用并非由客户端发起,而是由这个服务自身发起。比如,一个服务可能存在一些定时任务,每分钟去操作一下数据库之类的。当系统只有一个容器时,不用考虑主从的问题,只管到时间了就执行。但如果系统是分布式的,一个服务可能同时运行在多个容器中,查询类的定时任务没有影响,但是某些定时任务每次只需要执行一次,没有区分主从的情况下,每个容器下的服务都会企图去执行,很可能会造成不可预料的结果。
所以,我们需要达到的目标是,服务能够判断自己是否是Master,如果是,则执行,如果不是,则不执行。同时,如果Master服务掉线(比如宕机了),那么某个容器里的slave服务能够自动升级为Master,并执行Master执行的任务。
/gzcb/master/services
,此节点下的子节点为临时节点,分别代表不同的Master服务/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;/gzcb/master/services/AccountService:1.0.0
,但是会创建失败,返回结果码显示该节点已经存在。所以服务就知道已经有一个Master的AccountService(1.0.0)存在,它竞选失败。不管竞选成功还是失败,可以维护一份Master缓存信息,并保持监听,实时更新。这样,不仅能够自动竞选master,还能够通过修改临时节点数据的方式,手动指定Master。
/**
* 竞选Master
*
* /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节点。
最后,我选择了第2种方案。其实都是可行的Master竞选方式。
以上。