Quickwit 101 - 基于对象存储的分布式搜索引擎架构

开发 架构
你可能会疑惑,Quickwit 如何能够在索引器每 10 秒提交一次的情况下生成包含 1000 万份文档的 split。答案在于 Quickwit 的合并管道,它负责将一组 split 合并,直到达到一定数量的文档。

在本文中,我们将深入探讨 Quickwit 的架构及其关键组件。本文与 Quickwit 的基准测试 配合阅读效果更佳,该测试基于一个 23TB 的数据集。

  • https://quickwit.io/blog/benchmarking-quickwit-engine-on-an-adversarial-dataset

Quickwit 标志的灵感来源于 Paul 的想法,即软件和动态艺术具有一种有趣的共性。如果你观看 Theo Jansen 的作品,你会看到迷人的风力驱动雕塑在海滩上行走,其复杂程度让人难以理解其工作原理。这种神秘感也类似于使用软件的感受。当我们查看大型代码库时,这些复杂的构造能够正常运行似乎有些神奇……然而,大多数时候它们确实可以正常工作。

  • https://github.com/fulmicoton
  • https://www.youtube.com/watch?v=LewVEF2B_pM&ab_channel=theojansen

但我们是工程师!我们不满足于神奇的现象,我们想要了解事物的工作原理,并且不会仅仅因为软件的美观而做出决定……(是吗?)。确实,如果我们不想让引擎在关键时刻出问题并且能够安心睡觉的话(我也犯过很多这样的错误……),理解引擎内部工作原理是非常重要的。没有必要涵盖所有细节,建立一个系统的良好心理模型就足够了,这样可以理解它的局限性和如何高效地使用它。

这就是本文的目的:深入探讨 Quickwit 的架构及其关键组件,以便你可以建立一个简洁准确的系统心理模型。现在让我们开始吧!

计算与存储解耦

Quickwit 架构的核心原则是计算与存储的分离。我们的方法与 Datadog 通过 Husky 所做的非常相似(但早于他们),目标也是相同的:成本效益和可扩展性,同时避免集群管理的噩梦。

  • https://www.datadoghq.com/blog/engineering/introducing-husky/

这种方法使我们将索引(写路径)和搜索(读路径)完全分开。索引器和搜索器通过元存储共享相同的世界视图。索引器向存储写入数据并更新元存储,而搜索器从存储和元存储读取数据。

在当前的 Quickwit 中,元存储通常由 PostgreSQL 数据库支持。但是 Quickwit 还有一个更简单的实现,其中元存储由存储在对象存储上的简单 JSON 文件支持;对于简单的情况,无需依赖外部数据库。这是本文选择的配置。

图片图片

索引

为了向 Quickwit 提供数据,你通常会将 JSON 文档发送到索引器的摄入 API。索引器然后对这些文档进行“索引”:它将文档流分割成短批次,并为每个批次创建一个索引片段(具体来说是一个文件)。我们可以配置生成这些片段的时间间隔(参见 commit_timeout_secs)。这些片段随后上传到 S3。

  • https://quickwit.io/docs/configuration/index-config#indexing-settings

在上传开始时,索引器将元存储中的片段元数据标记为 Staged。一旦上传完成,状态变为 Published,表示片段可用于搜索。从文档摄入到搜索准备好的时间间隔称为“搜索时间”,大致等于 commit_timeout_secs + upload_time。

这些步骤由一个处理管道实现,详细信息见此 相关文章。

  • https://quickwit.io/blog/quickwit-actor-framework

图片图片

解析 ‘Split’

为了提供搜索查询的上下文,理解 “split” 包含的内容至关重要,因为这是索引的数据单位。它是一个独立的索引,具有自己的模式,并且包含了针对快速有效搜索和分析操作优化的数据结构:

  • 倒排索引:用于全文搜索。对于那些想深入了解的人,我强烈推荐阅读 fulmicoton 的博客文章 1 和 2。

https://twitter.com/fulmicoton

https://fulmicoton.com/posts/behold-tantivy/

https://fulmicoton.com/posts/behold-tantivy-part2/

  • 列式存储:用于排序和分析目的。
  • 行式存储:用于根据文档 ID 检索文档。
  • 热缓存:可以将其视为索引的蓝图。包含前面数据结构的元数据,确保搜索器仅检索必要的数据。热缓存是搜索器检索的第一部分数据;通常保留在内存中。

为了说明这一点,我们来看一个实际例子:一个包含 1000 万条 GitHub 归档事件的 split,其中所有文档字段都启用了所有数据结构。

图片图片

split 的大小约为 15GB。与未压缩的文档大小相比,其压缩比接近 3。需要注意的是,可以通过仅在相关字段上启用特定存储来优化这一比率。

最后,热缓存具有一个很好的特性:它占 split 大小的比例不到 0.1%,大约 10MB,这意味着它可以轻松地放入 RAM 中。

关于合并的一点说明

你可能会疑惑,Quickwit 如何能够在索引器每 10 秒提交一次的情况下生成包含 1000 万份文档的 split。答案在于 Quickwit 的合并管道,它负责将一组 split 合并,直到达到一定数量的文档。这主要有两个原因:

  • 性能提升:你不希望在每次搜索请求时打开许多小的 split。这也大大减少了元存储需要处理的数据量。通过合并,我们最终可以在 PostgreSQL 中为每 1000 万份文档保留一行记录。
  • 成本效益:你希望限制每次搜索请求中的 GET 请求次数。

搜索

在读取侧,当搜索器接收到搜索请求时,它会经历以下步骤:

  1. 元存储检索:搜索器获取 metastore.json 文件。
  2. split 列表:列出与搜索请求相关的 split。这一阶段特别利用了搜索请求的时间范围来排除不符合时间范围的 split。
  3. 叶搜索执行:对于每个 split,执行并发的「叶搜索」。它包括:

(IO) 获取 split 的热缓存。

(IO) 预热阶段:对于每个词条,它获取发布列表的字节范围,然后获取发布列表本身。如果有必要,它还会获取搜索所需的定位信息和列。为了便于阅读,预热阶段在下面的图表中表示为单一的获取,但实际上可能是多次获取。

(CPU) 搜索阶段:在 split 上运行查询。

  1. 结果聚合:聚合叶搜索的结果。
  2. 文档获取:搜索器从相关 split 中获取文档。
  3. 结果返回:最终将匹配的文档和/或聚合返回给用户。

这导致了以下读取路径:

图片图片

元存储

让我们回到索引器和搜索器共享的视图:元存储。它存储了我们在前几节部分看到的关于索引的关键信息:

  • 索引配置:文档映射、时间戳字段、合并策略等。
  • split 元数据:如果你的数据集有一个时间字段,Quickwit 会特别存储每个 split 的最小和最大时间戳值,从而在搜索时启用基于时间的过滤。
  • 检查点:对于每个数据源,一个“源检查点”记录了已处理的文档的截止点。如果你使用 Kafka 作为数据源,检查点包含每个分区的起始和结束偏移量,这些偏移量被索引到 Quickwit 中。这解锁了恰好一次语义。

元存储可以由 PostgreSQL 或存储在对象存储上的单个 JSON 文件支持。后者用于本文,因为它是最简单的配置。以下是元存储内容的一个快照:

metastore.json

{
    "index_uri": "s3://indexes/{my-index}",
    "index_config": {...},
    "splits": [
        {
            "split_id": "...",
            "num_docs": 10060714,
            "split_state": "Published",
            "time_range": {
                "start": 1691719200,
                "end": 1691936694
            },
            "split_footer": {
              "start": 1612940400,
              "end": 1616529599
            }
            ...
        }
    ],
    "checkpoints": {
      "a": "00000000000000000128",
      "b": "00000000000000060187",
      ...
    }
}

你注意到那个令人好奇的 split_footer 字段了吗?那是……热缓存的字节范围!

分布式索引和搜索

分布式索引和搜索引入了一些挑战:

  • 集群形成:Quickwit 使用了一个名为 Chitchat 的开源实现来形成集群,这个实现基于 Scuttlebutt。

https://quickwit.io/docs/overview/architecture#cluster-formation

https://www.cs.cornell.edu/home/rvr/papers/flowgossip.pdf

  • 元存储写入:在写入路径上,只有一个进程应该处理对元存储文件的写入。为此,一个单独的 metastore 角色实例读取/写入 JSON 文件。其他集群成员向此实例发送读取/写入 gRPC 请求。
  • 索引任务分配:一个控制平面角色负责将索引任务分配给各个索引器。
  • 搜索工作负载分配:这需要一个 Map-Reduce 机制。接收搜索请求的搜索器承担“根”角色,将叶子请求委托给“叶子节点”,然后聚合并返回结果。

以下是写入路径的可视化表示:

图片图片

类似地,对于读取路径:

图片图片

要进行更深入的了解,请参阅我们的 文档。

  • https://quickwit.io/docs/overview/architecture
责任编辑:武晓燕 来源: 黑客下午茶
相关推荐

2022-08-15 14:56:30

搜索引擎分布式

2014-11-25 10:09:59

ElasticSear分布式搜索引擎Lucene

2020-07-31 09:55:27

Linux分布式Elasticsear

2011-06-20 18:23:06

SEO

2019-07-10 13:17:07

大数据搜索代码

2022-10-14 07:42:50

LuceneHTTPWeb

2009-02-19 09:41:36

搜索引擎搜狐百度

2009-09-22 16:23:52

搜索引擎

2018-10-16 14:26:22

分布式块存储引擎

2020-03-20 10:14:49

搜索引擎倒排索引

2017-08-07 08:15:31

搜索引擎倒排

2024-03-18 00:00:01

分布式搜索引擎

2016-12-26 13:41:19

大数据搜索引擎工作原理

2010-06-13 16:27:28

搜索引擎

2023-12-07 09:17:44

java分布式

2010-04-20 11:43:46

2012-09-07 13:22:21

搜索搜狗

2022-10-08 09:13:18

搜索引擎⽹站

2011-06-22 17:28:51

SEO

2012-05-14 11:01:50

搜索引擎微软
点赞
收藏

51CTO技术栈公众号