Elasticsearch聚合性能优化
业务背景
最近业务量增加,在放量过程中,通过监控发现一个与全局搜索相关的接口出现了少量超时。该接口的主要逻辑是根据关键字搜索命中会话列表,并统计各会话列表中命中的内容条数。业务侧逻辑较为简单,通过 Elasticsearch (ES) 进行搜索并聚合后返回结果。排查时发现,即使搜索条件没有命中任何内容,查询仍然有可能超时。
耗时根因分析
首先检查了业务服务器和 ES 集群的性能指标,整体负载均较低。因此,问题可能出在 ES 查询语句的效率上。我们使用 ES 自带的 search profiler
定位各阶段的耗时。
通过 ES 的 search profiler
发现,耗时主要发生在 terms
聚合阶段(如图 1 所示)。初步怀疑是聚合逻辑导致查询请求超时。为了验证这一猜想,我们将查询语句中的聚合部分删除,结果查询速度非常快,仅需几十毫秒就返回了结果。
那么,为什么即使搜索条件没有命中任何内容,聚合阶段仍然耗费了大量时间呢?这需要从 Elasticsearch 的 terms aggregations
流程说起。
Global Ordinals
Global Ordinals 是一种将字符串类型的 term 映射到唯一整数 ID 的机制。这样做的好处是将昂贵的字符串比较操作替换为更高效的整数比较操作,从而加速聚合过程。默认情况下,在首次进行查询聚合操作时,会动态构建一个映射表。如果需要聚合的字段,唯一值非常多(高基数字段),那么构建Global Ordinals就非常的耗时。
在我的案例中,需要参与聚合字段conversation
恰好就是这种高基数字段,所以即使最终查询没有匹配到任何内容,请求也有可能超时,时间主要耗费在构建这个全局映射表上。
解决方案
了解原因后,解决方案相对简单:
1. 在索引阶段预构建 Global Ordinals
Elasticsearch 提供了 eager_global_ordinals
选项,开启该选项后,在对文档进行索引时会提前构造 Global Ordinals。这样可以大大减少查询时的耗时。缺点是,如果 term 字段变化频繁,可能会影响索引性能。
PUT my-index-000001/_mapping
{
"properties": {
"tags": {
"type": "keyword",
"eager_global_ordinals": true
}
}
}
2. 减少构建 Global Ordinals 的 Terms 数量
在执行查询时,指定 include
或 exclude
参数,限制构建 Global Ordinals 的 Terms 数量。在具体业务场景下可能有用。
GET /_search
{
"aggs": {
"tags": {
"terms": {
"field": "tags",
"include": ".*sport.*",
"exclude": "water_.*"
}
}
}
}
3. 不使用 Global Ordinals
在执行聚合查询时,通过设置 "execution_hint" : "map"
强制 ES 使用 map 进行聚合。使用 map 聚合时,每个分片先进行本地查询,找到匹配的文档,然后在本地构建一个哈希表进行 term 聚合统计。最后,协调节点从各分片收集数据并聚合后按查询条件返回结果。这种方式适用于最终匹配到的文档较少的场景。
GET /_search
{
"aggs": {
"tags": {
"terms": {
"field": "tags",
"execution_hint": "map"
}
}
}
}
结合我的业务场景,使用 "execution_hint": "map"
是一个比较均衡的方案。调整之后的最终效果如下(如图 2 和图 3 所示):