弹性搜索 (es) 是最常见的开源分布式搜索引擎。它基于 lucene (一个信息检索库), 并提供强大的搜索和查询功能。要了解其搜索原则, 您必须了解 lucene。若要了解 es 体系结构, 您必须知道如何实现分布式系统。一致性是分布式系统的核心。

本文介绍 es 群集组合、节点发现、主选择、错误检测和缩放。在节点发现和主选择方面, es 使用自己的实现, 而不是外部组件, 如 zookeeper。我们将描述这种机制是如何运作的, 以及它的问题。本系列包括:

  1. es 集群组成 (第1部分)
  2. 节点发现 (第1部分)
  3. 总选举 (第1部分)
  4. 错误检测 (第2部分)
  5. 群集缩放 (第2部分)
  6. 与动物园管理员和拉孚等实施方法的比较 (第二部分)
  7. 摘要 (第2部分)

es 集群构成

首先, 弹性搜索群集 (es 群集) 由多个节点组成, 这些节点具有不同的类型。通过下面的配置, 可以生成四种类型的节点:

conf/elasticsearch.yml:
    node.master: true/false
    node.data: true/false

四种类型的节点是真假 node.masternode.data 。其他类型的节点 (如 ingestnode (用于数据预处理)) 不在本文的范围内。

如果 node.master 为 true, 则该节点是主节点候选节点, 可以参与选举。在 es 文档中, 它通常被称为符合主资格的节点, 类似于强模候选人。在正常操作过程中, es 只能有一个主控点 (即领导者), 因为有多个主机会导致大脑分裂。

如果 node.data 为 true, 则节点充当数据节点, 存储分配给节点的分片数据, 并负责分片数据的写入和查询。

此外, 任何群集中的节点都可以执行任何请求。群集将请求转发到相应的节点进行处理。例如, 当 node.master node.data 两者都是假的时, 此节点充当类似代理的节点, 接受请求, 并转发聚合结果。

Elasticsearch cluster

上图是 es 集群的示意图, 其中 node _ a 是当前群集的主, node _ b 和 node _ c 是主节点候选项;节点 _ a 和节点 _ b 也是数据节点;此外, 节点 _ d 是一个简单的数据节点;节点 _ e 是一个代理节点。

以下是需要考虑的一些问题: 应该为 es 群集配置多少符合主节点资格的节点?当群集的存储或计算资源不足且需要扩展时, 应将添加的节点设置为什么类型?

节点发现

节点启动后, 需要通过节点发现将其添加到群集。zendiscovery 是一个 es 模块, 提供功能, 如节点发现和主选择, 而无需依赖 zookeeper 等工具。请参见官方文件:

https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery-zen.html

简而言之, 节点发现依赖于以下配置:

conf/elasticsearch

zen.ping.unicast.hosts: [1.1.1.1、1.1.1.2、1.1.1.3]

此配置将从每个节点创建到每个其他主机的边。当群集中的所有节点形成连接映射时, 每个节点都可以看到群集中的其他节点, 从而防止孤岛。

正式建议将单播主机列表作为群集中符合主资格的节点列表进行维护。因此, 建议将单播主机列表作为群集中符合主节点的节点列表进行维护。

大师选举

如上所述, 群集中可能有多个符合主节点的节点, 主选择可确保只有一个选定的主节点。如果选择了多个节点为主节点, 则会发生分裂大脑, 这会影响数据一致性, 并导致群集中出现混乱, 并产生不同的意外结果。

为了避免大脑分裂, es 使用了一个通用的分布式系统概念, 确保选为主主机被仲裁的主符合条件的节点识别, 只产生一个主控点。此仲裁的配置如下:

conf/elasticsearch.yml:
    discovery.zen.minimum_master_nodes: 2

此配置对于群集至关重要。

1. 谁发起了主选举, 选举是在什么时候开始的?

主选择由符合主条件的节点在满足以下条件时启动:

  1. 符合主节点的当前状态不是主节点。
  2. 符合主节点的节点使用 zendiscovery 的 ping 操作查询群集中的其他已知节点, 并确认没有节点连接到主节点。
  3. 当前有多个节点 (包括此节点) 未连接到主节点。

简而言之, 当节点确定仲裁中符合主条件的节点 (包括自身) 认为群集没有主节点时, 就可以启动主选择。

2. 当需要主选举时, 应选择哪个节点?

第一个问题是, 应该选择哪个节点?如下面的源代码所示, 排序后选择第一个主匹配项 (即符合主资格的节点)。

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

那么, 它们是如何分类的呢?

public static int compare(MasterCandidate c1, MasterCandidate c2) {
    // we explicitly swap c1 and c2 here. The code expects "better" to be lower in a sorted
    // list, so if c2 has a higher cluster state version, it needs to come first.
    int ret = Long.compare(c2.clusterStateVersion, c1.clusterStateVersion);
    if (ret == 0) {
        ret = compareNodes(c1.getNode(), c2.getNode());
    }
    return ret;
}

如上面的源代码所示, clusterStateVersion 对节点的位置进行比较, 具有较高 clusterStateVersion 的优先级。当节点具有相同的 clusterStateVersion 情况时, 程序将用于 compareNodes 比较节点的 id (在节点最初启动时随机生成 id)。

概括地说:

  1. 更高 clusterStateVersion 的优先级。这可确保新主机具有最新 clusterState 的 (即群集的元数据), 从而避免丢失已提交的元数据更改。当选择主服务器时, 将根据 clusterState 此版本对其进行更新 (一个例外是群集重新启动且没有任何节点具有元结构; 在这种情况下, 需要首先选择主机, 然后主机使用持久性数据进行元恢复, 然后执行元同步)

也就是说, 倾向于选择具有低 id 的节点。id 是节点最初启动时生成的随机字符串。这样做的目的是为了确保选举结果的稳定, 避免因掌握候选人过多而导致选举失败。

3. 什么是成功的选举?

当符合主资格的节点 (node _ a) 启动选举时, 它将根据上面的排序策略选择已批准的主节点。该过程因节点 _ a 选择自己还是选择 node _ b 作为主服务器而异。

假设节点 _ a 选择节点 _ b 作为主节点:

node _ a 将联接请求发送到 node _ b, 然后:

  1. 如果 node _ b 已成为 master, 它将 node _ a 添加到群集, 并发布最新的群集 _ state, 其中包含 node _ a 的信息。它类似于在正常情况下添加新节点。当为 node _ a 发布新的群集 _ state 时, node _ a 完成联接。
  2. 如果 node _ b 正在运行主服务器, 则将此联接作为投票。在这种情况下, node _ a 将等待超时, 以查看 node _ b 是否成为主节点, 或者另一个节点是否被选为主节点。
  3. 如果 node _ b 认为它不是主服务器 (在任何时候), 它将拒绝此联接。在这种情况下, 节点 _ a 启动下一次选择。

假设节点 _ a 选择自己作为主:

node _ a 等待其他节点加入, 即等待来自其他节点的投票。当收集一半以上的选票时, 它将自己视为主节点, 将群集 _ 状态中的主节点更改为自身, 并向群集发送消息。

有关详细信息, 请参阅以下源代码:

        if (transportService.getLocalNode().equals(masterNode)) {
            final int requiredJoins = Math.max(0, electMaster.minimumMasterNodes() - 1); // we count as one
            logger.debug("elected as master, waiting for incoming joins ([{}] needed)", requiredJoins);
            nodeJoinController.waitToBeElectedAsMaster(requiredJoins, masterElectionWaitForJoinsTimeout,
                    new NodeJoinController.ElectionCallback() {
                        @Override
                        public void onElectedAsMaster(ClusterState state) {
                            synchronized (stateMutex) {
                                joinThreadControl.markThreadAsDone(currentThread);
                            }
                        }

                        @Override
                        public void onFailure(Throwable t) {
                            logger.trace("failed while waiting for nodes to join, rejoining", t);
                            synchronized (stateMutex) {
                                joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                            }
                        }
                    }

            );
        } else {
            // process any incoming joins (they will fail because we are not the master)
            nodeJoinController.stopElectionContext(masterNode + " elected");

            // send join request
            final boolean success = joinElectedMaster(masterNode);

            synchronized (stateMutex) {
                if (success) {
                    DiscoveryNode currentMasterNode = this.clusterState().getNodes().getMasterNode();
                    if (currentMasterNode == null) {
                        // Post 1.3.0, the master should publish a new cluster state before acknowledging our join request. We now should have
                        // a valid master.
                        logger.debug("no master node is set, despite the join request completing. Retrying pings.") ;
                        joinThreadControl.markThreadAsDoneAndStartNew(currentThread);
                    } else if (currentMasterNode.equals(masterNode) == false) {
                        // update cluster state
                        joinThreadControl.stopRunningThreadAndRejoin("master_switched_while_finalizing_join");
                    }

                    joinThreadControl

再试一次…..。
}
}
}

按照上面的过程, 下面是一个简单的场景, 使其更清晰:

假设群集有3个符合主节点的节点, node _ a、node _ b 和 node _ c, 并且选择优先级顺序为 node _ a、node _ b、node _ c。三个节点中的每一个都确定没有当前主节点。每个节点启动一个选择, 并根据优先级顺序, 所有节点选择 node _ a。因此, 节点 _ a 等待联接。node _ b 和 node _ c 向 node _ a 发送联接请求。当 node _ a 收到第一个联接请求以及自己的投票时, 它总共有两票 (超过一半), 并成为主人。此时, 群集 _ 状态包含两个节点。当 node _ a 接收来自其余节点的联接请求时, 群集 _ 状态包含所有三个节点。

4. 选举如何避免分裂大脑?

基本原则在于仲裁策略。如果只有通过仲裁批准的节点成为主节点, 则两个节点不可能得到仲裁的批准。

在上述过程中, 主候选对象需要等待在仲裁中提交批准的节点加入, 然后才能成为主节点。这可确保此节点已被仲裁批准。虽然上面的过程看起来很合理, 在大多数情况下效果很好, 但也存在问题。

此过程对节点在选举过程中可以投票的次数没有限制。在什么情况下, 一个节点会被允许两次投票?例如, node _ b 对 node _ a 进行一次投票, 但 node _ a 在一段时间后仍未成为主节点。节点 _ b 不能等待, 并启动下一次选举。此时, 它确定群集包含 Node_0, 该的优先级高于 node _ a, 因此 node _ b 将为 Node_0 投票。假设 Node_0 和 node _ a 都在等待选票, 则 node _ b 已投票两次, 每次投票给不同的候选人。

我们如何解决这个问题?例如, raft 算法引入了选举期限的概念, 确保每个节点在每个选举任期内只能投票一次。额外的选票将按 term+1 计算。如果最后两个节点都认为它们是主节点, 则一个术语必须大于另一个术语。因为这两个术语都收到了仲裁投票, 所以仲裁节点有一个更大的期限, 从而确保具有较小期限的节点不能提交任何状态更改 (提交需要仲裁节点才能成功的日志持久性、仲裁持久性条件由于期限检查而无法满足)。这可确保群集中的状态更改始终保持一致。

es (v6.2) 尚未解决此问题。在类似方案中的测试用例中, 有时会选择两个母版, 并且两个节点都认为自己是主节点, 并将状态更改发布到群集。发布包括两个阶段。首先, 它确保仲裁节点 “接受” 此更改, 然后要求所有节点提交此更改。不幸的是, 这两个母版可能都完成了第一阶段, 并进入提交阶段。这会导致节点间状态不一致, 这在 raft 中不是问题。两个大师如何完成第一阶段?因为在第一阶段, es 在进行简单检查后, 将新的群集 _ 状态放入内存队列中。如果当前群集 _ 状态的主机为空, 则不会对其进行检查。换句话说, 在接受了 node _ a 成为主节点的群集 _ 状态 (在提交之前) 之后, node _ b 也可以在群集 _ 状态下被接受为主机。这允许 node _ a 和 node _ b 满足提交条件并启动提交命令, 从而导致群集状态不一致。当然, 像这样的分裂大脑情况会很快自动恢复, 因为当主机在不一致发生后再次发布群集 _ 状态时, 将不再满足仲裁条件, 或者自动降级为候选项因为它的追随者不再构成法定人数我们将分析 es 一致性在以下元更改过程描述中存在问题的其他方案。

这都是第1部分!重新调整周一, 届时我们将介绍错误检测、群集缩放等主题。

Comments are closed.