发布时间: 2024年9月6日
最近的博客文章中,我们宣布了支持 Elastic Cloud Serverless 产品的无状态架构。通过将持久性保障和复制卸载到对象存储(例如 Amazon S3),我们获得了许多优势和简化。
历史上,Elasticsearch 一直依赖本地磁盘持久化来保障数据安全和处理陈旧或孤立的节点。在这篇博客中,我们将讨论无状态环境中的数据持久性保障,包括如何通过安全检查来防止陈旧节点不安全地确认新写入和删除操作。
在接下来的博客文章中,我们将介绍持久性承诺的基础知识,以及 Elasticsearch 如何使用操作日志(translog)快速且安全地确认对客户端的写入。接下来,我们将深入探讨问题,介绍帮助我们的概念,最后解释使我们能够自信地确认对客户端写入的额外安全检查。
持久性承诺和操作日志
当客户端向 Elasticsearch 写入数据时,例如使用 _bulk[1] API,Elasticsearch 会为请求提供一个 HTTP 响应码。Elasticsearch 只有在数据已安全存储时才会提供成功的 HTTP 响应码(200/201)。我们使用操作日志(称为 translog),在确认写入之前将请求追加并存储在其中。translog 允许我们重放未成功持久化到底层 Lucene 索引的操作(例如,如果在我们向客户端确认写入后节点崩溃)。有关 translog 和 Lucene 索引的更多信息,请参阅我们最近关于 thin indexing shards[2] 的博客文章,其中解释了我们如何将 Lucene 索引和 translog 存储在对象存储中。
不知道是最糟糕的 - 问题
主节点将一个分片分配给一个索引节点,该节点负责将传入数据索引到该分片。然而,我们必须考虑这种节点与主节点或集群其他部分失去通信的情况。
在这种情况下,主节点会(在超时后)假设该节点不再运行,并将受影响的分片重新分配给其他节点。之前的分配现在被视为陈旧的。一个陈旧的节点可能仍在运行,并试图索引和持久化它接收的数据。
在这种情况下,可能有两个分片所有者试图确认写入,但彼此之间失去通信,我们需要解决两个问题:
• 避免对象存储中的文件覆盖
• 确保确认的写入不会丢失
无状态的主分片词条 - 递增编号救援
多年来,Elasticsearch 一直使用我们称之为主分片词条的东西。每当主分片被分配给一个节点时,会为该分配分配一个主分片词条。如果一个主分片失败或从未分配状态变为已分配状态,主节点将在重新分配主分片之前递增主分片词条。这为主分片分配和所有权提供了严格的顺序,较高的主分片词条是在较低的主分片词条之后分配的。
在无状态环境中,我们在写入对象存储的索引文件路径中利用主分片词条,以确保不会发生上面描述的第一个问题。如果一个分片失败并被重新分配,我们知道它将有一个较高的主分片词条。一个分片只会在主分片词条特定的路径中写入文件,因此不会有旧的分片分配和新的分片分配写入相同的文件。它们只会写入不同的路径。
主分片词条还用于最终提供持久性保证,后面会详细介绍。
注意,主分片迁移不会递增主分片词条,相反,涉及主分片迁移的两个节点通过一个显式协议移交所有权。
无状态下的协调词条和节点离开生成
Elasticsearch 中的协调子系统是一种用于集群级别协调的强一致机制,包括集群成员资格和集群元数据(统称为集群状态)。
在无状态环境中,该系统也建立在对象存储之上,上传新的集群状态版本。与有状态环境类似,它维护一个称为“词条”的递增编号(在这里我们称之为协调词条,以与上一节描述的主分片词条区分开)。每当一个节点决定启动新的选举,它将在一个新的协调词条中进行,该词条高于之前看到的任何词条(更多详情请参阅这篇博客文章[3])。
在无状态环境中,选举通过一个我们称之为租约文件的对象存储文件进行。该文件包含协调词条和声明该词条的节点,即该词条的当选主节点。
这个文件将帮助我们进行安全检查。如果协调词条仍然相同,我们知道当选主节点没有改变。
然而,仅有协调词条是不够的,因为如果一个节点离开集群,该词条不一定会改变。为了检测一个数据节点是否已离开集群,我们还在租约文件中添加了节点离开生成。这是一个递增的编号,每次一个节点离开集群时递增。当词条改变时,它会从零重置(但在这里我们可以忽略这一点)。
租约文件作为持久化新集群状态的一部分被写入对象存储。在基于新集群状态采取任何行动(如分片恢复)之前,这个写入会先发生。
无状态下的对象存储读写语义
我们使用对象存储来存储无状态环境中的所有数据,因此对象存储的可见性保证非常重要。最终,安全检查建立在这些保证之上。
以下是我们依赖的主要对象存储可见性保证:
• 写后读:在成功写入后,任何读取都将返回新内容。
• 写后列出:在成功写入后,任何匹配新文件的列出操作都将返回该文件。
这些在几年前并不是理所当然的,但现在在 AWS S3、GCP 和 Azure Blob 存储中都可用。
无状态的安全检查
有了上面描述的必要构建块,我们现在可以进行实际的安全检查和安全论证。虽然 translog 保证了写入的持久性,但我们需要确保节点在确认写入之前仍然是分配的索引节点。为此,数据节点需要确定它具有足够新的集群状态,以确定是否可以安全地确认写入。
我们只对非正常事件感兴趣,例如节点崩溃、网络分区等。正常事件如分片迁移通过显式移交来保证其正确性(我们在这篇博客中不深入探讨这一点)。
让我们考虑一个非正常事件,例如主节点检测到持有分片的数据节点不再响应,因此将该节点从集群中移除。我们将在这个背景下检查安全检查,并看看它如何避免陈旧节点可能错误地确认对客户端的写入。
安全检查在响应客户端之前增加了一个额外检查:
• 从对象存储中读取租约文件。如果协调词条或节点离开生成已超过节点本地集群状态中的值,则它不能依赖集群状态,直到它收到一个具有更高或相等协调词条和节点离开生成的更新版本。拥有足够新的集群状态后,可以检查分片的主分片词条是否已更改。如果已更改,写入将失败。
正常路径在这里不会有任何等待,因为词条和节点离开生成相对于正常写入请求频率变化非常不频繁。因此,这个检查的开销很小。
注意,顺序很重要:translog 文件在安全检查之前成功上传。我们很快会看到原因。
非正常节点离开事件导致租约文件中的节点离开生成递增。之后,一个新节点可能会被分配该分片并开始恢复数据(这可能只是一次集群状态更新,但租约文件写入和节点开始恢复的顺序是唯一重要的部分,并且是有保证的)。
新分配的节点将读取分片数据并恢复 translog 中包含的数据。
我们看到以下事件顺序:
• 原始数据节点在读取租约文件之前写入 translog 文件
• 主节点在新数据节点开始恢复之前写入租约文件并递增节点离开生成,因此在读取 translog 之前
• 对象存储保证租约文件和 translog 文件的写后读
有两种主要情况需要考虑:
• 原始数据节点写入 translog 文件并读取租约文件,表明它仍在集群中并拥有分片(主分片词条未更改)。
• 我们知道主节点在数据节点读取租约文件之前没有成功更新租约文件。因此,原始数据节点写入 translog 文件的操作发生在新节点分配读取 translog 文件之前,保证这些操作将为新节点恢复可用。
• 原始数据节点写入 translog 文件,但在可能等待新的集群状态后,根据租约文件中的信息,它不再是分片的所有者(使写请求失败)。
• 我们不会对写请求成功响应,因此不会承诺持久性。
• translog 数据可能在恢复期间对新节点分配可用,但这没关系。失败的请求实际上持久化数据是可以的。
因此,我们看到 Elasticsearch 成功响应的任何写入都将在未来任何同一分片的所有者可用,从而完成我们的安全论证。
类似地,我们可以论证主节点故障转移情况是安全的。在这里,协调词条而不是节点离开生成将会改变。我们在这里不会详细讨论。
同样的安全检查也用于许多其他关键情况:
• 在索引文件删除期间。当 Lucene 合并段时,旧段可以被删除。我们在这里添加了一个安全检查,以防止删除新节点分配需要的文件。
• 在 translog 文件删除期间。当对象存储中的索引数据包含所有操作时,translogs 可以被删除。同样,我们在这里添加了一个安全检查,以防止删除新节点分配需要的 translog 文件。
结论
恭喜你读到这里,希望你喜欢这个深入探讨。我们描述了一种确保 Elasticsearch 在对象存储中持久且安全地持久化写入的新机制,即使在任何导致 Elasticsearch 否则会有两个节点拥有同一分片的索引的情况下。我们非常重视这些方面,如果你也如此,可以看看我们的 招聘信息[4]。
引用链接
[1]
\_bulk: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html[2]
thin indexing shards: https://search-labs-elastic-evj8lqjrb-elastic-web-team.vercel.app/search-labs/blog/thin-indexing-shards-elasticsearch-serverless[3]
这篇博客文章: https://www.elastic.co/blog/a-new-era-for-cluster-coordination-in-elasticsearch[4]
招聘信息: https://jobs.elastic.co/#/




