写点什么

E 往无前 | 让你的 ES 查询性能起飞!腾讯云大数据 ES 查询优化攻略“一网打尽”

  • 2023-11-29
    广东
  • 本文字数:8308 字

    阅读完需:约 27 分钟

E往无前 | 让你的ES查询性能起飞!腾讯云大数据ES查询优化攻略“一网打尽”


《E 往无前》系列将着重展现腾讯云 ES 在持续深入优化客户所关心的「省!快!稳!」诉求,能够在低成本的同时兼顾高可用、高性能、高稳定等特性,可以满足微盟、小红书、微信支付等内外部大客户的核心场景需求。


背景

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 界面配置,详情可参考(https://www.elastic.co/guide/en/elasticsearch/reference/6.6/index-lifecycle-management.html#index-lifecycle-management)

使用索引生命周期管理,可以实现索引数据的自动滚动跟过期,并结合冷热分离架构进行数据的降冷跟删除。为了让分片查询性能发挥到最优,需要对规模进行限制,我们通常有以下使用原则:

  1. 集群总分片数建议控制在 5w 以内,单个索引的规模控制在 1TB 以内,单个分片大小控制在 30 ~ 50GB ,docs 数控制在 10 亿内,如果超过建议滚动;

  2. 分片的数量通常建议小于或等于 ES 的数据节点数量,最大不超过总节点数的 2 倍,通过增加分片数可以提升并发;

  3. 分片数越多长尾效应越明显,所以并不是越多越好,在搜索场景合理控制分片数也可以提升性能。

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


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 的性能,用户需要合理设计字段。同时为了避免字段爆炸,

  6. 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,快速定位到目前数据所在的分片,查询的效率能够提升一个数量级。

具体使用方式参考:(https://www.elastic.co/cn/blog/customizing-your-document-routing)。但使用这种方式需要特别注意的是,指定的 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 拉取,性能最优。

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

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

分析:当我们需要非常多或者几乎全部字段时,此时使用 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 的性能衰退(https://github.com/elastic/elasticsearch/issues/75675),所以定期进行 forcemerge 并降低 deleted ,有助于维持较好的查询性能。


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


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

  1. 系统缓存 (page cache/buffer cache) :由 Linux 控制,ES 使用系统页缓存可以减少磁盘的访问次数。

  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(https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html) 这一类特殊的聚合,高效地对多级聚合中的所有桶进行分页。通过这种方式,我们可以将一个超大的聚合分析需求,拆分成流式的聚合查询小任务,通过不断迭代,通过较低的内存,也能跑完海量数据的聚合分析任务。特别的,term 聚合可以通过 num_partitions 内置参数进行拆分。

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

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

4. 排序:通过数据排序来进行查询时的数据裁剪,可以进一步提升聚合性能。腾讯云 ES 自研 Composite 聚合利用排序大幅提速,PR 已经合并到官方,详参考(https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html#_early_termination)


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


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

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 读懂监控,跟查询慢日志


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


首先需要关注的是 CPU 使用率,内存使用率以及 磁盘 IO Util ,当其中一项达到瓶颈,查询性能就可能上不去了。图片图片 2. 其次需要关注是否有长耗时的查询任务 跟查询拒绝率,当这些指标出现异常时,说明大概率出现了大查询,导致查询线程池长期被占用,需要分析大查询并进行优化。


图片 3. 通过慢日志,我们可以针对性的对大查询进行针对性的 profile 分析跟优化,配置方式参考 Elasticsearch 中的慢日志(https://cloud.tencent.com/developer/article/1733497?from=10680)


  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 提升性能。这里不再展开,详参考:(https://cloud.tencent.com/document/product/845/66513?from=10680)


5.15 升级到最新版本


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


结语


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

用户头像

还未添加个人签名 2020-06-19 加入

欢迎关注,邀您一起探索数据的无限潜能!

评论

发布
暂无评论
E往无前 | 让你的ES查询性能起飞!腾讯云大数据ES查询优化攻略“一网打尽”_ES_腾讯云大数据_InfoQ写作社区