Nrtsearch 1.0.0:增量备份、Lucene 10与更多更新
自我们发布第一篇Nrtsearch博客文章已过去3年多,自我们在生产环境使用基于Lucene的搜索引擎Nrtsearch已超过4年。目前我们已将超过90%的Elasticsearch流量迁移至Nrtsearch。我们很高兴宣布Nrtsearch 1.0.0的发布,该版本包含自初始版本以来的多项新功能和改进。
术语表
- EBS(弹性块存储):AWS中网络连接的块存储卷
- HNSW(分层可导航小世界):基于图的近似最近邻搜索技术
- Lucene:Nrtsearch使用的开源搜索库
- S3:AWS提供的云对象存储
- Scatter-gather:将请求发送到多个节点并将其响应组合以创建结果的模式
- Segment:Lucene索引中的子索引,可以独立搜索
- SIMD(单指令多数据):对多个数据点执行相同操作的CPU指令,可使向量搜索等操作更快更高效
主要变更
提交时的增量备份
Nrtsearch现在在每次提交时都会向S3执行增量备份,此备份用于启动副本节点。此变更背后的动机和更多细节将在下文讨论。
初始架构
回顾第一篇Nrtsearch博客中的架构 - 当调用提交时,主节点将Lucene索引段刷新到本地挂载的网络存储(在我们的案例中为Amazon EBS)。这保证了最后一次提交之前的所有数据都将在EBS卷上可用。如果主节点重启并移动到不同实例,我们不需要将索引下载到新磁盘;相反,我们只需要附加EBS卷,使主节点能够在一分钟内启动,减少索引停机时间。我们偶尔在主节点上调用备份端点,将索引备份到S3。当副本启动时,它们从S3下载最新的索引备份,然后从主节点同步更新。当主节点索引文档时,它向所有副本发布更新,副本可以从主节点拉取最新段以保持最新状态。
架构缺点
这种架构对我们来说大部分工作良好,但存在一些缺点:
- EBS卷是事实来源。如果EBS丢失、损坏或调整大小时间过长,我们将不得不重新索引所有数据
- EBS移动不如预期顺利。有时EBS卷无法从旧节点正确卸载,然后新节点需要一些时间来挂载它
- 高写入集群需要频繁备份整个索引,以便副本在下载索引后不必花费太多时间追赶主节点
切换到临时本地磁盘和提交时增量备份
为避免这些缺点,我们希望使用临时本地磁盘而不是EBS卷。使用本地磁盘有两个障碍:
- 确保重启主节点不会导致上次备份与最近提交之间所做的更改丢失
- 确保主节点能够足够快地下载索引,以保持与挂载EBS卷速度的竞争力
为确保主节点保留直到最后一次提交的所有更改,我们需要在每次提交后备份索引,而不是定期执行。为了以可行的方式实现这一点,我们切换到增量备份单个文件,而不是创建索引的存档。Lucene段是不可变的,因此当我们执行备份时,只需要上传自上次备份以来的新文件。在每次提交时,Nrtsearch检查S3中的文件,确定缺失的文件并上传它们。这使得提交稍微变慢,因为我们现在要上传文件到S3,而之前只是将它们刷新到EBS。额外时间通常从几毫秒到20秒不等,取决于数据大小,这微不足道,不会引起任何问题。
为解决第二个障碍,我们开始并行从S3下载多个文件,以充分利用可用网络带宽。结合本地SSD,这使下载速度提高了5倍。两个障碍都解决后,我们能够停止使用EBS卷,转而使用本地磁盘。
我们仍然能够进行完整的一致性备份(快照),以防索引损坏。我们现在不通过主节点执行此操作,而是直接在S3中的位置之间复制最新提交的数据。由于这些完整备份不涉及副本引导,它们可以比以前更不频繁。
Lucene 10
我们在Nrtsearch初始开发期间使用了当时可用的最新版本Lucene库8.4.0。现在我们已经更新使用最新发布的Lucene 10(10.1.0)。此更新包括大量改进、优化、错误修复和新功能。最显著的新功能是使用HNSW算法的向量搜索。此外,结合我们升级到Java 21,Lucene 10可以利用更新的Java功能,如SIMD向量指令和外部内存API。
状态管理大修
传统状态管理
原始的集群和索引状态管理简单且难以使用。
集群状态仅包含已创建索引的名称。服务器不知道应启动这些索引中的哪些,因此需要部署管理器(Kubernetes操作器)在服务器引导期间启动必要的索引。这增加了操作器的复杂性。
索引状态由三个主要部分组成:
- 设置:只能在索引启动前配置的属性
- 实时设置:可以动态更新的属性
- 字段:指定字段类型及其属性的索引模式
在集群上更新索引状态的过程耗时且容易出错:
- 向主节点发出更改索引状态的请求
- 在主节点上发出索引提交请求,使数据和状态在本地存储(EBS)上持久化
- 在主节点上发出备份请求,使最新提交的状态和索引数据在远程存储(S3)中持久化
- 重启所有集群副本以加载最新的远程状态和数据
痛点
状态更新过程存在几个问题:
- 索引状态和数据的提交耦合在一起。这是不必要的,因为唯一允许的状态更改与先前数据向后兼容。具体来说,可以向索引添加新字段,但不能删除或修改现有字段
- 状态更改在提交请求之前不是持久的。这意味着如果主服务器在状态修改和提交之间重启,更新可能会丢失
- 主节点上的本地磁盘(EBS)是集群状态的事实来源。但是,只有一个主节点,在重启和重新部署期间不可用。由于状态有时无法访问,围绕它构建工具很困难
- 需要备份索引并重启所有副本,显著延长了在集群中完全传播状态更改所需的时间
在内部,当应用状态更改时,不是以隔离方式完成的。因此,在单个请求处理期间多次采样时,状态值可能会改变。这可能导致不一致和更多边缘情况需要处理。
新状态系统
状态管理系统经过重新设计以解决上述问题。
集群状态已更新以包含有关索引的附加信息。跟踪每个索引的"已启动"状态。这允许在服务器引导期间自动启动必要的索引,消除了外部协调的需要。状态还包含每个索引的唯一标识符,以便在重新创建索引时隔离数据/状态。
状态更改的提交与数据提交解耦。因此,状态更改现在在更新请求的生命周期内提交。这防止了成功请求所做的更改丢失。客户端也不再会看到已应用于主节点但尚未提交的状态值。
状态数据的位置可以设置为本地(EBS)或远程(S3)。将位置设置为远程意味着本地数据不再是状态的事实来源,主磁盘不再需要持久化来维护集群状态。
向副本添加了热重载状态的能力,允许应用更改而无需服务器重启。状态更新过程现在简化为:
- 向主节点发出更改状态的请求
- 在所有副本上热重载状态
这大大减少了将更改应用到整个集群所需的时间。
在内部,索引状态更改为构建为不可变表示。当处理更改时,它被合并到现有状态中以产生新的不可变表示。在更改提交到状态后端(EBS或S3)后,新状态原子性地替换对旧状态的引用。这防止在提交之前观察到更改。客户端请求检索当前状态一次,并在请求的剩余时间内引用它。由于状态对象是不可变的,在单个请求处理期间更改将不可见。
新功能
向量搜索
从主要版本9开始,Lucene添加了对向量数据和最近邻搜索的支持,使用HNSW算法。Nrtsearch利用此API为最多4096个元素的浮点和字节向量数据提供kNN向量搜索。提供多种不同的相似性类型:余弦(带或不带自动归一化)、点积、欧几里得和最大内积。
Nrtsearch还公开了几个额外的高级功能:
- 浮点向量可以配置为使用标量量化值进行搜索,允许在准确性和内存使用之间进行权衡
- 向量搜索可用于嵌套文档中的字段
- 可以配置内部合并并行性以加速合并向量图数据
- 可以启用Java提供的优化SIMD指令支持以加速向量计算
聚合
将现有搜索应用程序从Elasticsearch迁移的要求之一是提供有限的聚合支持。我们最初尝试通过使用和扩展Lucene方面来复制任何需要的功能。这对一些更简单的用例有效,但不适合更复杂和/或嵌套的聚合。
使用方面有两个主要问题。可以使用Nrtsearch插件系统添加复杂的聚合逻辑。但是,这需要为每个用例创建自定义代码,这不是可扩展或可维护的过程。此外,方面处理未与并行搜索集成。通过将索引划分为切片来并行完成文档收集和排名。但是,方面结果处理在收集后发生并且是单线程的。对于具有复杂聚合的大型索引,这可能会明显增加请求延迟。
作为方面的替代方案,我们添加了与并行搜索集成的聚合系统。为每个索引切片独立跟踪聚合。当为排名召回和收集切片文档时,聚合也会处理它以更新内部状态(如术语文档计数)。
当每个索引切片的并行搜索完成时,它执行归约操作以将所有切片顶部命中合并在一起形成全局文档排名。这种归约也发生在聚合上。来自每个切片的聚合状态合并在一起以产生全局状态。全局状态用于产生在搜索响应中返回给客户端的聚合结果。在聚合嵌套的情况下,这种合并递归发生。
目前,此系统支持以下聚合:
- 术语(文本和数字)- 创建包含术语文档计数的桶结果
- 过滤器- 根据给定条件过滤文档到嵌套聚合
- 顶部命中- 基于相关性分数或排序的前k个排名文档
- 最小值- 观察到的最小值
- 最大值- 观察到的最大值
支持更多插件
Nrtsearch高度可扩展,支持各种插件,实现定制和增强功能。一些关键插件包括:
- 脚本- 用Java编写的自定义评分逻辑(简单自定义逻辑也可以使用Lucene JavaScript表达式指定,无需插件)
- 重新评分器- 允许自定义重新评分操作,通过重新计算子集文档的分数来优化搜索结果
- 高亮- 提供自定义高亮以强调搜索结果中的相关部分
- 命中记录器- 促进搜索结果命中的自定义记录,用于收集数据以训练机器学习模型
- 获取任务- 启用搜索命中的自定义处理以根据需要检索和丰富数据
- 聚合- 自定义聚合实现
这些插件使用户能够根据其特定的搜索和检索需求定制Nrtsearch。
扩展的搜索查询
我们在搜索请求中公开了更多Lucene查询类型。我们仍然缺少一些查询,我们计划根据需要添加更多查询类型。您可以在我们的文档中找到当前可用的查询类型。
未来工作
我们计划通过S3而不是gRPC进行NRT复制,以允许副本扩展而主节点不会成为瓶颈。我们还计划用单个段中的并行搜索替换虚拟分片,这是Lucene 10中添加的功能。