dot 快速的未来即将在您所在的城市举办活动。

加入我们参加 Redis 发布会

我们如何使用 RediSearch 1.4.1 提高垃圾回收性能

背景

从 4.0 版本开始,Redis 公开了新的模块 API,允许程序员通过新的命令和数据类型扩展数据库的功能。 RediSearch 利用它来允许对存储在 Redis 中的数据进行全文搜索。数据保存在 Redis 中作为哈希(Redis 核心数据类型),每个数据记录称为文档。文档的数据也在 RediSearch 的内部数据结构中进行索引。可以告诉 RediSearch 不要将数据存储在哈希中,在这种情况下,文档只会在 RediSearch 的内部数据结构中进行索引(此模式称为 NOSAVE)。

垃圾回收 (GC) 挑战

搜索引擎要解决的一个主要挑战是删除和更新文档。为了理解这个问题,我们首先需要了解 RediSearch 如何索引数据。例如,以下文档

Doc1
名称: “test”
正文: “这是一个示例”
RediSearch 将创建以下 倒排索引

术语 DocId
测试 Doc1
Doc1
Doc1
一个 Doc1
例子 Doc1

当用户搜索术语“example”时,RediSearch 会立即在倒排索引中找到该术语,并将 Doc1 作为查询的结果返回。

现在想象一下,我们需要通过扫描 Doc1 中的所有术语并将它的记录从每个术语中删除来从索引中删除 Doc1。问题是,在 NOSAVE 模式下运行时,我们没有 Doc1 中的数据,我们也不知道哪些术语需要清理。可以跟踪文档中的每个术语,但这会显著增加整体内存占用。

唯一的另一个解决方案是创建一个“垃圾回收”(GC)机制,在后台扫描整个索引,并将已删除的文档从倒排索引中删除。RediSearch 以前使用过这种方法。但是,随着产品的发展,我们发现它可能非常慢,因为它需要扫描整个索引。此外,由于 Redis 是单线程的,在扫描期间我们必须获取全局(单个)锁,这使得对整个数据集执行查询或更新成为不可能。

解决方案

为了解决这些问题,我们提出了一种新的 GC 方法,该方法利用了 Linux fork 进程的优势。我们的目标是在尽可能短的时间内获取全局锁,即在扫描期间不获取锁,而是在实际内存释放期间获取锁。每次 GC 启动时,它都会创建一个 fork 进程,该进程在后台扫描索引,而主进程继续执行查询和更新。fork 进程会通知主进程哪些内容需要删除,然后主进程会在很短的时间内获取锁(这足以从倒排索引中删除相关文档)。

基准测试

我们通过将 500 万条记录插入数据库,然后连续更新所有记录来比较这种新的 GC 实现与我们以前的方法。

注意:此比较是在使用以下规格的简单笔记本电脑配置上完成的:MacBook Pro(视网膜,15 英寸,2015 年中),OS 10.11.6,CPU 2.2 GHz 英特尔酷睿 i7,16 GB 1600 MHz DDR3 内存。我们可以在更强大的服务器配置上运行它,并可能获得更令人印象深刻的结果,但我们认为这些规格足以证明我们新方法的优势。

  旧 GC 新 GC
GC 收集的总字节数 908 KB 80,727 KB
插入阶段时间(以秒为单位) 484.657 447.099
更新/删除阶段时间(以秒为单位) 627.632 615.326
插入阶段后的内存使用量 1.73 Gb 1.73 Gb
更新/删除阶段后的内存使用量 1.84 Gb 1.74 Gb

如您所见,我们新的 GC 方法的主要优势是,RediSearch 现在可以收集 80,727 KB,而使用旧的 GC 机制只能收集 908 KB,这几乎是 100 倍的改进。旧的 GC 选择倒排索引并随机清理它的概率方法花费了更多的时间。我们的新 GC 方法在每次迭代中都在整个索引上运行,因此在完成更新时可以清理所有垃圾。此外,新方法在扫描索引时不需要获取全局锁。

启用新的 GC

新的 GC 机制现已在我们的最新 RediSearch 版本中提供 - 1.4.1 版本。默认情况下它处于关闭状态,但可以通过在加载模块时在命令行参数中发送“GC_POLICY FORK”来激活它。请记住,这是一个实验性的 GC 版本,因此我们不建议您在生产环境中使用它。但是,如果您的用例需要大量更新,欢迎您尝试此新的 GC 并给我们反馈。我们打算在未来的版本中将其设置为 RediSearch 的默认配置。

未来的工作

我们计划随着时间的推移扩展我们的 GC 机制,以进一步提高 GC 性能。我们正在考虑的一种方法是使用启发式机制来指示哪些术语更有可能包含垃圾,并首先清理这些术语。