无状态世界中的数据安全

我们讨论了无状态环境下的数据持久性保证,包括我们如何使用安全检查来阻止新写入和删除操作,防止陈旧节点确认新的写入或删除操作。

在最近的博文中,我们宣布了支撑 Elastic Cloud Serverless 产品的无状态架构。通过将持久性保证和复制卸载到对象存储(例如,Amazon S3),我们获得了许多优势和简化。

历史上,Elasticsearch 依赖本地磁盘持久性来保证数据安全并处理陈旧或隔离的节点。在本博客中,我们将讨论无状态环境下的数据持久性保证,包括我们如何使用安全检查来阻止陈旧节点不安全地确认这些操作。

在接下来的博文中,我们将介绍持久性承诺的基础知识,以及 Elasticsearch 如何使用操作日志 (translog) 快速安全地向客户端确认写入操作。接下来,我们将深入探讨这个问题,介绍有助于解决问题的概念,最后解释使我们能够自信地向客户端确认写入操作的附加安全检查。

持久性承诺和 translog

当客户端使用例如 _bulk API 向 Elasticsearch 写入数据时,Elasticsearch 将为请求提供 HTTP 响应代码。只有在数据安全存储后,Elasticsearch 才会提供成功的 HTTP 响应代码 (200/201)。我们使用操作日志(称为 translog),在确认写入之前将请求追加并存储在其中。translog 允许我们重放尚未成功持久化到底层 Lucene 索引的操作(例如,如果节点在我们向客户端确认写入后崩溃)。有关 translog 和 Lucene 索引的更多信息,请参阅我们最近关于 精简索引分片 的博文中 此部分,我们解释了我们现在如何在对象存储中存储 Lucene 索引和 translog。

不知道是最糟糕的 - 问题

主节点将分片分配给索引节点,然后该节点负责将传入数据索引到该分片中。但是,我们必须考虑该节点与主节点和/或集群其余部分失去通信的场景。

在这种情况下,主节点(超时后)将假定该节点不再运行,并将受影响的分片重新分配给其他节点。之前的分配现在将被视为陈旧。陈旧节点可能仍在运行,试图索引和持久化它接收到的数据。

在这种情况下,可能有两个分片所有者试图确认写入,但彼此之间无法通信,我们需要解决两个问题:

  • 避免对象存储中的文件覆盖
  • 确保已确认的写入不会丢失

主项 - 递增数字来救援

多年来,Elasticsearch 一直使用我们称为主项的东西。每当将主分片分配给节点时,都会为该分配提供一个主项。如果主分片失败或从未分配变为分配,则主节点将在重新分配主分片之前递增主项。这提供了主分片分配和所有权的严格顺序,较高的主项是在较低的主项之后分配的。

对于无状态,我们在写入对象存储的索引文件路径中使用主项,以确保上述第一个问题不会发生。如果分片失败并被重新分配,我们知道它将具有更高的主项。分片只会在特定于主项的路径中写入文件,因此旧的分片分配和新的分片分配不会写入相同的文件。它们只是写入不同的路径。

主项最终也用于提供持久性保证,稍后将详细介绍。

请注意,主分片重新定位不会递增主项,而是参与主分片重新定位的两个节点通过显式协议进行所有权移交。

协调项和节点离开代

Elasticsearch 中的协调子系统是一种强一致性机制,用于集群级协调,包括集群成员资格和集群元数据(统称为集群状态)。

在无状态中,此系统也基于对象存储构建,上传新的集群状态版本。与有状态一样,它为选举维护一个递增的数字,称为“项”(为了与上一节中描述的主项区分,我们将它称为协调项)。每当节点决定启动新的选举时,它都将在新的协调项中进行,该协调项高于任何先前看到的项(有关此在有状态中如何工作的更多详细信息,请参阅 此处 的博文)。

在无状态中,选举通过我们称为租约文件的对象存储文件进行。此文件包含协调项以及声称该项的节点(该项的当选主节点)。

此文件将有助于我们在此感兴趣的安全检查。如果协调项保持不变,我们就知道当选的主节点没有更改。

但是,仅协调项是不够的,因为如果节点离开集群,这并不一定会改变。为了检测数据节点是否未离开集群,我们还将节点离开代添加到租约文件中。这是一个递增的数字,每次节点离开集群时都会递增。当项更改时,它会从零重置(但在这里我们可以忽略这一点)。

租约文件作为持久化新集群状态的一部分写入对象存储。此写入发生在基于新集群状态采取任何操作(如分片恢复)之前。

对象存储写入后语义

我们在无状态中使用对象存储来存储所有数据,因此必须考虑对象存储的可见性保证。最终,安全检查建立在这些保证之上。

以下是我们依赖的主要对象 存储 可见性 保证

  • 写入后读取:成功写入后,任何读取都将返回新内容。
  • 写入后列出:成功写入后,任何与新文件匹配的列出都将返回该文件。

几年前,这些并非理所当然,但如今在 AWS S3、GCP 和 Azure Blob 存储中都可用。

安全检查

有了上述必要的构建块,我们现在可以继续进行实际的安全检查和安全论证。虽然 translog 保证写入的持久性,但我们需要确保节点在确认写入之前仍然是分配的索引节点。该节点的真实来源在集群状态中,因此数据节点需要建立它具有足够新的集群状态,以便确定是否可以安全地确认写入。

我们只关注非优雅事件,例如节点崩溃、网络分区等。优雅事件(如分片重新定位)通过保证其正确性的显式移交来处理(我们不会在本博文中深入探讨这一点)。

让我们考虑一个非优雅事件,例如主节点检测到持有分片的数据节点不再响应,因此它将该节点从集群中弹出。我们将在此上下文中检查安全检查,并了解它如何避免陈旧节点可能错误地向客户端确认写入。

安全检查在响应客户端之前添加一个额外的检查:

  • 从对象存储读取租约文件。如果协调项或节点离开代已经超过节点本地集群状态中的值,则它不能依赖集群状态,直到它收到具有更高或相等协调项和节点离开代的更新版本。使用足够新的集群状态,可以用来检查分片的主项是否已更改。如果已更改,则写入将失败。

在理想情况下,这里不会产生任何等待,因为与正常的写入请求频率相比,term 和 node-left generation 的变化非常不频繁。因此,此检查的开销很小。

请注意,顺序很重要:事务日志文件 (translog file) 在安全检查之前成功上传。我们很快就会看到原因。

非正常节点离开 (ungraceful node-left) 事件会导致租约文件 (lease file) 中的 node-left generation 自增。之后,新的节点可能会被分配到分片并开始恢复数据(这可能只是一个集群状态更新,但租约文件写入和节点开始恢复的顺序是这里唯一重要的部分,并且是保证的)。

新分配的节点将读取分片数据并恢复事务日志中包含的数据。

我们可以看到事件的顺序如下:

  • 原始数据节点在读取租约文件之前写入事务日志。
  • 主节点在新的数据节点开始恢复(以及在读取事务日志之前)之前,写入带有递增 node-left generation 的租约文件。
  • 对象存储保证租约文件和事务日志文件的写后读 (read-after-write)。

需要考虑两种主要情况:

  • 原始数据节点写入事务日志文件并读取一个租约文件,该文件表明它仍然在集群中并且是分片的所有者(主 term 没有更改)。
    • 然后我们知道主节点在数据节点读取租约文件之前没有成功更新租约文件。因此,原始数据节点对事务日志的写入发生在新节点分配读取事务日志之前,保证这些操作可用于新节点进行恢复。
  • 原始数据节点写入事务日志文件,但在可能根据租约文件中的信息等待新的集群状态之后,它不再是分片的所有者(使其写入请求失败)。
    • 我们没有成功响应写入请求,因此没有承诺持久性。
    • 事务日志数据在恢复期间可能对新节点分配可用,但这没关系。对于失败的请求实际上具有持久保存的数据是可以的。

因此,我们可以看到 Elasticsearch 成功响应的任何写入都将可用于同一分片的任何未来所有者,从而得出我们的安全论证。

类似地,我们可以论证主节点故障转移情况是安全的。这里协调 term 而不是 node-left generation 将发生变化。我们这里不再赘述。

同样的安全检查用于许多其他关键情况:

  • 在索引文件删除期间。当 Lucene 合并段时,可以删除旧段。我们在这里添加安全检查以防止删除较新的节点分配需要的文件。
  • 在事务日志文件删除期间。当对象存储中的索引数据包含所有操作时,可以删除事务日志。同样,我们在这里添加安全检查以防止删除较新的节点分配需要的事务日志文件。

结论

恭喜您坚持到了最后,希望您喜欢这里的深入探讨。我们描述了一种新机制,用于确保 Elasticsearch 持久且安全地将写入持久保存到对象存储中,即使在任何类型的中断导致 Elasticsearch 拥有两个节点写入同一分片的情况下也是如此。我们非常重视这些方面,如果您也重视,或许可以查看我们开放的 职位招聘

感谢 David Turner、Francisco Fernández Castaño 和 Tim Brooks 完成了这里的大部分实际工作。

了解更多关于 Elastic Cloud Serverless 的信息,并开始 14 天的 免费试用 以自行测试。

准备好构建最先进的搜索体验了吗?

仅仅依靠个人的努力无法实现足够先进的搜索。Elasticsearch 由数据科学家、ML 运维工程师和其他许多同样热衷于搜索的人员提供支持,他们对搜索的热情与您一样高。让我们联系起来,一起构建神奇的搜索体验,让您获得想要的结果。

亲自试一下