Elasticsearch原理分析——选主流程

Elasticsearch原理分析——选主流程


Discovery模块负责发现集群中的节点,以及选择主节点。ES支持多种不同Discovery类型选择,内置的实现称为Zen Discovery,其他的包括公有平台亚马逊的EC2、谷歌的GCE等。本章讨论内置的Zen Discovery实现。Zen Discorvery封装了节点发现(Ping)、选主等实现过程,现在我们先讨论选主流程,在后面的章节中整体性的介绍Discovery模块。

1. 设计思想

所有分布式系统都需要以某种方式处理一致性问题。一般情况下,可以将策略分为两组:试图避免不一致和定义发生不一致之后如何协调它们。后者在适用场景下非常强大,但对数据模型有比较严格的限制。因此这里研究前者,以及如何应对网络故障。

2. 为什么使用主从模式

除主从(Leader/Follower)模式外,另一种选择是分布式哈希表(DHT),可以支持每小时数千个节点的离开和加入,其可以在不了解底层网络拓扑的异构网络中工作,查询响应时间大约为4到10跳(中转次数)。例如,Cassandra就是使用这种方案。但是在相对稳定的对等网络中,主从模式会更好

ES的典型场景中的另一个简化是集群中没有那么多节点。通常,节点的数量远远小于单个节点能够维护的连接数,并且网络环境不必经常处理节点的加入和离开。这就是为什么主从模式更适合ES。

3. 选举算法

在主节点选举算法的选择上,基本原则是不重复造轮子。最好实现一个众所周知的算法,这样的好处是其中的优点和缺陷是已知的。ES的选举算法的选择上主要考虑下面两种。

  1. Bully 算法

    Leader选举的基本算法之一。它假定所有节点都有一个唯一的ID,使用该ID对节点进行排序。任何时候的当前Leader都是参与集群的最高ID节点。该算法的优点是易于实现。但是,当拥有最大ID的节点处于不稳定状态的场景下会有问题。例如,Master负载过重而假死,集群拥有第二大ID的节点被选为新主,这时原来的Master恢复,再次被选为新主,然后又假死……

    ES通过推迟选举,直到当前的Master失效来解决上述问题,只要当前主节点不挂掉,就不重新选主。但是容易产生脑裂(双主),为此,在通过“法定得票人数过半”解决脑裂问题。

  2. Paxos 算法

    Paxos非常强大,尤其在什么时机,以及如何进行选举方面的灵活性比简单的Bully算法有很大的优势,因此在现实生活中,存在比网络连接异常更多的故障模式。但是Paxos实现起来非常复杂。

4. 相关配置

与选主过程相关的重要配置有下列几个,并非全部配置。

  1. discovery.zen.minimum_master_nodes:最小主节点数,这是防止脑裂、防止数据丢失的极其重要的参数。这个参数的实际作用早已超越了其表面的含义。除了在选主时用于决定“多数”,还用于多处重要的判断,至少包含以下时机:

    • 触发选主:进入选主的流程之前,参选的节点数需达到法定人数。
    • 决定Master:选出临时的Master之后,这个临时的Master需判断加入它的节点达到法定人数,才确认选主成功。
    • gateway选举元信息:向有Master资格的节点发起请求,获取元数据,获取的响应数量必须达到法定人数,也就是参与元信息选举的节点数。
    • Master发布集群状态:发布成功数量为多数。

    为了避免脑裂,它的值应该是半数以上(quorum):

    (master_eligible_nodes/2) + 1
    

    例如,如果有3个具备Master资格的节点,则这个值至少应该设置为(3/2) + 1 = 2。

    该参数可以动态设置:

    PUT /_cluster/settings
    {
        "persistent":{
            "discovery.zen.minimun_master_nodes":2
        }
    }
    
  2. discovery.zen.ping.unicast.hosts:集群的种子节点列表,构建集群时本节点会尝试连接这个节点列表,那么列表中的主机会看到整个集群中都有哪些主机。可以配置为部分或全部集群节点。可以向下面这样指定:

    discovery.zen.ping.unicast.hosts: 192.168.137.14:9300,192.168.137.6:9300,192.168.137.122:9300
    

    默认端口使用9300端口,如果需要更改端口号,则可以在IP后手工指定端口。也可以设置一个域名,让该域名解析到多个IP地址,ES会尝试连接这个IP列表中的全部地址。

  3. discovery.zen.ping.unicast.hosts.resolve_timeout:DNS解析超时时间,默认为5秒。

  4. discovery.zen.join_timeout:节点加入现有集群的超时时间,默认为ping_timeout的20倍。

  5. discovery.zen.join_retry_attempts: 加入超时之后的重试次数,默认3次

  6. discovery.zen.join_retry_delay join_timeout:超时之后,重试前的延 迟时间,默认为100毫秒。

  7. discovery.zen.master_election.ignore_non_master_pings:设置为true 时,选主阶段将忽略来自不具备Master资格节点(node.master: false)的 ping请求,默认为false。

  8. discovery.zen.fd.ping_interval:故障检测间隔周期,默认为1秒。

  9. discovery.zen.fd.ping_timeout:故障检测请求超时时间,默认为30 秒。

  10. discovery.zen.fd.ping_retries:故障检测超时后的重试次数,默认为3 次。

5. 流程概述

ZenDiscovery的选主过程如下:

  1. 每个节点计算最小的已知节点ID,该节点为临时Master。向该节点发送领导投票。
  2. 如果一个节点收到足够多的票数,并且该节点也为自己投票,那么它将扮演领导者的角色,开始发布集群状态。

所有节点都会参与选举,并参与投票。但是,只有有资格成为Master的节点(node.master: true)的投票才有效。

获得多少选票才可以赢得选举胜利,就是所谓的法定人数。在ES中,法定大小是一个可以配置的参数。配置项:

discovery.zen.minimum_master_nodes

为了避免脑裂,最小值应该是有Master资格的节点数 n/2 + 1

6. 流程分析

整体流程可以概括为:选举临时Master,如果本节点当选,则等待确立Master,如果其他节点当选,则尝试加入集群,然后启动节点失效探测器。具体如下图所示:

在这里插入图片描述

执行本流程的线程池:generic

下面我们具体分析每个步骤的实现。

6.1 选举临时Master

选举过程的实现位于 ZenDiscovery#findMaster。该函数查找当前集群活跃的Master,或者从候选者中选择新的Master。如果选主成功,则返回选定的Master,否则返回空。

为什么是临时的Master?因为还需要等待下一个步骤,该节点的得票数足够时,才确立为真正的Master。

临时Master的选举过程如下:

  1. ping”所有节点,获取节点列表fullPingResponses, ping 结果不包含本节点,把本节点单独添加到fullPingResponses中。

  2. 构建两个列表。

    activeMasters列表:存储集群中当前活跃Master列表。遍历第一步获取的所有节点,将每个节点所认为的当前Master节点加入activeMasters列表中(不包含本节点)。在遍历中配置了discovery.zen.master_election.ignore_non_master_pings为true(默认为false),而节点又不具备Master资格,则跳过该节点。

    具体流程如下图所示:
    在这里插入图片描述

    这个过程是将集群当前已经存在的Master加入activeMasters列表,正常情况下只有一个。如果集群已存在Master,则每个节点都记录了当前Master是哪个,考虑到异常情况下,可能各个节点看到的当前Master不同。在构建activeMasters列表的过程中,如果节点不具备Master资格,则可以通过ignore_non_master_pings选项忽略它认为的那个Master。

    masterCandiddates列表:存储master候选者列表。遍历第一步获取列表,去掉不具备Master资格的节点,添加到这个列表中。

  3. 如果activeMasters为空,则从masterCandidates中选举,结果可能选举成功,也可能选举失败。如果不为空,则从activeMasters中选择最合适的作为Master。

    整体流程如下图所示:
    在这里插入图片描述
    从masterCandidates中选主

    masterCandidates中选主与选主的具体细节封装在ElectMasterService类中,例如,判断候选者是否足够,选择具体的节点作为Master等。

    masterCandidates中选主是,首先需要判断当前候选者人数是否达到法定人数,否则选主失败。

    public boolean hasEnoughCandidates (Collection<MasterCandidate> candidates) {
        //后选者为空,返回失败
        if(candidates.isEmpty()){
            return false;
        }
        //默认值为-1,确保单节点的集群可以正常选主
        if(minimumMasterNodes < 1){
            return true;
        }
        return candidates.size >= minimumMasterNodes;
    }
    

    当候选者人数达到法定人数后,从候选者中选出一个来做Master:

    public MasterCandidate electMaster (Collection<MasterCandidate> candidates) {
        Collection<MasterCandidate> sortedCandidates = new ArrayList<>(candidates);
        //通过自定义的比较函数对候选者节点从小到大排序
        sortedCandidates.sort(MasterCandidate::compare);
        //返回最小的作为Master
        return sortedCandidates.get(0);
    }
    

    可以看出这里只是将节点排序后选择最小的节点作为Master。但是排序时使用自定义的比较函数MasterCandidate::compare,早期的版本中只是对节点ID进行排序,现在会优先把集群状态版本号高的节点放在前面

    使用默认比较函数的情况下,sort结果为从小到大排序。参考Long类型的比较函数的实现:

    public static int compare (long x, long y) {
        return (x<y) ? -1 : ((x == y) ? 0:1);
    }
    

    自定义比较函数的实现:

    public static int compare (MasterCandidate c1, MasterCandidate c2) {
        //先比较集群状态版本,注意此处c2在前,c1在后
        int ret = Long.compare(c2.clusterStateVersion, c1.clusterStateVersion);
        //如果版本号相同,则比较节点ID
        if(ret == 0){
            ret = compareNodes(c.getNode(), c2.getNode());
        }
        return ret;
    }
    

    节点比较函数compareNodes的实现:对于排序效果来说,如果传入的两个节点中,有一个具备Master资格,而另一个不具备,则把有Master资格的节点排在前面。如果都不具备Master资格,或者都具备Master资格,则比较节点ID。

    但是masterCandidates列表中的节点都是具备Master资格的。compareNodes比较函数的两个if判断是因为在别的函数调用中会存在节点列表中可能存在不具备Master资格节点的情况。因此此处只会比较节点ID。

    private static int compareNodes(Discovery o1, Discovery o2) {
        //两个if处理两个阶段中一个具备Master资格而另一个不具备的情况
        if(o1.isMasterNode() && !o2.isMasterNode()){
            return -1;
        }
        if(!o1.isMasterNode() && o2.isMasterNode()){
            return 1;
        }
        //通过节点ID排序
        return o1.getId().compareTo(o2.getId());
    }
    

    从aciveMasters列表中选择

    列表存储着集群当前活跃的Master,从这些已知的Master节点中选择一个作为选举结果。选择过程非常简单,取列表中的最小值,比较函数仍然通过compareNodes实现,activeMasters列表中的节点理论情况下都具备Master资格。

    public DiscoveryNode tieBreakActiveMasters (Collection<DiscoveryNode> activeMasters) {
        return activeMasters.stream().min(ElectMasterService:compareNodes).get();
    }
    

6.2 投票与得票的实现

在ES中,发送投票就是发送加入集群 (JoinRequest) 请求。得票就是申请加入该节点的请求的数量。

收集投票,进行统计的实现在ZenDiscovery#handleJoinRequest方法中。当节点检查收到的投票是否足够时,就是检查加入它的连接数是否足够,其中会去掉没有Master资格节点的投票。

public synchronized int getPendingMasterJoinsCount () {
    int pendingMasterJoins = 0;
    //遍历当前收到的join请求
    for(DiscoveryNode node : joinRequestAccumulator.keySet()) {
        //过滤不具备Master资格的节点
        if(node.isMasterNode()) {
            pendingMasterJoins++;
        }
    }
    return pendingMasterJoins;
}

6.3 确立Master或加入集群

选举出的临时Master有两种情况:该临时Master是本节点或非本节点。为此单独处理。现在准备向其发送投票。

  1. 如果临时Master是本节点:
    1. 等待足够多的具备Master资格的节点加入本节点(投票到达法定人数),以完成选举。
    2. 超时(默认30秒,可配置)后还没有满足数量的join请求,则选举失败,需要进行新一轮选举。
    3. 成功后发布新的clusterState。
  2. 如果其他节点被选为Master:
    1. 不再接受其他节点的join请求。
    2. 向Master发送加入请求,并等待回复。超时时间默认为1分钟(可配置),如果遇到异常,则默认重试三次(可配置)。这个步骤在joinElectedMaster方法中有实现。
    3. 最终当选的Master会先发布集群状态,才确认客户端的join请求。因此,joinElectdMaster返回代表收到了join请求的确认,并且已经收到了集群状态。本步骤检查收到的集群状态中的Master节点如果为空,或者当选的Master不是之前选择的节点,则重新选举。

7. 节点失效检查

到此为止,选主流程已执行完毕,Master身份已确定,非Master节点已加入集群。

节点失效检查会监控节点是否离线,然后处理其中的异常。失效检测试选主流程之后不可或缺的步骤,不执行失效检测可能会产生脑裂(双主或多主)。在此我们需要启动两种失效探测器:

  • 在Master节点,启动NodesFaultDetection,简称NodesFD。定期探测加入集群的节点是否活跃。
  • 在非Master节点启动MasterFaultDetection,简称MasterFD。定期探测Master节点是否活跃。

NodesFaultDetectionMasterFaultDetection都是定期(默认为1秒)发生ping请求探测节点是否正常的。当失败达到一定次数(默认为3次),或者收到来自底层连接模块的节点离线通知,开始处理节点离开事件。

7.1 NodesFaultDetection事件处理

检查一下当前集群总结点数是否达到法定节点数(过半),如果不足,则会放弃Master身份,重新加入集群。为什么这么做?设想下面的场景,如下图所示。
在这里插入图片描述
假设有5台机器组成的集群产生网络分区,2台组成一组,另外3台组成一组,产生分区前,原Master为Node1.此时3台一组的节点会重新选举并成功选举Node3为Master,会不会产生双主?

NodesFaultDetection就是为了避免上述场景下产生双主。对应事件处理主要实现如下:在

ZenDiscovery#handleNodeFailure中执行NodeRemovalClusterStateTaskExecutor#execute

public ClusterTaskResult<Task> execute(final ClusterState currentState, final List<Task> tasks) throw Exception {
    //判断剩余节点是否大豆法定人数
    if(electMasterService.hasEnoughMasterNodes(remainingNodesClusterState.nodes()) == false){
        final int masterNodes = electMasterService.countMasterNodes(remainingNodesClusterState.nodes());
        rejoin.accept(
            LoggerMessageFormat.format("not enough master nodes(has [{}], but needed [{}])", masterNodes, electMasterService.minimumMasterNodes());
        return resultBuilder.build(currentState);
        
    }else{
        return resultBuilder.build(allocationService.deassociateDeadNodes(remainingNodesClusterState,true,describeTasks(tasks)));
    }
    
}

主节点在探测到节点离线事件处理中,如果发生节点数量不足法定人数,则放弃Master身份,从而避免产生双主。

7.2 MasterFaultDetection事件处理

探测Master离线的处理很简单,重新加入集群。本质上就是该节点重新执行一遍选主流程。

对应事件处理主要实现如下:

ZenDiscorvery#handleMasterGone

private void handleMasterGone(final DiscoveryNode master, final Throwable cause, final String reason){
    synchronized (stateMutex){
        if(localNodeMaster() == false && masterNode.equals(committedState.get().nodes.getMasterNode())){
            pendiingStatesQueue.failAllStatesAndClear(new ElasticsearchException("master left [{}]",reason));
            //重新加入集群
            rejoin("master left (reason = "+ resaon +")");
        }
    }
}

8. 小结

选主流程在集群中启动,从无主状态到产生新主时执行,同时集群在运行正常过程中,Master探测到节点离开,非Master节点探测到Master节点离开时都会执行。

9. 关注我

搜索微信公众号:java架构强者之路
在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值