Skip to content

搜索速度的优化

为文件系统cache预留足够的内存

在一般情况下,应用程序的读写都会被操作系统“cache”(除了 direct方式),cache保存在系统物理内存中(线上应该禁用swap), 命中cache可以降低对磁盘的直接访问频率。搜索很依赖对系统 cache 的命中,如果某个请求需要从磁盘读取数据,则一定会产生相 对较高的延迟。应该至少为系统cache预留一半的可用物理内存,更 大的内存有更高的cache命中率。

使用更快的硬件

写入性能对CPU的性能更敏感,而搜索性能在一般情况下更多的 是在于I/O能力,使用SSD会比旋转类存储介质好得多。尽量避免使用 NFS 等远程文件系统,如果 NFS 比本地存储慢3倍,则在搜索场景下 响应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访 问。如果搜索类型属于计算比较多,则可以考虑使用更快的CPU。

文档模型

为了让搜索时的成本更低,文档应该合理建模。特别是应该避免 join操作,嵌套(nested)会使查询慢几倍,父子(parent-child) 关 系 可 能 使 查 询 慢 数 百 倍 , 因 此 , 如 果 可 以 通 过 非 规 范 化 (denormalizing)文档来回答相同的问题,则可以显著地提高搜索 速度。

预索引数据

还可以针对某些查询的模式来优化数据的索引方式。例如,如果 所有文档都有一个 price字段,并且大多数查询在一个固定的范围上运 行range聚合,那么可以通过将范围“pre-indexing”到索引中并使用 terms聚合来加快聚合速度。

字段映射

有些字段的内容是数值,但并不意味着其总是应该被映射为数值 类型,例如,一些标识符,将它们映射为keyword可能会比integer或 long更好。

避免使用脚本

一般来说,应该避免使用脚本。如果一定要用,则应该优先考虑 painless和expressions。

优化日期搜索

在使用日期范围检索时,使用now的查询通常不能缓存,因为匹 配到的范围一直在变化。但是,从用户体验的角度来看,切换到一个 完整的日期通常是可以接受的,这样可以更好地利用查询缓存。

为只读索引执行force-merge

为不再更新的只读索引执行force merge,将Lucene索引合并为 单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时, 每个分段会单独执行搜索再将结果合并,将只读索引强制合并为一个 Lucene分段不仅可以优化搜索过程,对索引恢复速度也有好处。例如,应该避免持续地写一个固定的索引,直到它巨大无比,而 应该按一定的策略,例如,每天生成一个新的索引,然后用别名关 联,或者使用索引通配符。这样,可以每天选一个时间点对昨天的索 引执行force-merge、Shrink等操作。

预热全局序号(global ordinals)

全局序号是一种数据结构,用于在keyword字段上运行terms聚 合。它用一个数值来代表字段中的字符串值,然后为每一数值分配一 个 bucket。这需要一个对 global ordinals 和 bucket的构建过程。 默认情况下,它们被延迟构建,因为ES不知道哪些字段将用于 terms 聚合,哪些字段不会。可以通过配置映射在刷新(refresh)时告诉ES 预先加载全局序数

execution hint

terms聚合有两种不同的机制:

  • 通过直接使用字段值来聚合每个桶的数据(map)。
  • 通 过 使 用 字 段 的 全 局 序 号 并 为 每 个 全 局 序 号 分 配 一 个bucket(global_ordinals)。

ES 使用 global_ordinals 作为 keyword 字段的默认选项,它使 用全局序号动态地分配bucket,因此内存使用与聚合结果中的字段数 量是线性关系。在大部分情况下,这种方式的速度很快。 当查询只会匹配少量文档时,可以考虑使用 map。默认情况下, map 只在脚本上运行聚合时使用,因为它们没有序数。

预热文件系统cache

如果ES主机重启,则文件系统缓存将为空,此时搜索会比较慢。 可以使用index.store.preload设置,通过指定文件扩展名,显式地告 诉操作系统应该将哪些文件加载到内存中

如果文件系统缓存不够大,则无法保存所有数据,那么为太多文 件预加载数据到文件系统缓存中会使搜索速度变慢,应谨慎使用。

转换查询表达式

在组合查询中可以通过bool过滤器进行and、or和not的多个逻辑 组合检索,这种组合查询中的表达式在下面的情况下可以做等价转 换:

(A|B) & (C|D) ==> (A&C) | (A&D) | (B&C) |(B&D )

调节搜索请求中的batched_reduce_size

该字段是搜索请求中的一个参数。默认情况下,聚合操作在协调 节 点 需 要 等 所 有 的 分 片 都 取 回 结 果 后 才 执 行 , 使 用 batched_reduce_size 参数可以不等待全部分片返回结果,而是在指 定数量的分片返回结果之后就可以先处理一部分(reduce)。这样可 以避免协调节点在等待全部结果的过程中占用大量内存,避免极端情 况下可能导致的OOM。该字段的默认值为512,从ES 5.4开始支持。

使用近似聚合

近似聚合以牺牲少量的精确度为代价,大幅提高了执行效率,降 低了内存使用。

深度优先还是广度优先

ES有两种不同的聚合方式:深度优先和广度优先。深度优先是默 认设置,先构建完整的树,然后修剪无用节点。大多数情况下深度聚 合都能正常工作,但是有些特殊的场景更适合广度优先,先执行第一 层聚合,再继续下一层聚合之前会先做修剪,官方有一个例子可以参 考 : https://www.elastic.co/guide/cn/elasticsearch/guide/current/_ preventing_combinatorial_explosions. html。

限制搜索请求的分片数

一个搜索请求涉及的分片数量越多,协调节点的CPU和内存压力 就越大。默认情况下,ES会拒绝超过1000个分片的搜索请求。我们应该更好地组织数据,让搜索请求的分片数更少。如果想调节这个值, 则可以通过action.search.shard_count配置项进行修改。 虽然限制搜索的分片数并不能直接提升单个搜索请求的速度,但 协调节点的压力会间接影响搜索速度,例如,占用更多内存会产生更 多的GC压力,可能导致更多的stop-the-world时间等,因此间接影响 了协调节点的性能。

利用自适应副本选择(ARS)提升ES响应速度

为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转 发到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任 何一个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系 统压力和健康水平。

例如,一个分片的三个副本分布在三个节点上,其中Node2可能 因为长时间GC、磁盘I/O过高、网络带宽跑满等原因处于忙碌状态, 如下图所示。

如果搜索请求被转发到副本2,则会看到相对于其他分片来说,副 本2有更高的延迟。

  • 分片副本1:100ms。
  • 分片副本2(degraded):1350ms。
  • 分片副本3:150ms。 由于副本2的高延迟,使得整个搜索请求产生长尾效应。

ES希望这个过程足够智能,能够将请求路由到其他数据副本,直 到该节点恢复到足以处理更多搜索请求的程度。在ES中,此过程称为 “自适应副本选择”。

在 实 现 过 程 中 , ES 参 考 一 篇 名 为 C3 的 论 文 : Cutting Tail Latency in Cloud Data Stores via Adaptive Replica Selection(https://www.usenix.org/conference/nsdi15/technic al-sessions/presentation/suresh)。这篇论文是为Cassandra写 的,ES基于这篇论文的思想做了调整以适合自己的场景。 ES的ARS实现基于这样一个公式:对每个搜索请求,将分片的每 个副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮 询方式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求 路由到那里。ARS公式为: 每项含义如下:

  • os(s),节点未完成的搜索请求数;
  • n,系统中数据节点的数量;
  • R(s),响应时间的EWMA(从协调节点上可以看到),单位为毫秒;
  • q(s),搜索线程池队列中等待任务数量的EWMA;
  • µ(s),数据节点上的搜索服务时间的EWMA,单位为毫秒。

通过这些信息我们大致可以评估出分片副本所在节点的压力和健康程度,这就可以让我们选出一个能够更快返回搜索请求的节点。

Released under the MIT License.