介绍
Elasticsearch term聚合 用于创建 给定字段的唯一值对应的存储桶。例如,包含国家名称的字段上的 term 聚合将为中国创建一个存储桶,为巴基斯坦创建一个存储桶,为俄罗斯创建一个存储桶,等等。在一般情况下,term聚合非常快,但在某些特殊情况下,它们可能会很慢。缓慢聚合的一个原因可能是集群配置错误。性能不佳的另一个原因可能是执行term聚合的字段上的高基数值。
在这篇博文中,我将首先简要概述为确保 Elasticsearch 集群的最佳性能应遵循的一般说明。然后是几个部分,这些部分提供了有助于理解term聚合的基本机制的背景材料,包括 (1) 高基数的定义,(2) 刷新间隔的描述,以及 (3)全局序数。接下来,我将展示如何查看构建全局序数对term聚合性能的影响。最后,我将介绍几种提高高基数项聚合性能的技术,包括 (1) 基于时间的索引,(2) 急切的全局序数,以及 (3) 防止 Elasticsearch 构建全局序数的技术。
在一个例子中,这篇博文中记录的技术能够将在一家非常大的零售银行运行的高基数term聚合的执行时间从 15 秒减少到 15 毫秒以下。
一般建议
调整集群会对集群的整体性能产生很大影响。以下 Elasticsearch 文档提供了有关配置和调整 Elasticsearch 集群的详细信息,应遵循:
- 重要的系统配置
- 调整搜索速度
- 调整索引速度
- 调整磁盘使用情况
基数
term聚合的性能可能会受到正在聚合的字段的基数的极大影响。基数是指存储在特定字段中的值的唯一性。高基数意味着一个字段包含很大比例的唯一值。低基数意味着一个字段包含大量重复值。例如,存储国家名称的字段的基数相对较低,因为世界上只有不到 200 个国家。或者,存储 IBAN 号码或电子邮件地址的字段具有高基数,因为可能存储了数百万个唯一值。
在这篇博文中讨论高基数时,指的是具有数十万或数百万个唯一值的字段。
Elasticsearch 刷新间隔
为了理解这篇博客的其余部分,必须对刷新间隔有一个大致的了解。
当文档被插入到 Elasticsearch 中时,它们会被写入缓冲区,然后定期从该缓冲区刷新到Segments中。这种刷新操作称为刷新,新插入的文档只有在刷新后才能搜索。默认情况下每秒刷新一次,但是刷新间隔是可配置的。
刷新间隔与性能相关,因为在后台,Elasticsearch 将小段合并为更大的段,而那些较大的段则合并为更大的段,依此类推。因此,通过启用频繁刷新,Elasticsearch 需要做更多的后台工作来合并小段,而不是需要做的不太频繁的刷新会创建更大的段。
如果新插入的数据需要近乎实时的搜索功能,则需要频繁刷新,但在其他用例中可能不需要这种频繁刷新。如果应用程序可以等待更长时间以等待最近的数据出现在其搜索结果中,则可以增加刷新间隔以提高数据摄取的效率,从而释放资源以提高整体集群性能。
全局序数
term聚合依靠全局序数来提高效率。全局序数是一种数据结构,它为给定字段的每个唯一term维护一个增量编号。全局序数是在每个分片上计算的,默认情况下,后续term聚合将完全依赖这些全局序数来有效地在分片级别执行聚合。然后将全局序数转换为最终归约阶段的实数,该阶段结合了来自不同分片的结果。如果修改了分片,则需要为该分片计算新的全局序数。
高基数字段上的term聚合的性能可能很慢且不可预测,部分原因是(1)构建全局序数所花费的时间将随着字段基数的增加而增加,以及(2)默认情况下,全局序数是惰性的建立在自上次刷新以来发生的第一个聚合之上。此外,频繁的文档插入、频繁的刷新和频繁执行的term聚合的组合将导致频繁地重新计算全局序数。
可以在此 GitHub 问题中找到有关全局序数性能的更多详细信息。
如何查看全局序数对term聚合的影响
Elasticsearch 日志文件对于检测性能问题非常有帮助。将日志级别设置为适当的值,记住过多的日志记录可能会增加磁盘 IO 并对性能产生负面影响。监视名为elasticsearch_index_search_slowlog.log. 如果slowlog文件中出现slow term aggregations,那么它们的性能不佳可能是由于全局序数的构建,可以检查如下:
继续向 Elasticsearch 中插入数据,以确保在我们执行term聚合时重建全局序数。从慢日志文件中复制慢项聚合并手动执行和分析它,以了解执行时间花费在哪里。
继续将数据插入 Elasticsearch。从慢日志文件中复制term聚合并手动执行它。在执行 terms 聚合时,同时执行hot_threads api。如果热线程通常返回包含对 的引用的结果GlobalOrdinalsBuilder,那么代码可能会花费大量时间来构建全局序数。
通过执行以下命令 临时启用全局序号信息的记录:
PUT _cluster/settings
{"transient": {"logger.org.elasticsearch.index.fielddata": "TRACE"}
}
这会将有关构建全局序号所花费的时间的信息写入elasticsearch.log文件,例如:
global-ordinals [<field_name>][1014089] took [592.3ms]
完成上述操作后,请务必将日志记录设置回默认值,因为将日志记录设置为 时可能会影响性能TRACE。
给定字段的全局序数数据结构的大小可以通过memory_size_in_bytes在执行indices stats命令时查看,如下所示:
GET <index_name>/_stats/fielddata?fielddata_fields=<字段名>
如果构建全局序数会消耗大量资源,上述命令应该会给出一个想法。本文的其余部分重点介绍减轻构建全局序数对term聚合的影响的步骤。
使用基于时间的索引
如果该分片自上次计算其全局序数以来已被修改,则仅需要在该分片上重新创建全局序数。如果自上次计算其全局序数以来未修改分片,则将继续使用先前计算的全局序数。对于时间序列数据,实施基于时间的索引是确保大多数索引/分片保持不变的好方法,这将减少刷新操作后需要重新计算的全局序数的大小。
例如,如果两年的数据存储在月度索引而不是一个大型索引中,则每个月度索引的大小是一个大型索引的 1/24。由于我们正在考虑时间序列数据,我们知道只有最近的月度索引才会插入新文档。这意味着只有 24 个索引中的一个被主动写入。由于全局序数仅在已修改的分片上重建,因此 24 个月度索引中的 23 个分片将继续使用先前计算的全局序数。与将两年的数据存储在一个大型索引中相比,这将把构建全局序数所需的工作减少多达 24 倍。
启用急切的全局序数
高基数项聚合的性能可以通过 急切地构建全局序数来提高。启用全局序数的急切构建将在刷新段时创建全局序数数据结构,而不是每次刷新后的第一个查询。但是,权衡是急切地构建全局序号可能会对摄取性能产生负面影响,因为每次刷新都会计算新的全局序号,即使它们可能不被使用。为了尽量减少由于频繁刷新而频繁构建全局序数所带来的额外工作量,应该增加刷新间隔。
不要建立全局序数
另一种提高高基数term聚合性能的方法是完全避免构建全局序数,而是直接在原始term上执行term聚合。这可能是有益的,因为在高基数域上计算全局序数可能会很慢,并且不构建全局序数将消除这种延迟。这是以降低每个后续term聚合的效率为代价的,因为它们不能利用全局序数。
此外,请注意,如果没有为term聚合构建全局序号,则term聚合将在每个请求中使用更多内存,因为 Elasticsearch 需要保留结果集中出现的所有唯一term的映射。如果要聚合大量唯一term,这可能会触发内部断路器以防止内存过度使用,这可能导致聚合失败。
因此,只有在预计term聚合将在相对较少数量的文档上执行时,才应应用此方法。例如,如果聚合与 在filter context 中 执行 的选择性bool 查询一起定义,则可能会出现这种情况。使用真实数据运行实验,以确定这种方法是否可以提高特定用例的性能。
在 Elasticsearch 6.7 或更高版本中,这可以通过指定"execution_hint": "map"来完成,它告诉 Elasticsearch 直接聚合字段值而不利用全局序数,或者在旧版本中,这可以通过使用在term聚合,如下所述。
默认情况下,term聚合将返回按每个存储桶中的文档数量排序的前十个term的存储桶。以下是顶级 IBAN 标识符的示例term聚合。默认情况下,此term聚合将在最后一次刷新后的第一次执行时重建全局序数。
"aggregations": {"top-ibans": {"terms": {"field": "IBAN_keyword"}}
}
这可以重写以避免在 Elasticsearch 6.7 或更高版本中使用全局序号,如下所示。
"aggregations": {"top-ibans": {"terms": {"field": "IBAN_keyword","execution_hint": "map"}}
}
在 6.7 之前的 Elasticsearch 版本中,有一个错误会导致计算全局序数,即使"execution_hint": "map"已指定。这已在此 github pull request中修复。对于旧版本,可以应用以下技术来防止为上述 term 聚合构建全局序数。
"aggregations" : {"top-ibans" : {"terms" : {"script": {"source" : "doc['IBAN_keyword'].value","lang" : "painless"}}}
}