【腾讯云ES】让你的ES查询性能起飞:Elasticsearch 搜索场景优化攻略“一网打尽”

2024-05-10 16:13:02 浏览数 (3)

一、 背景

Elasticsearch是一个基于Lucene库的开源搜索引擎,简称ES。腾讯联合 Elastic 公司在腾讯云上提供了内核增强版 ES 云服务,目前在腾讯内外部广泛应用于日志实时分析、结构化数据分析、全文检索等场景。海量规模、丰富的应用场景不断推动着腾讯云ES团队对原生ES进行持续的高可用、高性能、低成本等全方位的优化。 本文旨在介绍腾讯云ES 在优化查询性能之路上的探索历程,是对大量内外部客户不断优化实践的一个阶段性总结。本文会先从ES基本原理入手,在此基础上,从内核角度引导大家如何才能充分“压榨” ES 的查询性能。

二、Elasticsearch 的查询模型

我们首先来看下 ES 总体的查询模型。 ES 的任意节点可作为请求的协调节点,接收用户请求。协调节点将请求转发至对应一个或多个数据分片的主或者从分片进行查询,各个分片查询结果最后在协调节点汇聚,返回最终结果给客户端。

ES 的分布式查询主要有2个阶段,Query阶段跟Fetch阶段。

  • Query 阶段:协调节点将查询拆分成多个分片任务,发送到数据分片上通过调用Lucene 执行查 “倒排索引”,查询满足条件的文档id集合。Query 内又可以细分为2个阶段,本质上是一个基于CBO的倒排合并过程: (1) 对查询语句进行拆解,预估每个子语句的匹配结果数量; (2) 对符合条件的最小结果集进行遍历,检查其是否匹配其他查询子语句,得到一个最终的结果集。
  • Fetch 阶段:归并生成最终的检索、聚合结果。Fetch 也可以细分为以下2个阶段: (1)对Query 阶段的多个分片结果进行归并; (2)抓取用户需要的字段信息。 如果只有一个分片,那ES 会将流程合并为 QueryAndFetch 一个阶段。

三、 Elasticsearch 的索引设计

ES的底层是Lucene,可以说Lucene的查询性能就决定了ES的查询性能。Lucene内最核心的倒排索引,本质上就是Term到所有包含该Term的文档的DocId列表的映射。ES 默认会对写入的数据都建立索引,并且常驻内存,主要采用了以下几种数据结构:

  1. 倒排索引:保存了每个term对应的docId的列表,采用skipList的结构保存,用于快速跳跃。
  2. FST(Finite State Transducer):原理上可以理解为前缀树,用于保存term字典的二级索引,用于加速查询,可以在FST上实现单Term、Term范围、Term前缀和通配符查询等。内部结构如下:

3. BKD-Tree:BKD-Tree是一种保存多维空间点的数据结构,主要用于数值类型(包括空间点)的快速查找。

四、 Elasticsearch 的字段存储

除了索引外,ES 同时提供了行存(stored fields , _source)、列存(doc_value)来进行业务字段的存储,并提供了开启跟关闭的接口。行存跟列存各自约占一半的存储,是用户存储的大头。

1. Stored Fields :类似于MySQL 的行存,按行存储,主要用于字段值的展示,例如Kibana 。 (1) ES内置元数据字段(_index,_id,_score等等)默认开启store。 (2) 所有业务字段默认关闭store,但业务字段的store 都会被存到 _source。 (3)默认通过 index.codec 压缩算法进行压缩。查询时需要解压。 (4)内部结构:

2. _source Field : 是Stored Fields 中的一个特殊的超大字段,包含该条文档输入时的所有业务字段的原始值。 (1)大部分特性同 Stored Fields。 (2)_source 字段是该行中的第一个存储字段。优先读取。

3. doc_value Fields:类似于大数据场景中的列存,按列存储,主要用于聚合跟排序等分析场景。 (1) 不同文档的相同字段的值一起连续存储在内存中,默认不通过压缩算法压缩。可以“几乎”直接访问某个文档的某个字段。调用方式: "docvalue_fields": ["tag1"]。 (2) 数据被编码后,精度跟格式可能会发生变化。 (3)非text 默认开启doc_value。text 字段无法直接开启 doc_value。 (4) 内部结构:如下图,列式存储很容易通过字典编码跟偏移量压缩。

五、 Elasticsearch 的查询优化 “三十六计”

5.1 分片数,副本数,索引规模的合理评估

ES (版本>=6.6) 提供了索引生命周期管理功能。索引生命周期管理可以通过 API 或者 kibana 界面配置,详情可参考 index-lifecycle-management。使用索引生命周期管理,可以实现索引数据的自动滚动跟过期,并结合冷热分离架构进行数据的降冷跟删除。 为了让分片查询性能发挥到最优,需要对规模进行限制,我们通常有以下使用原则:

  1. 集群总分片数建议控制在5w以内,单个索引的规模控制在 1TB 以内,单个分片大小控制在30 ~ 50GB ,docs数控制在10亿内,如果超过建议滚动;
  2. 分片的数量通常建议小于或等于ES 的数据节点数量,最大不超过总节点数的2倍,通过增加分片数可以提升并发;
  3. 分片数越多长尾效应越明显,所以并不是越多越好,在搜索场景合理控制分片数也可以提升性能。

增加副本数,也可以分摊查询的负载,提升查询的性能。 考虑到用户自我管理分片容易考虑不周全,腾讯云ES推出的自研自治索引,作为一站式索引全托管解决方案,提供分片自动调优、滚动周期动态调整、查询裁剪、故障自动修复、索引生命周期管理等功能。可在降低运维与管理成本的同时,提高使用效率与读写性能。以后大家可以不用为索引生命周期管理、分片动态调整等操作烦恼了。

5.2 Mapping 的设计

Mapping的设计对于如何发挥ES的查询性能非常重要。ES 的Mapping 类似于传统关系型数据库的表结构定义。在ES 中,一旦一个字段被定义在了 mapping中,是无法被修改的(新增字段除外),所以一般我们需要修改索引的话,都会滚动或者重建索引,并采用 reindex 或logstach 来迁移数据。 为了高效发挥mapping 的性能并降低存储成本,介绍一些常见的使用技巧:

  1. 正如上面所说,对于同一份数据,ES 默认会建立索引,行存,列存。对于某些并不重要的字段,可以通过指定(index: false , store: false ,doc_values: false)来关闭,以减少冗余存储成本。腾讯云ES 自研压缩编码优化,能够进一步降低存储成本。
  2. ES 默认对于数值字段建立BKDTree 索引,但是倒排索引能够最大发挥Lucene 的查询性能。所以对于有限枚举值的数值字段,也建议使用keyword 类型以创建倒排索引。
  3. 字段值太长会大幅增加 ES的序列化跟Highlight 开销,且Lucene 限制单个term 长度不能超过65536,对于超长的值可以配置 ignore_above 忽略超长的数据,以避免性能的严重衰减。
  4. 字段可以设置子字段,比如对于text 字段有sort和聚合查询需求的场景,可以添加一个keyword子字段以支持这两种功能。
  5. 字段数量如果太多会降低ES 的性能,用户需要合理设计字段。同时为了避免字段爆炸,ES 有如下优化使用方式: (1) 用户可以在某个父层级字段设置 enabled: false 来防止其下面创建子字段 mapping ,但是能被行存查询出来。 (2)mapping 层级可以设置dynamic=runtime,虽然加入新字段也会更新 mapping,但是新加入的字段不会被索引,也就是不会使得索引变大,不过虽然不被索引,但是新加入的字段依然可以被查询,只是查询的代价会更大(运行时构建)。所以这种类型一般不建议用在经常查询的条件字段上,而更适合用在一些不确定数据结构的日志类索引中。 (3)mapping 层级也可以设置dynamic=strict (不允许新增一个不在 mapping中的字段,一旦新增的字段不在 mapping 定义中,则直接报错)或者dynamic=false(新字段不会被索引,不能作为查询条件,但是能被行存查询出来)

5.3 查询 Routing 路由优化

正常情况下,单个查询会扫描所有分片,容易遇到长尾效应,且大量节点在空转,可利用ES路由能力,大幅提高查询吞吐、降低长尾。通过写入时支持指定routing ,ES 会计算 target_shard_id = hash(routing) 将写入数据路由到指定分片上,这样在查询时,也可以通过指定routing,快速定位到目前数据所在的分片,查询的效率能够提升一个数量级。

具体使用方式参考:Customizing Your Document Routing | Elastic Blog。但使用这种方式需要特别注意的是,指定的 routing 须尽可能随机,保证分片之间尽量均衡,然后容易造成“热Key” 导致负载不均衡。

5.4 查询裁剪

正如上面所说,单个查询会扫描所有分片,容易遇到长尾效应,且大量节点在空转。查询裁剪可以让查询的效率提升一个数量级。而 routing 路由优化即是分片裁剪的特例。用户也可以有其他优化用法,总结如下:

  1. 索引裁剪:如果已经滚动产生了很多索引,这个时候每次通过别名查询全量索引时,一样会有大量空转查询,可以通过索引名特征或时间范围,指定具体的索引名 进行查询。(腾讯云ES推出的自研自治索引 能够帮助用户自动实现时序索引裁剪,在日志业务海量数据的某些场景下,性能优化效果10倍 )
  2. 分片裁剪:例如,用户可以在查询URL 指定 preference=_shards:0 或者routing 来指定查某一个分片进行查询。
  3. Segment裁剪: Segment 是分片内部的数据单元,腾讯云ES 自研 Segment 裁剪,可以提升20%~30%查询性能。

业务层面也可以直接做到(1)(2)

5.5 Index Sorting 优化

ES (版本>=6.0) 提供了数据排序(Index Sorting)的功能,具体用法参考Index Sorting。通过数据排序,通过牺牲少量的写入性能,在写入时将文档归类放置存储,非常有利于查询裁剪,极限压测通常大约能提升 20%-40%的查询性能,同时数据的压缩比也会有一定的提升。

5.6 Fetch 字段性能优化:不同类型字段拉取性能优化对比

我们在上面提到,ES 存储字段的类型这么多,那么我们最关心的不同类型字段的拉取性能究竟有什么区别呢? 我们基于8C 32G 规格,构建100w 条测试数据(每条数据包含100个字段)不断变化查询字段数进行查询,得到查询耗时的结果如下。我们可以看到,通过不同方式拉取字段的性能是存在一个平衡点的,大约在40左右。

(1) 当字段数很少时,低于 40 时,使用 doc_value Fields 拉取,性能最优。

代码语言:javascript复制
分析:如果我们只需要返回其中包含的一小部分字段时,读取并解压这个巨大的_source字段可能会开销很高。

(2) 当字段超较多时,达到 40 以上时,使用 _source 变为最优。

代码语言:javascript复制
分析:当我们需要非常多或者几乎全部字段时,此时使用 doc_value Fields 可能会有非常多的随机IO。这个时候,读取 _source 一个字段就能够处理全部业务字段。

在不同业务规模场景下,数据大小不一样,_source、列存、Store 查询性能的平衡点可能会偏移,需要实际的压测。业务可以根据需求选择最合适的存储字段。

5.7 Forcemerge 优化

ES 的写入模型采用的是类似LSM-Tree 的存储结构。ES 实时写入的数据都在 lucene 内存 buffer 中,同时依赖写入 translog 保证数据的可靠性。当积攒到一定程度后,将他们批量写入一个新的Segment。 这样,数据写入都是 Batch 和 Append,能达到很高的吞吐量。但是这种方式,也会产生大量的小Segment,查询时会产生非常多的随机IO,导致查询效率低下。 ES后台会进行segment merge(段合并)操作,但是默认段合并非常缓慢。所以我们可以通过强制的 forcemerge 来大幅降低Segment 数量,减少函数空转跟随机IO,极限压测通常大约能提升20%~30%的查询性能。 需要注意的是,当ES 频繁使用update 进行更新,累积到较大的数据规模时,deleted 累积过多,也会造成ES 的性能衰退(#75675),所以定期进行forcemerge 并降低 deleted ,有助于维持较好的查询性能。

5.8 如何用好缓存:ES 的缓存设计

缓存是加快数据检索速度的王道。ES 是使用各种缓存的大户。从整体来说,ES 可以利用的缓存汇总介绍如下:

  1. 系统缓存 (page cache/buffer cache) :由Linux 控制,ES 使用系统页缓存可以减少磁盘的访问次数。如果用户的索引比用户的内存配置小,可以通过配置"index.store.type": "mmapfs",让ES 尽可能将数据全部装入缓存。(ES 默认使用NIOFS 读行存,所以默认读行存一定会读盘。)
  2. 分片级请求缓存(Shard Request Cache):请求级别的查询缓存,主要用于缓存聚合结果跟suggest结果。
  3. 节点级查询缓存(Node Query Cache):字段级别的查询缓存,主要用于缓存某个字段的查询结果,并且由节点级别的LRU策略来控制。使用 Filter 可以告知 ES 优先对某些查询语句优先进行缓存。 需要注意的是,当索引过大时,构建Node Query Cache 可能会造成查询毛刺,并占用较多的内存,可以通过 indices.queries.cache.count 调节,或者通过 index.queries.cache.enabled 关闭。
  4. Fielddata Cache:可以理解为ES 在内存中实时动态构建的文档 “正排索引” 缓存,主要是用于text 跟聚合场景。从5.0 开始,text 字段默认关闭了 Fielddata 功能,所以目前默认只在聚合场景开启(global ordinals)。在低基数的聚合场景下,对聚合有较好的提升效果。但由于当有新数据写入时就需要重新构建,且全量构建较为耗时(可能会是聚合本身耗时的数倍),所以腾讯云ES 也基于CBO 策略对高基数的聚合场景进行了优化,在高基数场景下跳过构建缓存。

5.9 聚合优化

ES中的聚合查询,类似SQL的SUM/AVG/COUNT/GROUP BY分组查询,大数据场景下的Cube/物化视图,主要用于统计分析场景。ES 聚合主要分为以下三大类:

  • Metric 聚合 - 计算字段值的求和平均值,Geo-hash,采样等
  • Bucket 聚合 - 将字段值、范围、或者其它条件分组到Bucket中
  • Pipeline 聚合 - 从已聚合数据中进行聚合查询

需要注意的是,聚合不同于查询,通常普通查询是有限定size, 查够size就可以提前结束,但是聚合则每次都需要查询完全量的数据才能进行下一步的分桶、去重,如下图所示,首先每个分片内部做一次子聚合,然后所有子聚合的结果多路归并,再做一次聚合,才能得到最终的聚合结果。

所以聚合的速度通常要比普通查询慢很多。ES 的高基数聚合查询非常消耗内存,超过百万基数的聚合很容易导致节点内存不够用以至OOM,腾讯云ES 在这块的可用性方面也做了非常多的工作。那如何满足海量数据聚合分析场景的需求呢?我们通过大量实践,总结出以下4个聚合优化利器:

1. 拆分:用户可以通过 Composite Aggregation 这一类特殊的聚合,高效地对多级聚合中的所有桶进行分页。通过这种方式,我们可以将一个超大的聚合分析需求,拆分成流式的聚合查询小任务,通过不断迭代,通过较低的内存,也能跑完海量数据的聚合分析任务。特别的,term 聚合可以通过num_partitions 内置参数进行拆分。

2. 关闭缓存:上面提到,由于聚合查询的数据范围大,所以其构建缓存的开销在高基数场景下甚至能达到分钟级,term 聚合可以通过指定内置参数 "execution_hint" :"map" 关闭聚合缓存。如果不加"execution_hint": "map", ES 每次聚合会构建一次该索引全量的global_ordinals (本质是一个正排索引)用于缓存加速下一次聚合,有新写入就会导致该缓存失效,下次查询再出发全量构建。在Composite 聚合的场景下,腾讯云ES 也基于CBO 策略对高基数的聚合场景进行了优化,在高基数场景下跳过构建缓存。

3. 分片路由:通过分片路由对聚合分析任务进一步拆分,大大降低聚合的多路归并的开销。如下图:

4. 排序:通过数据排序来进行查询时的数据裁剪,可以进一步提升聚合性能。腾讯云ES 自研 Composite 聚合利用排序大幅提速,PR已经合并到官方,详参考composite-aggregation-early_termination

5.10 减少查询结果的序列化开销

原生ES在实际业务压测中,我们发现如果使用FilterPath容易产生性能问题,为了进一步提升查询性能,内核优化支持裁剪查询结果。腾讯云ES 提供自研开关如下:

代码语言:javascript复制
PUT /_cluster/settings
{
  "transient": {
    "search.simplify_search_results": true,  // 普通查询
    "search.simplify_aggregation_results": true  // Composite聚合
  }
}

详参考:查询结果字段 裁剪优化

5.11 批量从ES拉取数据的最佳方式

ES 批量拉取数据的场景下通常有以下几种方式:

  • from size :非常不建议,ES 默认限制 from size < 10000,在分布式系统中深度翻页排序的花费会随着分页的深度而成倍增长,如果数据特别大对CPU和内存的消耗会非常巨大甚至会导致OOM。
  • 滚动翻页(Search Scroll):原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。需要注意的是,每一个 scroll_id 会占用大量的资源,同时存在游标过多或者保存时间过长,会非常消耗内存。当不需要scroll数据的时候,尽可能快的把scroll_id显式删除掉。
  • 流式翻页(Search After):这种方式是通过维护一个实时游标来避免scroll的缺点,它可以用于实时请求和高并发场景。因为Search After读取的并不是不可变的快照,而是依赖于上一页最后一条数据,所以无法跳页请求,用于滚动请求,与Scroll类似,不同之处在于它是无状态的。需要注意使用这种方式的条件是需要至少指定一个唯一不重复字段来排序。

腾讯云ES 基于 Search Scroll 优化内核,降低了查询过程中(反)序列化跟压缩解压的开销, 进一步优化批量拉取数据的性能,具体参考:Search Scroll 查询流程优化, Scroll 查询结果columnar格式优化。

5.12 读懂监控,跟查询慢日志

当我们需要针对性的对业务的查询场景进行分析,定位性能瓶颈时,我们首先需要读懂监控,跟慢日志。

  1. 首先需要关注的是CPU 使用率,内存使用率以及 磁盘IO Util ,当其中一项达到瓶颈,查询性能就可能上不去了。

2. 其次需要关注是否有长耗时的查询任务 跟查询拒绝率,当这些指标出现异常时,说明大概率出现了大查询,导致查询线程池长期被占用,需要分析大查询并进行优化。

3. 通过慢日志,我们可以针对性的对大查询进行针对性的 profile 分析跟优化,配置方式参考Elasticsearch 中的慢日志

4. ES 自身也提供了一些接口,可以查看节点执行查询的一些状态:

  • profile:统计单个查询任务每个阶段的耗时;
  • _nodes/stats:节点统计信息,包括线程池、Cache 使用情况;
  • _tasks:节点活跃任务信息,可以看到集群当前正在处理的查询任务并进行分析;
  • _nodes/hot_threads:节点热点堆栈,用于分析热点,也可以使用 Jstack,火焰图。

5.13 负载不均的优化

负载均衡对于最大限度发挥ES 集群的性能是非常重要的,局部出现热点或短板都容易导致集群整体的负载上不去。这里列举了几种常见的情况以及优化方式:

  • ES 的负载均衡主要是通过分片均衡机制来实现的。在磁盘足够时,首先会保证节点层面的分片数均衡,其次保证索引层面的分片数均衡。因此,大表通常需要配置跟集群节点数量相等的分片数或者其倍数,并且设置索引层面的total_shards_per_node。在使用 routing 的场景下,则需要尽可能打散 routing 来保证数据跟负载均衡。
  • ES 从6.1版本引入了自适应副本选择( use_adaptive_replica_selection )的功能,来根据请求耗时选择合适的副本进行计算,默认是开启的。但是在高并发查询多个副本的场景下,副本选择的倾向性容易导致集群查询负载不均。在副本较多的场景下建议选择关闭。
  • ES client 默认会针对查询报错的长连接,会将其加入黑名单(blacklist),client 流量会发生倾斜。当客户端发生流量倾斜后,ES 默认会对发送到该可用区的查询,会优先查该可用区的副本,旨在减少搜索延迟,但这个机制在高并发场景下也可能会导致可用区查询流量不均。可以通过参数(es.search.ignore_awareness_attributes)设置为true 关闭。

5.14 JDK&GC算法优化

腾讯云ES 使用 腾讯基于社区 Open JDK 定制开发的 JDK 版本 Tencent Kona JDK, 验证跟Open JDK 相比,有更强的吞吐,更低的CPU 和内存使用率。同时使用G1GC 来减少长时GC,并通过大规模的 JVM 参数调优验证,进一步优化 GC 提升性能。这里不再展开,详参考:Elasticsearch Service Kona JDK

5.15 升级到最新版本

ES 的社区非常活跃,全球有1700 Contributers,ES 官方也在不断的迭代更新优化,每个月都更新小版本,每年会出大版本。腾讯云ES 团队也在不断的打磨 ES 。建议用户尽量使用最新的稳定版本。

ES 版本

介绍

7.14

腾讯云ES自研Search Scroll 批量拉取数据接口优化,性能提升20%,参考5.11

7.13

Date Histogram 聚合内部重写为 filters 聚合,性能提升10 倍。 https://www.elastic.co/cn/blog/new-in-elasticsearch-7-13-even-faster-aggregations

7.10

腾讯云ES自研X-Pack 鉴权性能优化,通过特殊权限处理、缓存、延迟加载等机制消除 CPU 热点,在当索引数量较多的场景下,提升查询性能30% 。

7.9

优化了多层嵌套聚合所消耗的内存。

7.0

通过计算跳过不必要的记录,查询大量文档的top N 性能提升3-7倍。Faster Retrieval of Top Hits in Elasticsearch with Block-Max WAND

六、 结语

本文首先介绍 ES 的分布式查询模型、索引数据结构、字段存储等基本原理,然后在此基础上详尽地介绍了如何让查询性能发挥到最优的各种使用技巧,以及腾讯云ES 在性能方面所做的耕耘。希望能够帮助大家分析定位查询性能的问题,找到使用ES的最佳姿势,也希望能为未来继续挖掘ES性能抛砖引玉。

1 人点赞