工程

Elasticsearch 集群协调迎来新时代

Elasticsearch 之所以变得如此广泛流行,其中一个原因是,它可以很好地从只有几个节点的小集群扩展为拥有数百个节点的大集群。它的核心就是集群协调子系统。Elasticsearch 7 版本包含了一个新集群协调子系统,与早期版本相比,它提供了很多优点。本文将介绍在版本 7 中对这个子系统的改进,描述如何使用新的子系统、这些改变对版本 6 升级有哪些影响,以及这些改进如何防止用户无意中将数据置于危险之中,最后将浅析新子系统的工作原理。

什么是集群协调?

Elasticsearch 集群可以执行需要多个节点协同工作的很多任务。例如,每个搜索必须路由到所有正确的分片,以确保结果的准确性。在索引或删除某些文档时,必须更新每个副本。每个客户端请求都必须从接收它的节点转发到能处理它的节点。这些节点对集群概况分别有各自的认识,并据此来执行搜索、索引和其他协调活动。这个集群概况就是指集群状态。集群状态决定了每个索引的映射和设置、分配给每个节点的分片,以及同步的分片副本。在集群中保持该信息的一致性十分重要。最近推出的很多功能(包括基于序号的复制跨集群复制)只有在群集状态一致的情况下才能正常运行。

协调子系统的运行方式是选择一个特定节点作为集群的主节点。这个选举出的主节点需要确保其集群中的所有节点都能够接收集群状态的更新。这比开始听起来要困难得多,因为类似 Elasticsearch 这样的分布式系统必须为各种奇怪的情况做好准备。例如,节点有时运行缓慢,有时垃圾回收会导致停顿,或者突然断电。 网络会出现分区、包丢失、高延迟时段,或者可能提交乱序消息。有时可能会同时出现多个这样的问题,而且可能间歇性地发生。尽管如此,集群协调子系统必须能够保证每个节点具有集群状态的一致性视图。

重要的是,Elasticsearch 必须有足够的弹性来应对各个节点的故障。通过只有在法定数量的节点接受集群状态更新时,才将更新视为成功,Elasticsearch 实现了这种弹性。法定数量是从集群里符合主节点条件的节点中精心选出的一部分节点的数量。仅要求部分节点进行响应的优势在于,某些节点可能会失败,但不会影响集群的可用性。在选择法定数量节点时要十分小心,这样集群就无法选举出两个独立的主节点,从而避免两个主节点作出不一致的决策而最终导致数据丢失。

通常,我们建议集群设置三个符合主节点条件的节点,这样如果其中的一个节点失败,其他两个节点仍然可以安全地形成法定数量,并继续提供服务。如果一个集群少于三个符合主节点条件的节点,那它将无法安全地容忍其中任何一个节点发生故障。相反,如果一个集群中符合主节点条件的节点远多于三个,则节点选举和集群状态更新可能需要更长的时间。

演进还是根本性革新?

Elasticsearch 6.x 及之前的版本使用了一个称为 Zen Discovery 的集群协调子系统。这个子系统经过多年的进化和完善,能够成功地为大大小小的集群提供支持。不过,我们想做出一些改进,这需要对子系统的运行方式做出一些根本性的改变。

Zen Discovery 允许用户通过使用 discovery.zen.minimum_master_nodes 设置来决定多少个符合主节点条件的节点可以形成法定数量。在每个节点上一定要正确配置此项设置,并在集群动态扩展时也正确地更新它,这一点至关重要。系统无法检测到用户是否错误配置了此项设置,而且在实践当中,在添加或删除节点后很容易忘记调整此项设置。因此,Zen Discovery 试图通过在每次主节点选举过程中等待几秒来防止出现这种错误配置,并且对其他的超时机制通常也十分保守。这意味着,如果选举的主节点失败,在选择替代节点之前,集群至少在几秒钟内是不可用的。如果集群无法选举出一个主节点,则有时会很难了解是什么原因。

在 Elasticsearch 7.0 中,我们重新设计并重建了集群协调子系统:

  • 移除了 minimum_master_nodes 设置,让 Elasticsearch 自己选择可以形成法定数量的节点。 
  • 典型的主节点选举现在只需很短时间就能完成。 
  • 集群的扩充和缩减变得更加安全和简单,并且大幅降低了因系统配置不当而可能造成数据丢失的风险。 
  • 节点状态记录比以往清晰很多,有助于诊断它们不能加入集群的原因,或者为何不能选举出主节点。

随着节点的添加或删除,Elasticsearch 会自动更新集群的选举配置,以维护最佳的容错级别。选举配置是一组符合主节点条件的节点,在做出决策时,它们具有投票权。通常,选举配置包含集群中所有符合主节点条件的节点。法定数量就是指选举配置中的多数节点:所有集群状态更新都需要选举配置中超过半数的节点同意。由于选举配置以及法定数量都由系统管理,因此,即使添加或删除了节点,也可以避免任何可能导致数据丢失的错误配置。

从 7.0 开始,如果一个节点无法发现主节点,并且本身也无法赢得选举,Elasticsearch 将会定期记录警告消息,详尽描述当前的状态,以帮助诊断许多常见的问题。

另外,Zen Discovery 以前有一个非常罕见的故障模式(在 Elasticsearch 弹性状态页中描述为“Repeated network partitions can cause cluster state updates to be lost”(重复的网络分区可能导致集群状态更新丢失)),现在这个模式已不复存在。这个问题项现在已标记为已解决。

如何使用?

如果您完全使用默认配置启动新安装的 Elasticsearch 节点,它们会自动查找在同一主机中运行的其他节点,并在几秒钟内形成一个集群。如果您在同一个主机中启动更多节点,则默认情况下它们也会发现并加入这个集群。因此,在 Elasticsearch 7.0 版本中启动多节点开发集群与在之前版本一样简单。

这种全自动集群形成机制在单台主机中可以很好地运行,但在生产或其他分布式环境中使用还显得不够强健。对于这类环境,该机制的风险在于:各节点可能无法及时发现彼此,进而可能形成两个或多个独立的集群。从版本 7.0 开始,如果您要启动一个全新的集群,并在多台主机上都有节点,则必须指定该集群在初次选举中应使用的一组符合主节点条件的节点作为选举配置。这被称为集群引导,只在第一次形成集群时需要。已经加入集群的节点会将选举配置存储在它们的数据文件夹中,并在重启后重复使用这些配置,而将要加入现有集群的全新节点可以从集群选举的主节点接收该信息。

您可以为 cluster.initial_master_nodes 设置指定一组符合主节点条件的初始节点的主机名称或 IP 地址,以此来引导集群。您可以在命令行,或在一个或多个符合主节点条件的节点的 elasticsearch.yml 文件中提供此设置。此外,您还需要配置发现子系统,以便节点知道如何发现彼此。

如果没有设置 initial_master_nodes,则在启动全新节点时会尝试发现现有的集群。如果节点找不到可以加入的集群,则会定期记录一条警告消息,指明

master not discovered yet, this node has not previously joined a bootstrapped (v7+) cluster,
and [cluster.initial_master_nodes] is empty on this node

在向集群添加新的符合主节点条件的节点时,不再需要任何特殊的仪式。只需将新节点配置为发现现有集群,再启动它们即可。在有新节点加入时,集群会安全地自动调整其选举配置。只要不同时停止一半或更多符合主节点条件的节点,即使移除节点也是安全的。如果您需要停止一半或更多符合主节点条件的节点,或者您有更复杂的扩展或编排需求,可以使用更有针对性的扩展程序,使用一个 API 来直接调整选举配置。

如何升级?

您可以通过滚动升级或完全重启集群,将 Elasticsearch 集群从版本 6 升级到版本 7。我们建议使用滚动升级,因为这样可以逐个升级节点,并同时保持集群在升级期间的可用性。在滚动升级到版本 7 之前,必须将版本 6 的集群升级到版本 6.7。您可以通过完全重启集群,从任何 6.x 版本升级到版本 7,但是这需要关闭整个集群,然后再重新启动。不论哪种方式,在版本 6 和版本 7 之间对 Elasticsearch 进行的更改都远比此处描述的对集群协调的改进多得多。为了确保顺利升级,请始终仔细遵循详细的升级说明

如果执行滚动升级,则集群会基于集群中的节点数量和任何现有的 minimum_master_nodes 设置自动进行引导。这意味着在开始升级之前务必要确保设置正确。这里不需要设置 initial_master_nodes,因为在执行滚动升级时,集群引导会自动发生。在选举主节点时,版本 7 符合主节点条件的节点会优先投票给版本 6.7 的节点,因此,在升级过程中,版本 6.7 的节点将更有可能选举为主节点,直到升级完所有符合主节点条件的节点。

如果您执行完全重启集群的升级方式,则必须按照上述的方式引导升级后的集群:在启动新升级的集群之前,必须先将 initial_master_nodes 设置为符合主节点条件的节点的主机名称或 IP 地址。

在版本 6 以及更早的版本中,还有一些其他设置,允许您在 discovery.zen.* 命名空间中配置 Zen Discovery 的行为。其中有些设置不再有效,且已被删除。其他的一些设置已被重新命名。如果某个设置被重命名,则它的旧名称就会在版本 7 中弃用。您需要调整配置来使用新名称

| 旧名称 | 新名称 | | --- | --- | | discovery.zen.ping.unicast.hosts | discovery.seed_hosts | | discovery.zen.hosts_provider | discovery.seed_providers | | discovery.zen.no_master_block | cluster.no_master_block |

新的集群协调子系统包括一个新的故障检测机制。这意味着 discovery.zen.fd.* 命名空间中的 Zen Discovery 故障检测设置不再有效。大多数用户应在版本 7 以及更高版本中使用默认的故障检测配置,但是,如果需要进行任何更改,可以使用 cluster.fault_detection.* 设置来完成。

安全第一

版本 7.0 之前的 Elasticsearch 有时会允许您无意中执行一系列步骤,这有可能会导致集群状态出现不一致。而在版本 7.0 以及更高版本中,将会让您完全知道可能在执行一些不安全的操作,并要求您确认是否真地要继续操作。

例如,如果有一半或更多符合主节点条件的节点永久丢失,Elasticsearch 7.0 集群将无法自动恢复。通常,在一个集群中有三个符合主节点条件的节点,以便 Elasticsearch 可以在不中断的情况下容忍其中一个节点丢失。如果其中有两个节点永久丢失,则剩下的节点就无法安全地继续工作。

版本 7.0 之前的 Elasticsearch 会静静地等待集群从这种情况中恢复。用户可以通过启动新的、空的、符合主节点条件的节点来替换任意数量的丢失节点,从而使集群重新上线。从永久丢失的一半或更多符合主节点条件的节点进行自动恢复是不安全的,因为剩余的节点都无法保证拥有最新集群状态的副本。这就可能会导致数据丢失。例如,一个分片副本可能已从同步集中移除。如果其他节点不知道这一点,则这个过时的分片副本就可能被分配给一个主节点。最危险的部分就是,用户完全没有意识到这一系列步骤已经将他们的集群置于危险之中。用户可能需要几周或几个月之后才会注意到其中的不一致。

在 Elasticsearch 7.0 以及更高版本中,这种不安全活动受到了更多限制。集群宁愿保持不可用状态,也不会冒这种风险。在没有备份的极少数情况下,如果绝对有必要,仍然可以执行这种不安全的操作。它会采取几个额外的步骤,确认您知道风险的存在,并避免意外执行不安全操作。

如果已经丢失了一半或更多符合主节点条件的节点,则首先要做的就是将这些丢失的节点重新上线。如果节点的数据目录仍然完好,则最好的办法就是使用这些数据目录启动新节点。如果这种方法可行,则可安全地使用最新的集群状态重新形成集群。

下一个要尝试的方法就是从最近的快照中恢复集群。这可使集群进入一种已知的健康状态,但会丢失自获取快照以来写入的数据。然后,您可以重新索引任何丢失的数据,因为您知道丢失的时间段。由于快照是增量的,因此您可以非常频繁地执行这些操作。通常可以每 30 分钟获取一次快照,以将这种恢复过程中丢失的数据量限制在一定范围。

如果这些恢复操作都不可行,则最后的手段就是采用 elasticsearch-node 不安全的恢复工具。这是一个命令行工具,系统管理员可用它来执行一些不安全的操作(如从少数节点中选举一个过时的主节点)。通过将可能破坏一致性状态的步骤明晰化,Elasticsearch 7.0 消除了通过一系列不安全的操作无意中造成数据丢失的风险。

工作原理?

如果您熟悉分布式系统理论,则可能会将集群协调看作是可使用分布式共识来解决的一个问题示例。 在 Elasticsearch 的开发之初,分布式共识并未得到广泛理解,不过最近几年,这项技术已经有了长足的发展。

Zen Discovery 采用了很多分布式共识算法中的想法,但只是有机地采用,并没有严格按照理论所规定的那个模型。而且,它还有一个非常保守的超时机制,使得它有时在出现故障后恢复非常慢。7.0 中引入的新集群协调子系统使我们有机会更紧密地遵循这个理论模型。

分布式协调被认为是一个难以解决好的问题。我们曾经严重依赖形式化的方法来预先验证我们的设计,自动化工具在正确性和安全性方面都提供强有力的保证。您可以在公共 Elasticsearch 形式化模型资料库中找到 Elasticsearch 新集群协调算法的形式化规范。这个算法的核心安全模块简单明了。在 Elasticsearch 资料库中,形式化模型与生产代码之间存在直接的一一对应关系。

如果您熟悉 Paxos、Raft、Zab 和 Viewstamped Replication (VR) 等系列分布式共识算法,那么也该熟悉核心安全模块。该模块模拟了一个可重写寄存器,并使用了主节点的概念,这个概念与 Paxos 投票、Raft 条款和 VR 的视图特别类似。该核心安全模块及其形式化模型还包括集群引导、跨节点重启的持续性和动态重配置。所有这些功能对于确保系统能够在所有环境中都正常运行十分重要。

围绕这一理论强健的核心,我们构建了一个活动层,用来确保无论集群发生什么故障,一旦网络恢复并且有足够的节点在线,就会选举出一个主节点,并且该主节点可以发布集群状态更新。这个活动层使用了大量的前沿技术来避免许多常见问题。选举调度程序具有适应性,可以根据网络状况改变自身行为,以避免出现过多的争议选举。Raft 形式的预投票阶段可以在选举开始之前就抑制无法取胜的选举,从而避免恶意节点的干扰。延迟检测可以防止节点在远远落后于主节点时破坏集群。主动双向故障检测可以确保集群中的节点始终能够互相通信。大部分集群状态更新都能够以小的 diffs 高效发布出去,从而避免在节点之间复制整个集群状态。被优雅终止的领导节点将明确让位给它们选择的继承者,通过避免全面选举,减少在有计划的故障转移期间的中断时间。我们开发了测试基础架构来有效模拟可能会持续几秒、几分钟或几小时的非正常中断所带来的影响,从而验证一旦中断得到解决后集群是否可以始终快速地恢复。

为什么不选择 Raft?

我们经常被问到的一个问题是,为什么不简单地“插入”像 Raft 一样的标准分布式共识算法。有很多公认的算法,每种算法都有不同的利弊权衡。我们仔细评估了所有可以找到的文献,并从中汲取灵感。在我们早期的概念验证中,有一个概念便使用了非常接近 Raft 的协议。我们从这一经验中了解到,将其与 Elasticsearch 完全集成,需要做出非常巨大的改变。此外,许多标准算法还规定了一些对于 Elasticsearch 来说不是最佳选择的设计决策。例如:

  • 标准算法通常是围绕操作日志构建的,而 Elasticsearch 的集群协调更多的是直接基于集群状态本身。相比基于操作可能达到的优化来说,集群协调可以更简单地对批处理(将相关操作合并到单个广播中)之类的操作进行重大优化。
  • 标准算法在集群伸缩能力方面都相当受限,需要一系列步骤来完成许多维护任务,而 Elasticsearch 的集群协调可以在一个步骤中安全地执行任意的重配置。这通过避免不确定的中间状态简化了周围的系统。
  • 标准算法通常过于关注安全性,而忽略了如何保证活动性的细节,也没有描述如果发现某个节点不健康时,集群该如何反应。Elasticsearch 的健康检测机制非常复杂,已经在该领域经过了多年的使用和优化,对于我们来说,维持其现在的运行状况十分重要。事实上,与保证系统的活动性相比,实现系统的安全性所需付出的成本要少得多。大多数的实现工作都集中在系统的活动性上。
  • 项目的目标之一是,支持从运行 Zen Discovery 的 6.7 集群零中断滚动升级到运行新协调子系统的版本 7 集群。将任何标准算法调整为允许这种滚动升级的算法似乎都不可行。

开发一个完整的、以工业级强度实现的分布式共识算法需要大量的投入,并且必须超越学术文献所概述的范围。在实践中,定制是不可避免的,但是协调协议十分复杂,任何定制都存在引入错误的风险。从工程角度来看,将这些定制视为开发新协议可能更有意义。

总结

Elasticsearch 7.0 提供了一个更快、更安全且更易于使用的新集群协调子系统。它支持从 6.7 零中断滚动升级,并为弹性数据复制提供了基础。要尝试用新的集群协调子系统,请下载最新的 7.0 公测版,参考文档,运行一下,然后给我们提供反馈