blog-post

关于 Manticore Search 中的列式存储

介绍

在本文中,我们将探讨 Manticore 列式存储的目的,它与行式存储的不同之处,以及在什么情况下使用它是有意义的。我们还将熟悉存储格式的基本结构及其在搜索守护进程的查询处理工作流中的集成细节。

默认属性存储(行式)

在 Manticore 中,有两个不同的实体:仅支持全文查询的全文字段和各种类型的属性,可用于分组、排序和过滤。默认存储引擎 (engine='rowwise') 将所有文档的所有属性存储在内存中。

为了将属性加载到内存中,使用 mmap,可以通过选项 access_plain_attrsaccess_blob_attrs 进行配置。第一个选项负责加载 .spa 文件,这些文件包含所有固定长度的属性(整数、 bigint、浮点数等)。第二个选项用于加载 .spb 文件,这些文件包含可变长度的属性(字符串、mva、float_vector 等)。由于 mmap 仅在访问时将数据加载到内存中,因此初始使用属性的查询可能会较慢。为了解决这个问题,默认将 ‘access_plain_attrs’ 和 ‘access_blob_attrs’ 设置为 ‘mmap_preread’ 模式,在该模式下,Manticore 会在启动时通过后台线程读取 .spa.spb 文件。这通常意味着(但不保证)属性文件会保留在内存中,从而避免在查询过程中从磁盘读取数据的需要。然而,如果系统决定可用内存不足,属性可能会部分或完全从内存中卸载,从而再次降低查询速度。为了避免这种情况,可以在 access_plain_attrs / access_blob_attrs 中指定 ‘mlock’。如果内存充足,属性将有保证保留在内存中,系统不会将其卸载。然而,如果内存不足,则无法保证。

为什么我们需要列式存储?

在可用内存足以存储所有属性的情况下,传统的 ‘行式’ 存储工作效率高。当没有足够的内存来容纳所有属性时,就会出现问题。
实际上,mmap 可以根据需要自动卸载和重新加载属性文件的部分内容,即使在内存有限的情况下也能进行操作。然而,实际上这种方法会显著降低性能,达到不可接受的水平。问题部分在于行式存储架构,该架构将一个文档的所有属性顺序存储,然后是下一个文档的属性,依此类推。以下是这样的数据表的示例:

.spa 文件具有以下结构:

.spb 文件看起来是这样的:

当查询包括按特定属性分组时,仅需要检索该属性对于每个文档的值。然而,mmap 的工作方式是它无法读取单个字节;相反,它加载一个或多个内存页,每个页通常为 4 KB。这意味着,在尝试读取一个属性的值时,系统通常会加载所有邻近属性的值,即使在查询处理中并不需要它们。

rowwise 存储的另一个细微差别是缺乏数据压缩。由于它通过直接内存访问操作,代码假设随时可用的数据被立即提供。此外,同一文档内不同属性的数据是异构的,这使得有效压缩单个文档变得困难。此外,这种存储格式中没有可以压缩的文档块的概念。

列式存储 (engine='columnar') 正是为那些没有足够内存加载所有属性的情况而开发的。此存储格式提供以下优势:

  • 每个属性的数据单独存储,可以在不影响其他属性的情况下读取。
  • 由于单个属性中的数据通常是同质的,因此可以进行压缩。
  • 属性值可以被划分为多个文档的块以进行压缩。
  • 在解压块后,可以应用流处理优化。
  • 一小部分元数据存储在 RAM 中,其余存储在磁盘上。
  • 为了快速访问热数据,而不是通过 mmap 将页面加载到内存中,使用文件系统缓存。

从图示来看,列式存储中的数据结构可以表示如下:

列式存储作为一个名为 MCL(Manticore 列式库)的独立库提供。该库负责创建存储、打包和读取数据。
此外,搜索守护进程本身添加了大量代码以处理属性,考虑了列式存储的特性。

最初,守护进程实现了 rowwise 存储,旨在进行随机数据访问。可以简单地用列式存储替换内存中的数据访问,但如果不考虑列式存储设计用于流处理并在压缩块中存储数据,性能将会严重下降。

以下是一些为与列式存储协同工作的守护进程而添加的示例:

  • 列式排序器
    在 Manticore 的架构中,通过全文搜索找到的文档会立即发送到所谓的排序器。排序器可以简单地排序,也可以对文档进行分组、计算聚合等。然而,在列式存储中访问属性的速度较慢,因为值是逐个检索的。
    如果查询相对轻量——意味着全文搜索很快或根本不存在——则逐个从存储中检索属性可能会显著影响性能。因此,在某些情况下,使用在标准排序器之上的额外排序器是有益的。这个额外的排序器并不排序,而是累积文档并批量检索列式属性,然后将文档传递给主排序器进行最终处理,这显著更快。

  • 列式分组器
    Manticore中的分组器负责将传入的文档转换为一个或多个分组键,以便随后传递给排序器。列式分组器的主要目标是通过减少 虚拟调用 的数量来提高性能。例如,当常规分组器工作时,它会请求从存储中检索数据的表达式的数据。这个表达式是在守护进程端实现的,提供对不同类型的属性存储的透明访问。从内部来看,它使用一个列式迭代器(下面讨论),这个迭代器是在MCL库端实现的,直接访问存储。然而,专业的列式分组器知道它只会与列式存储一起工作,并去掉了一些抽象,直接与列式迭代器一起工作。
    当按字符串分组时,使用不同的机制。当字符串被添加到列式存储时,自动计算并存储每个字符串的哈希(考虑当前的 字符集 )作为一个单独的整数属性。列式分组器从列式存储中检索这些哈希,而不是读取字符串本身并计算哈希作为分组键。

  • 类似地,通过移除列式表达式,列式过滤器和列式聚合在守护进程中被实现,该表达式通过列式迭代器检索当前文档的属性值。

  • 查询优化器( 基于成本的优化器 ,CBO)也需要了解列式存储。CBO确定查询的执行路径。例如,它可以将一个过滤器替换为通过相应的二级索引进行搜索,从查询中移除过滤器。因此,二级索引将返回文档编号(行ID),其余过滤器将在其上进行操作。
    当使用列式存储时,优化器必须比较二级索引和列式分析器的性能(后者在列式存储上执行快速搜索,更多内容见下文)。根据数据和可用线程的数量,有时一种方法比另一种方法速度更快。例如,列式分析器的并行化效果更好。

MCL中包含什么?

在直接属于MCL库的组件中,可以区分出两个主要组:

1.构建器
负责从守护进程接收原始数据并构建列式存储。

2.访问器
负责数据访问。主要有两种类型:

  • 迭代器
    一种较慢的访问存储的方法。迭代器可以移动到指定文档并提取属性值。它的工作速度较慢,因为它并未适应流处理。然而,在某些情况下,守护进程无法以流式方式处理数据,这时使用列式迭代器。

  • 分析器
    一种显著更快的数据访问方法。分析器将一组过滤器作为输入,并输出满足这些过滤器的文档编号(行ID)。它们工作迅速,因为在数据处理期间使用列式存储的所有可用元数据,并且能够在单次调用中立即解包并处理多个文档。例如,如果块的最小-最大值表明所需值不存在,它们可以完全跳过数据块(而不解包它们)。
    在守护进程中,此类型的访问是基于CBO分析自动启用的,但也可以通过索引提示手动启用;此类型的提示称为ColumnarScan。

结论

在本文中,我们广泛介绍了Manticore列式存储的原理以及其与Manticore搜索守护进程集成的一些细节。

安装Manticore Search

安装Manticore Search