作者:来自 Elastic Tanguy Leroux
在本文中,我们将介绍 Elasticsearch 的精简索引分片(thin indexing shards),这是我们为 Elastic Cloud Serverless 开发的一种新型分片,允许将 Elasticsearch 索引存储在云对象存储中。
我们首先回顾一下 Elasticsearch 目前如何存储和复制数据,然后再深入探讨精简索引分片为远程存储数据而引入的变化。我们将看到,精简索引分片之所以得名,是因为它们管理数据文件的生命周期,从在本地磁盘上创建到在对象存储中上传,再到最终从磁盘中删除,这只能暂时增加磁盘空间。然后,我们将描述文件在对象存储中持久化后如何读取,以及我们如何通过使用基于块的缓存来缓解云存储服务的高延迟。最后,我们将谈谈我们未来的改进计划。
有状态 Elasticsearch 如何在本地磁盘上存储和复制数据
Elasticsearch 管理两种主要类型的数据:
- 集群状态,这是一种内部数据结构,包含 Elasticsearch 实例正常运行所需的各种信息。它通常包含集群中其他节点的身份和属性、正在运行的任务的当前状态、集群范围的设置、索引元数据及其映射等。
- 索引,以文档的形式包含用户的业务数据,这些文档由 Elasticsearch 索引,以便可以搜索或聚合它们。
本文重点介绍索引,即存储文档的地方。索引代表 Elasticsearch 集群中存储的总数据量的最大部分,它可以由多个节点上的许多索引组成。索引被分成称为主分片的分片,用于在节点之间分配文档量以及搜索和索引负载。主分片还可以有副本:当文档在主分片中被索引时,它也会在其副本中被索引,以确保数据安全地保存在多个位置。这样,如果某个分片因某种原因丢失或损坏,就会有另一个副本可供恢复。请注意,集群状态也会在本地磁盘上的一组节点(由特定的主节点角色标识)中保存。
我们将此模型描述为 “节点到节点” 复制模型,其中每个节点都是 “stateful - 有状态的”,因为它们依赖本地磁盘来安全且持久地保存其托管的分片的数据。在此模型中,有状态的 Elasticsearch 实例始终必须进行通信以保持主分片和副本分片同步。这是通过将写入操作(新文档索引、更新或删除)从主分片复制到副本分片来实现的。复制并持久保存后,该操作将确认回客户端应用程序:
虽然这种有状态架构对我们很有用,但复制每个操作在资源方面都有不可忽略的成本。主分片节点和副本分片节点都需要 CPU 来提取文档。网络涉及将操作从一个节点传输到另一个节点,当节点位于不同的可用区域时,大规模传输的成本可能很高。最后,需要存储来将数据持久保存在多个磁盘上。
在 Elastic Cloud Serverless 中,我们利用使用云存储服务的优势为 Elasticsearch 实施了一种新的无状态(stateless)模型。
在 Stateless 中有什么变化?
我们在无状态 Elasticsearch 中实现了两项重大变化:
- 我们将分片数据的持久性从本地磁盘转移到对象存储;
- 我们将复制模型从在节点之间复制操作更改为通过对象存储复制分片文件。
结合起来,这两项变化带来了一些有趣的改进。将分片的持久性从磁盘转移到对象存储基本上意味着分片文件只在磁盘上临时保留:创建后不久,文件就会上传到对象存储并从本地磁盘中删除。如果必须再次读取文件,则会从对象存储中检索该文件并将其本地存储在基于块的缓存中以供将来读取。一旦文件持久地保存在对象存储中,所有节点都可以访问它们。不再需要在多个节点上维护分片副本,也不需要复制操作,因此我们可以大大简化我们的复制模型。
索引和搜索层
为了避免现有模型和新的 stateless 模型之间的混淆,我们引入了一些新术语:
- 负责索引文档并将分片文件上传到对象存储的分片称为 “indexing shard - 索引分片”,并自动分配给 Index Tier 中的节点。由于这些分片不需要在索引节点上本地存储完整数据集,因此我们将它们称为精简索引分片(thin indexing shards)。
- 负责搜索文档的分片简称为 “search shard - 搜索分片”。这些分片分配在 Search Tier 的节点上。
让我们说明现有有状态模型和新的 stateless 模型之间的区别:
好处
新的 statelss 模型有很多好处。通过删除写入操作的复制,我们可以节省 CPU、网络和磁盘等硬件资源。这些资源现在可以专用于提取更多数据,或者换个角度看,可以用更少的资源提取相同数量的数据。将数据保存在一个高度可用的地方可以独立扩展每个层。向层添加更多索引或搜索节点不会受到本地磁盘性能或节点间网络性能的支配。对象存储的成本通常比快速本地 SSD 磁盘便宜,可以帮助降低运行 Elasticsearch 实例的总体成本。
现在我们已经看到了 stateless 架构的整体情况,让我们深入了解实现细节。
从本地磁盘到云存储
在 Elasticsearch 索引中索引文档涉及多个步骤。文档可以通过摄取管道进行摄取,该管道在将文档路由到索引的主分片之一之前对其进行转换或丰富。在那里,文档的来源被解析,任何版本冲突都得到解决。然后,文档被编入主分片并转发到副本分片,在那里它也被编入索引。如果所有这些操作都成功,索引操作将被确认回客户端应用程序。
当文档被编入分片时,它实际上在两个不同的地方被编入索引:首先在 Lucene 中,然后在 translog 中。
Elasticsearch 使用 Lucene 来索引和搜索文档。每次将文档索引到 Lucene 中时,Lucene 都会分析该文档,以构建各种内部数据结构,这些结构的类型取决于我们计划稍后对文档执行的搜索查询。为了保持快速索引,数据结构保存在 Lucene 索引缓冲区的内存中。当足够的数据被索引到内存中时,Lucene 会刷新其内存缓冲区以将数据结构写入磁盘,从而创建一组称为段(segment)的不可变文件。将段写入磁盘后,它所包含的索引文档子集即可搜索。请注意,Elasticsearch 通过每秒刷新 Lucene 缓冲区并打开新段来自动使新索引的文档可搜索:我们在 Elasticsearch 术语中将此称为刷新(refresh)。
Lucene 段文件在创建后永远不会改变,这一事实使它们非常适合缓存:操作系统可以直接在内存中映射文件(或文件的一部分)以加快访问速度。我们稍后会看到,这种不变性还简化了我们在精简索引分片(thin indexing shards)中使用的基于块的实现。不可变的段文件还意味着文档的更新和删除会创建新的段,其中文档的先前版本被软删除(soft deleted),并且可能添加新版本。随着时间的推移,Lucene 会将小段合并为单个、更优化的段,以维护一个高效的 Lucene 索引,并回收更新/删除的文档留下的未使用空间。
Lucene 合并,同时索引所有维基百科(英文)
在文档被积极索引的分片中,段会不断创建和合并,小段只会在磁盘上保留很短的时间。因此,Lucene 不会指示操作系统确保将文件的字节持久写入存储设备。相反,文件可以保留在操作系统的文件系统缓存中,位于内存中,在那里访问它们的速度比在磁盘上快得多。因此,如果托管分片的节点崩溃,段文件就会丢失。
为了安全地将段文件保存在磁盘上,Elasticsearch 确保定期创建 Lucene 提交(commits),这是一个相对昂贵的操作,不能在每次索引或删除操作后执行。为了能够恢复已确认但尚未提交的操作,每个分片都会将操作写入称为 translog 的事务日志中。如果发生崩溃,可以从 translog 中重放操作。
为了确保 translog 不会变得太大,Elasticsearch 会自动执行 Lucene 提交并创建一个新的空 translog 文件。这个过程在 Elasticsearch 术语中称为刷新(refresh),在后台执行。
在 Stateless 中有什么变化?
在前面的部分中,我们描述了 stateful Elasticsearch 中索引文档的逻辑。此逻辑在 statess 中几乎保持不变,不同之处在于,磁盘上文件同步提供的持久性保证已被上传文件到对象存储所取代。
Lucene 提交文件
对于 Lucene,索引文档和刷新索引会继续在本地磁盘上生成段文件。刷新分片时,Lucene 提交文件(即提交中包含的所有段文件加上额外的提交点文件)将由精简索引分片上传到对象存储。为了减少对对象存储的写入次数(写入次数比读取次数要昂贵得多),Lucene 提交文件在上传期间会连接在一起形成单个 blob。此 blob 对象由包含有关分片和提交的信息的标头以及提交中包含的文件的完整列表组成。此列表引用了每个文件的名称、大小和位置。这个位置很重要,因为某些文件可能是由之前的提交创建的,因此位于对象存储中的不同 blob 中。在标头之后,提交添加的新文件将简单地附加到 blob 中。
将多个文件打包到同一个 blob 中有助于降低 PUT 请求引起的成本,但也有助于减少上传提交所需的时间:对对象存储的请求通常具有较高的延迟(第 99 个百分位数在 130 毫秒范围内),但可以提供较高的读/写吞吐量。通过将文件连接到同一个 blob 对象中,我们只需为多个文件支付一次延迟,就可以足够快地上传或下载它们。请注意,为了降低刷新成本,我们还将多个提交上传到单个 blob 中。如果你有兴趣了解有关此主题的更多信息,我们最近写了一篇关于此的文章。
上传后,可以从本地磁盘中删除 Lucene 提交文件。如果我们将分片的总大小(已上传到对象存储中)与它在本地磁盘上实际占用的大小进行比较,我们可以看到,只有一部分分片是索引所必需的。一旦索引减少或停止,这个大小就会降至零:
每 200 毫秒上传一次 Translog
对于 translog,情况就不同了。不可能在每个分片的每次操作之后都上传 translog 文件:上传请求的成本会过高,而使用对象存储产生的高延迟会大大降低索引性能。相反,在 stateless,我们决定每 200 毫秒或达到 16MiB 时上传一次 translog(以先到者为准),我们还决定将同一节点上所有分片的 translog 文件串联成一个节点 translog 文件。
Blob 缓存:上传后访问文件
Lucene 和 translog 文件上传后,会从磁盘中删除。但索引文档可能需要再次读取一些文件:查找文档 id、文档中字段的值,或加载现有源以应用更新脚本。在这种情况下,精简索引分片需要从对象存储中获取所需的文件(或其中的一部分)。但高延迟和对象存储 API 请求的成本使 Elasticsearch 无法每次都从对象存储中读取必要的字节。相反,我们在对象存储前面实现了一个基于块的缓存来缓存 Blob 块。这样,当 Lucene 需要从文件中读取一些字节时,缓存会从对象存储中获取相应的数据块(通常是 16MiB 块)并将其本地存储在磁盘上以供将来读取。为了获得更好的性能,精简索引分片会在上传 blob 时预热缓存,以便在分片从本地文件转换为相应的上传 blob 时,提交文件的相关部分已经缓存。
此 blob 缓存的灵感来自可搜索快照,并使用 LFU 算法逐出不常用的 blob 块。它可以在最小的无服务器节点上存储大约 50 GiB 的数据。
更快的分片恢复
分片恢复是在 Elasticsearch 节点上重建分片的过程。当为新索引创建空分片时、当分片需要从一个节点重新定位到另一个节点时或当分片需要从对象存储中恢复时,将执行此恢复。在 stateless 的情况下,重新定位分片不需要在节点之间完全复制所有段文件。相反,Elasticsearch 使用缓存从对象存储中获取唯一必要的 blob 块,以允许启动分片。它通常代表分片总大小的一小部分,并允许分片更快地启动。
结论和未来的改进
在本文中,我们介绍了如何将主存储从本地磁盘更改为对象存储。我们利用精简索引分片,在本地部分缓存数据以节省本地存储成本,而不会影响吞吐量。我们看到了这给我们的用户带来的硬件资源和总体成本方面的所有收益。
虽然我们大幅减少了索引文档所需的磁盘空间,但我们现在正在考虑改进精简索引分片使用的内存。今天,每个索引分片都需要几 MiB 的内存来存储索引设置、映射和 Lucene 数据结构。在不久的将来,我们希望精简索引分片在没有执行活动索引时几乎不需要内存,并且在需要索引时按需重新填充对象。我们还在考虑改进 blob 缓存。我们使用的算法适用于大多数用例,但我们正在考虑探索替代方案。最后,我们正在为 Lucene 做出贡献,以便在从对象存储中获取字节时实现更多并行化。
准备好自己尝试一下了吗?开始免费试用。
想要获得 Elastic 认证?了解下一期 Elasticsearch 工程师培训何时开课!
原文:Elasticsearch Serverless: Thin Indexing Shards — Search Labs