zookeeper实现leader选举的一种方法

写这篇文章之前,我需要解释下为什么需要实现leader选举。
我们在软件系统构建过程中,总会有一个场景,就是如何保证系统高可用。保证高可用有一个简单方式就是多加几个副本,也就是部署多个节点,构成一个集群。一台机器挂掉,业务由另一台接管。在一个时刻只有一台生效,这个生效的机器就是Leader。类比于一个部门只有一个主管一样,主管不在了,副职可以立马顶上,不至于活都没法干。 这里的关键一点是在一个时间点,只有Leader可以处理业务。需要这个特性的场景,在软件系统中是有很多的。
比如有一个出账模块,每天凌晨1:00定时跑。为了解决单点故障,生成环境可能需要部署多套,但是不能都跑,否则统计数据不正确。所以需要只有一台能顺利跑,其它处于standby状态。除非leader机器挂掉,剩余机器中重新选一个leader出来跑。
不夸张的说,在这样一个分布式环境下完成leader选举是一个非常麻烦的事情。远没有需求分析中形如一段话:“完成leader选举”,或者设计文档中一个箭头那么简单。
这里面存在如下难题:
1.系统怎么及时知道leader挂掉了?
2.系统按照什么规则确定leader?
3.如果防范多leader出现?比如原来的leader还在跑,其余机器认为leader挂掉了,重新发起选举,从而有两个leader出现。
要解决以上问题,从零开始实现肯定是费力不讨好。高成本低回报的事情我们不能做。古人告诉我们要站在巨人的肩上。前辈告诉我们不要重复造轮子。所以借助zookeeper实现是明智正确的选择。

zookeeper的详细知识点我这里就不普及了,网上相关资料很多。毕竟这个分布式协调服务功能强大,它也是hadoop的重要组件。大体涉及到分布式架构相关的都会和它有点关系。
zookeeper中节点的创建类型有4种,这里我们重点关注临时顺序节点(EphemeralSequential)。此类型节点有以下几个特性:
1.节点的生命周期和客户端会话绑定,即创建节点的客户端会话一旦失效,那么这个节点也会被清除。
2.每个父节点都会负责维护其子节点创建的先后顺序,并且如果创建的是顺序节点(SEQUENTIAL)的话,父节点会自动为这个节点分配一个整形数值,以后缀的形式自动追加到节点名中,作为这个节点最终的节点名。追加的方式为10位数字,左边补0。
利用上面的特性,我们可以设计出实现leader选举的流程如下:
1.创建一个节点znode,比如“/election”,然后再创建子节点"/election/S_"为临时顺序节点。
2.规定最小后缀序号的znode为leader.
3.其它znode都监视最小节点,也就是订阅数据变更事件。当leader节点失效时,能收到通知。
4.收到通知时,调用getChildren()获取"/election"下的子节点列表,最小的那个节点就为leader.
上述流程编码实现是可行的,不过可以进一步优化。我们发现,当节点很多的时候,假设为N个节点。当leader失效时,余下的N-1个节点都收到了通知,都需要执行getChildren()获取子节点列表。也就是说这个影响范围太大了,对zookeeper也构成压力。有一个词叫羊群效应(herd effect)形容的很恰当。也就是说leader失效了,znode2去查询,znode3..N也去查询,但是znode3..N是否必要呢?
由于前面已经有明确的指定,最小序号的为leader.也就是说如果znode1和znode2都存活,那leader一定为znode1,当znode1失效后,znode2肯定就为leader,当然前提条件是znode2有效。
所以步骤3做一个调整,不再是都监视最小节点,而是监视比自己小的还活着的下一个节点。
写的形式化一点:
当前节点为"/election/S_i",监视节点"/election/S_j"的改变,j<i && 不存在节点k满足j<k<i.

这里还有一个问题,就是第一个节点进来的时候,它怎么知道它就是leader.所以我们还设计了一个节点“/election_leader. 节点进来的时候看节点election_leader是否存在,如果没有,不用犹豫,直接创建一个,把自己的node序号写进去。标识我就是leader.后续节点再进来时候一看,election_leader已经有了,同时read看一下,就知道当前leader是谁了。然后订阅比自己小的那个节点数据变更事件。

纸上得来终觉浅,程序员这时候要不耐烦了,Talk is cheap,show me the code.
下面亮干货。
java中zookeeper封装的比较好的库有两个,com.101tec.zkclient和org.apache.curator。

我采用前者。

package cn.superv.demo.zk;

import cn.superv.demo.zk.common.Assert;
import cn.superv.demo.zk.common.HerdEffectListener;
import cn.superv.demo.zk.impl.ZkclientZookeeperClient;
import cn.superv.demo.zk.impl.ZkclientZookeeperTransporter;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.remoting.zookeeper.ChildListener;
import com.alibaba.dubbo.remoting.zookeeper.StateListener;
import com.alibaba.dubbo.remoting.zookeeper.ZookeeperClient;
import com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter;
import org.I0Itec.zkclient.IZkDataListener;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;

/**
 * Created by jpzhang on 2018/4/17
 * Description:{zookeeper leader选举}
 */
public class ZkLeader {
    private static Logger logger = LogManager.getLogger();

    public static void main(String[] args) throws IOException {
        ZookeeperTransporter transporter = new ZkclientZookeeperTransporter();
        logger.info("zktransporter connect.");
        final ZkclientZookeeperClient zookeeperClient = (ZkclientZookeeperClient) transporter.connect(URL.valueOf("zookeeper://127.0.0.1:2181"));

        zookeeperClient.addStateListener(new StateListener() {
            @Override
            public void stateChanged(int connected) {
//                logger.info("connected={}", connected);
            }
        });

        List<String> currentData = zookeeperClient.addChildListener("/election", new ChildListener() {
            @Override
            public void childChanged(String path, List<String> children) {
//                logger.info("获取路径:{},下面包含如下节点:{}", path, Arrays.toString(children.toArray(new String[0])));
            }
        });
        logger.info("当前节点内容:{}", currentData.toArray());
        final String electionNode = "/election";
        final String electionLeaderNode = "/election_leader";

//        zookeeperClient.createEphemeral("/election/1");
//        zookeeperClient.createEphemeral("/election/2");
//        zookeeperClient.createEphemeral("/election/3");
//        zookeeperClient.createEphemeral("/election/4");

        Scanner scanner = new Scanner(System.in);
        String line = null;
        boolean exitFlag = false;
        while ((line = scanner.nextLine()) != null) {
            //匹配任何空白字符,包括空格、制表符、换页符等等
            String[] cmdArgs = line.split("\\s+");
            logger.info("command={}", cmdArgs[0]);

//            if (line.equalsIgnoreCase("quit")){
//                zookeeperClient.close();
                break;
//            }
            switch (cmdArgs[0]) {
                case "close":
                    zookeeperClient.close();
                    break;
                case "connect":
                    logger.info("zkClient当前状态:{}", zookeeperClient.isConnected());
                    break;
                case "quit":
                    exitFlag = true;
                    break;
                case "create":
                    if (cmdArgs.length != 2) {
                        logger.warn("需要指定子节点名称");
                        break;
                    }
                    String prefix=electionNode + "/S_";
                    final String node=zookeeperClient.createEphemeralSequential(prefix,cmdArgs[1]);
                    //判断leader是否存在
                    if (!zookeeperClient.checkExists(electionLeaderNode)){
                        zookeeperClient.createPersistent(electionLeaderNode);
                        zookeeperClient.getZkClient().writeData(electionLeaderNode,node);
                    } else {
                        String leaderNode=zookeeperClient.getZkClient().readData(electionLeaderNode);
                        //处理leaderNode指明有节点,但是节点已经失效的情况
                        if (!zookeeperClient.checkExists(leaderNode)){
                            zookeeperClient.getZkClient().writeData(electionLeaderNode,node);
                        }
                    }
                    int seqNumber=Integer.parseInt(node.substring(prefix.length()));
                    logger.info("生成临时节点,node={},seqNumber={}", node,seqNumber);

                    String watchNode = prefix + String.format("%010d", seqNumber-1);
                    zookeeperClient.getZkClient().subscribeDataChanges(watchNode, new HerdEffectListener(node,zookeeperClient));

                    break;
                case "read":
                    if (!StringUtils.isNumeric(cmdArgs[1])){
                        logger.info("非法序列号,找不到数据");
                        break;
                    }
                    String path = electionNode + "/S_" + String.format("%010d", Integer.parseInt(cmdArgs[1]));
                    if (!zookeeperClient.checkExists(path)){
                        logger.info("指定的节点不存在");
                        break;
                    }
                    String ip=zookeeperClient.getZkClient().readData(path);
                    logger.info("节点内容为:{}",ip);
                    break;
                default:
                    break;
            }
            if (exitFlag) {
                break;
            }
        }

    }

}
package cn.superv.demo.zk.common;

import cn.superv.demo.zk.impl.ZkclientZookeeperClient;
import org.I0Itec.zkclient.IZkDataListener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Collections;
import java.util.List;

/**
 * Created by jpzhang on 2018/4/18
 * Description:{避免羊群效应的事件监听器,用于leader选举}
 */
public class HerdEffectListener implements IZkDataListener {
    private static Logger logger = LogManager.getLogger();

    private String node;
    private ZkclientZookeeperClient zookeeperClient;
    public static final String ELECTION_NODE = "/election";
    public static final String ELECTION_LEADER_NODE = "/election_leader";

    public HerdEffectListener(String node, ZkclientZookeeperClient zookeeperClient) {
        Assert.notNull(node, "节点不能为空");
        Assert.notNull(zookeeperClient, "zookeeperClient不能为空");
        this.node = node;
        this.zookeeperClient = zookeeperClient;
    }

    @Override
    public void handleDataChange(String dataPath, Object data) throws Exception {
        //ignore
    }

    @Override
    public void handleDataDeleted(String dataPath) throws Exception {
        //默认最小的节点是leader,如果比我小的挂掉了,开始竞争
        List<String> children = zookeeperClient.getChildren(ELECTION_NODE);
        Collections.sort(children);
        final String prefix = "/election/";
        logger.info("node={},firstNode={},watchNode={}", node, children.get(0), dataPath);
        //我是最小的,成为leader
        if (node.equals(prefix + children.get(0))) {
            if (!zookeeperClient.checkExists(ELECTION_LEADER_NODE)) {
                zookeeperClient.createPersistent(ELECTION_LEADER_NODE);
            }
            zookeeperClient.getZkClient().writeData(ELECTION_LEADER_NODE, node);
        } else {
            //我前面一个挂掉了,需要指向更前面一个
            String nextWatchNode = null;
            for (int i = 0; i < children.size(); i++) {
                if (node.equals(prefix + children.get(i))) {
                    nextWatchNode = prefix + children.get(i - 1);
                    break;
                }
            }
            if (nextWatchNode != null) {
                zookeeperClient.getZkClient().subscribeDataChanges(nextWatchNode, this);
            }
        }
    }
}
入口类完成了节点进入的模拟。测试过程如下:
create 192.168.1.101
create 192.168.1.102
create 192.168.1.103

可以看到当前leader为165节点。
删除166节点
leader仍然为165,同时可以看到167注册了165的监听。
2018-04-20 14:40:52,494 |  INFO | cn.superv.demo.zk.common.HerdEffectListener:41 [Thread: ZkClient-EventThread-12-127.0.0.1:2181]|node=/election/S_0000000167,firstNode=S_0000000165,watchNode=/election/S_0000000166
DEBUG - Subscribed data changes for /election/S_0000000165
删除165节点
可以看到167成为新的leader.
估计有程序员要问,上面那个看zookeeper数据的工具是啥,挺酷的。
呵,我们也不藏着,它的名字是ZooInspector,网上是可以找到的,找不到的话,contact me.
ps:为啥csdn上传不了图片,坑。谁能告诉我。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值