Elasticsearch系列之二选主7.x之前

image.png

无涯.png

在之前的 Elasticsearch 系列之一初识中,我们了解到:主节点负责管理集群的节点状态、分片、索引等等。只有设置node.master为 true 的节点,方可选举且被选举作为主节点。

在7.x版本前后,Elasticsearch 采用了不同的选主算法。7.x之前基于 Bully 算法选主,之后基于 Raft 算法。本篇我们重点介绍7.x之前的选主

Bully 算法

Bully 算法要求所有参与节点必须有 ID,将会选择存活中最大的 ID 作为主

以下算法介绍摘自百度百科

选举过程中会发送以下三种消息类型:

  1. Election 消息:表示发起一次选举
  2. Answer(Alive) 消息:对发起选举消息的应答
  3. Coordinator(Victory) 消息:选举胜利者向参与者发送选举成功消息

触发选举流程的事件包括:

  1. 当进程 P 从错误中恢复
  2. 检测到 Leader 失败

选举流程:

  1. 如果 P 是最大的 ID,直接向所有人发送 Victory 消息,成功新的 Leader ;否则向所有比他大的 ID 的进程发送 Election 消息
  2. 如果 P 再发送 Election 消息后没有收到 Alive 消息,则 P 向所有人发送 Victory 消息,成功新的 Leader
  3. 如果 P 收到了从比自己ID还要大的进程发来的 Alive 消息,P 停止发送任何消息,等待 Victory 消息(如果过了一段时间没有等到Victory 消息,重新开始选举流程)
  4. 如果 P 收到了比自己 ID 小的进程发来的 Election 消息,回复一个Alive消息,然后重新开始选举流程
  5. 如果 P 收到 Victory 消息,把发送者当做 Leader

假死

假设当前有5个节点。当 master 节点对 node1 的 ping 无法响应时,node1 将会认为 master 挂了,发起新的选举。此时如果 master 对其余3个节点的 ping 能正常响应,说明 master 实际并没有挂掉。

这种只有少数节点认为master挂了的现象就是假死。如果频繁假死就会导致集群频繁选举,影响集群稳定

ES 的解决方案:当一个节点认为 master 挂了时,会向其它节点询问 master 的状态,如果超过1/2的节点认为 master 挂了,才会重新选举。

脑裂

所有分布式系统都有网络分区问题。Bully 算法一旦发生网络分区,将在每个分区择出自己的主。此时整个集群将会有多个主出现。

ES的解决方案为:获得超过整个集群节点数的1/2+1的节点的承认,才是 master

ES选主源码分析

ES选主主要在以下时机触发:

  • 节点启动、退出、
  • master 定时 ping 其他节点,当发现存活数不足时,将放弃主身份,重新选举
  • 其他节点定时 ping 主节点,如果不通,且超过1/2节点认为不通则重新选举

以下源码分析基于6.2.3

主流程

整个选主的大致流程如下:

ES选主流程

选主核心逻辑在 ZenDiscovery 模块中,核心方法为 innerJoinCluster

private void innerJoinCluster() {
        DiscoveryNode masterNode = null;
        final Thread currentThread = Thread.currentThread();
  			//开始新一轮选举,此时开始接受投票
        nodeJoinController.startElectionContext();
  			//死循环,直到选出自己认为的临时master
        while (masterNode == null && joinThreadControl.joinThreadActive(currentThread)) {
            masterNode = findMaster();
        }
        if (transportService.getLocalNode().equals(masterNode)) {
            //如果自己被选为临时master,则阻塞等待其它节点投票确认,只需discovery.zen.minimum_master_nodes-1个节点确认即可
            final int requiredJoins = Math.max(0, electMaster.minimumMasterNodes() - 1); // we count as one
            nodeJoinController.waitToBeElectedAsMaster(requiredJoins, masterElectionWaitForJoinsTimeout,
                    new NodeJoinController.ElectionCallback() {
                        @Override
                        public void onElectedAsMaster(ClusterState state) {
                          	//确认成功,结束本轮选举
                            synchronized (stateMutex) {
                                joinThreadControl.markThreadAsDone(currentThread);
                            }
                        }
                      	@Override
                        public void onFailure(Throwable t) {
                          	//确认失败,则开始新的一轮选举(新的选举线程)
                            synchronized (stateMutex) {
                                joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                            }
                        }
                    }

            );
        } else {
          	// 自己选的主不是自己,结束本轮选举,不再接收选票
            nodeJoinController.stopElectionContext(masterNode + " elected");
            //给别人投票,直到临时主被整个集群确认为master,或超时
            final boolean success = joinElectedMaster(masterNode);

            //当集群主是自己择出的临时主,则选主结束
            synchronized (stateMutex) {
                if (success) {
                    DiscoveryNode currentMasterNode = this.clusterState().getNodes().getMasterNode();
                    if (currentMasterNode == null) {
                      	//集群仍无主,重新选举(新线程)
                        joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                    } else if (currentMasterNode.equals(masterNode) == false) {
                      	//集群主节点不是自己选出的,重新选举
                        joinThreadControl.stopRunningThreadAndRejoin("master_switched_while_finalizing_join");
                    }
                    joinThreadControl.markThreadAsDone(currentThread);
                } else {
                    // 给别人投票失败,重新选举
                    joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                }
            }
        }
    }

通过 discovery.zen.minimum_master_nodes 配置避免脑裂,通常为 节点数/2+1

等待其它节点确认

public void waitToBeElectedAsMaster(int requiredMasterJoins, TimeValue timeValue, final ElectionCallback callback) {
        final CountDownLatch done = new CountDownLatch(1);
        final ElectionCallback wrapperCallback = new ElectionCallback() {
            @Override
            public void onElectedAsMaster(ClusterState state) {
              	//满足最小投票数,则解除CountDownLatch阻塞
                done.countDown();
                callback.onElectedAsMaster(state);
            }

            @Override
            public void onFailure(Throwable t) {
               //失败,则解除CountDownLatch阻塞 
                done.countDown();
                callback.onFailure(t);
            }
        };

				//...
 				//设置最小投票数,回调函数
        electionContext.onAttemptToBeElected(requiredMasterJoins, wrapperCallback);
  			//再检查一下
        checkPendingJoinsAndElectIfNeeded();
				//...
        //通过CountDownLatch 阻塞,直到超时,或被countDown
        //discovery.zen.master_election.wait_for_joins_timeout控制阻塞超时时间
        if (done.await(timeValue.millis(), TimeUnit.MILLISECONDS)) {
          return;
        }
				//...
    }

当处理其它节点的投票请求时 ,handleJoinRequest 也会检查得票是否足够 checkPendingJoinsAndElectIfNeeded

    public synchronized void handleJoinRequest(final DiscoveryNode node, final MembershipAction.JoinCallback callback) {
        if (electionContext != null) {
            electionContext.addIncomingJoin(node, callback);
            checkPendingJoinsAndElectIfNeeded();
        } else {
            masterService.submitStateUpdateTask("zen-disco-node-join",
                node, ClusterStateTaskConfig.build(Priority.URGENT),
                joinTaskExecutor, new JoinTaskListener(callback, logger));
        }
    }

选临时主

findMaster 方法用于择出本轮选举,自己认为的主节点。

private DiscoveryNode findMaster() {
  			//ping其它节点,里面会将自身剔除
  			//pingTimeout通过配置discovery.zen.ping_timeout设置
        List<ZenPing.PingResponse> fullPingResponses = pingAndWait(pingTimeout).toList();
        
        final DiscoveryNode localNode = transportService.getLocalNode();

  			//加入自身到ping结果中,但不认为当前集群有活跃主节点
        fullPingResponses.add(new ZenPing.PingResponse(localNode, null, this.clusterState()));

        // 根据配置discovery.zen.master_election.ignore_non_master_pings决定是否移除(node.master=false)的节点
        final List<ZenPing.PingResponse> pingResponses = filterPingResponses(fullPingResponses, masterElectionIgnoreNonMasters, logger);
				
  			//收集各节点认为已当前集群的活跃master
        List<DiscoveryNode> activeMasters = new ArrayList<>();
        for (ZenPing.PingResponse pingResponse : pingResponses) {
            //非自身节点
            if (pingResponse.master() != null && !localNode.equals(pingResponse.master())) {
                activeMasters.add(pingResponse.master());
            }
        }

        //node.master=true的节点作为候选主节点
        List<ElectMasterService.MasterCandidate> masterCandidates = new ArrayList<>();
        for (ZenPing.PingResponse pingResponse : pingResponses) {
            if (pingResponse.node().isMasterNode()) {
                masterCandidates.add(new ElectMasterService.MasterCandidate(pingResponse.node(), pingResponse.getClusterStateVersion()));
            }
        }

        if (activeMasters.isEmpty()) {
          	//没有活跃的主节点
            if (electMaster.hasEnoughCandidates(masterCandidates)) {
              	//但有足够(discovery.zen.minimum_master_nodes)的主资格节点(避免脑裂),则选举
                final ElectMasterService.MasterCandidate winner = electMaster.electMaster(masterCandidates);
                return winner.getNode();
            } else {
                //选举失败
                return null;
            }
        } else {
          	//有活跃的主节点,择出一个主节点
            return electMaster.tieBreakActiveMasters(activeMasters);
        }
    }

从代码可知:选主的最快时间取决于 pingAndWait 的执行时间

ping

通过 pingAndWait 方法收集选票,阻塞住,只能在 pingTimeout 时间后返回

private ZenPing.PingCollection pingAndWait(TimeValue timeout) {
        final CompletableFuture<ZenPing.PingCollection> response = new CompletableFuture<>();
        try {
            zenPing.ping(response::complete, timeout);
        } catch (Exception ex) {
            // logged later
            response.completeExceptionally(ex);
        }
  			//通过CompletableFuture.get()阻塞住,直到response::complete触发
        return response.get();
        //...
}

详细看下 UnicastZenPing.ping 方法

protected void ping(final Consumer<PingCollection> resultsConsumer,
                        final TimeValue scheduleDuration,
                        final TimeValue requestDuration) {
				//解析其它节点链接参数
  			//discovery.zen.ping.unicast.hosts 配置其它节点
				//discovery.zen.ping.unicast.hosts.resolve_timeout 配置节点地址解析超时时间
        seedNodes = resolveHostsLists(...)
          //...
        final ConnectionProfile connectionProfile =
            ConnectionProfile.buildSingleChannelProfile(TransportRequestOptions.Type.REG, requestDuration, requestDuration);
        final PingingRound pingingRound = new PingingRound(pingingRoundIdGenerator.incrementAndGet(), seedNodes, resultsConsumer,nodes.getLocalNode(), connectionProfile);
        final AbstractRunnable pingSender = new AbstractRunnable() {
           //...
            protected void doRun() throws Exception {
                sendPings(requestDuration, pingingRound);
            }
        };
  			//依次提交3次 ping 任务,每间隔1/3个 pingTimeout 执行一次。第一次 ping 通过 generic 线程池立刻执行,之后两轮通过 schedule 线程池延时执行,但实际执行线程池仍为generic
        threadPool.generic().execute(pingSender);
        threadPool.schedule(TimeValue.timeValueMillis(scheduleDuration.millis() / 3), ThreadPool.Names.GENERIC, pingSender);
        threadPool.schedule(TimeValue.timeValueMillis(scheduleDuration.millis() / 3 * 2), ThreadPool.Names.GENERIC, pingSender);
  			//schedule线程池确保在 discovery.zen.ping_timeout时间 之后,通过 generic线程池完成本轮PingingRound
        threadPool.schedule(scheduleDuration, ThreadPool.Names.GENERIC, new AbstractRunnable() {
            @Override
            protected void doRun() throws Exception {
              	//触发pingAndWait方法的CompletableFuture,解除选举线程的阻塞
                finishPingingRound(pingingRound);
            }
          //...
        });
    }

每轮 ping 执行如下:

    protected void sendPings(final TimeValue timeout, final PingingRound pingingRound) {
        //...
      	//对所有节点,发出ping
        nodesToPing.forEach(node -> sendPingRequestToNode(node, timeout, pingingRound, pingRequest));
    }

    private void sendPingRequestToNode(final DiscoveryNode node, TimeValue timeout, final PingingRound pingingRound,
                                       final UnicastPingRequest pingRequest) {
      	//通过线程池,异步并发去ping
        unicastZenPingExecutorService.submitToExecutor(new AbstractRunnable() {
            @Override
            protected void doRun() throws Exception {
                		//...
                    connection = pingingRound.getOrConnect(node);
                		transportService.sendRequest(...);
            }
          //...
        });
    }
		

通过 unicastZenPingExecutorService 线程池并发去 ping 其它节点的,连接超时为 pingTimeout,ping 的超时时间为1.25个 pingTimeout

discovery.zen.ping.unicast.concurrent_connects 参数控制 unicastZenPingExecutorService 的最大线程数

discovery.zen.ping_timeout 控制此处的 ping 超时时间

ping 响应回调时,将非本节点的响应,收集起来

        public void addPingResponseToCollection(PingResponse pingResponse) {
            if (localNode.equals(pingResponse.node()) == false) {
                pingCollection.addPing(pingResponse);
            }
        }

响应 ping 请求:

    class UnicastPingRequestHandler implements TransportRequestHandler<UnicastPingRequest> {

        @Override
        public void messageReceived(UnicastPingRequest request, TransportChannel channel) throws Exception {

          	//同一集群才响应
            if (request.pingResponse.clusterName().equals(clusterName)) {
                channel.sendResponse(handlePingRequest(request));
            } else {
                throw new IllegalStateException(xxx);
            }
        }

    }
    private UnicastPingResponse handlePingRequest(final UnicastPingRequest request) {
        //...
      	//将集群状态作为入参
        pingResponses.add(createPingResponse(contextProvider.clusterState()));
        return new UnicastPingResponse(request.id, pingResponses.toArray(new PingResponse[pingResponses.size()]));
    }

    private PingResponse createPingResponse(ClusterState clusterState) {
        DiscoveryNodes discoNodes = clusterState.nodes();
      	//返回自身node、集群当前master node、
        return new PingResponse(discoNodes.getLocalNode(), discoNodes.getMasterNode(), clusterState);
    }

选择

根据上一步得到的 ping 结果,分场景选主

1、当前集群无主:则从主资格节点中选择

    public MasterCandidate electMaster(Collection<MasterCandidate> candidates) {
        assert hasEnoughCandidates(candidates);
        List<MasterCandidate> sortedCandidates = new ArrayList<>(candidates);
        sortedCandidates.sort(MasterCandidate::compare);
        return sortedCandidates.get(0);
    }

对 MasterCandidate 排序,选择第0个

        public static int compare(MasterCandidate c1, MasterCandidate c2) {
            //版本高的在前
            int ret = Long.compare(c2.clusterStateVersion, c1.clusterStateVersion);
            if (ret == 0) {
                ret = compareNodes(c1.getNode(), c2.getNode());
            }
            return ret;
        }
				    /** master nodes go before other nodes, with a secondary sort by id **/
         private static int compareNodes(DiscoveryNode o1, DiscoveryNode o2) {
            if (o1.isMasterNode() && !o2.isMasterNode()) {
                return -1;
            }
            if (!o1.isMasterNode() && o2.isMasterNode()) {
                return 1;
            }
             //id小的在前
            return o1.getId().compareTo(o2.getId());
        }

首先比较节点的集群版本,再比较节点的 nodeId。

2、当前集群已有主

从活跃的 master 节点,通过比较 nodeId ,取最小的 node 作为主节点

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

参考

推荐阅读

Guava Cache实战—从场景使用到原理分析

详解 HTTP2.0 及 HTTPS 协议

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

image.png