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

个人博客原文地址:http://www.ltang.me/2016/08/06/run-for-master-with-zookeeper-md/

背景

为了提高系统的健壮性,我们常常做出多节点负载均衡的设计,通过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。

关键代码

/**
* 竞选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竞选方式。

以上。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值