作者:来自 Elastic Valeriy Khakhutskyy
了解如何将基于 scroll 的 datafeeds 切换为基于 aggregation 的 datafeeds,以优化大规模部署中的机器学习任务。
无缝连接领先的 AI 和机器学习平台。开始免费云试用,探索 Elastic 的 gen AI 能力,或立即在你的机器上试用。
几乎在我参与过的每一个大型 Elastic 部署中,总会有一个 Elastic Security 或 Elastic Observability 的异常检测( AD )作业,看起来运行正常,但却始终处于“落后状态”。落后六小时、十二小时,而这个差距始终无法缩小。
datafeed 并没有损坏。它在按设计正常工作:在每次运行时读取每一条原始文档,跨越所有分片。在一个大型集群中,如果使用跨集群搜索( CCS )以及类似 logs-* 这样范围很广的索引模式,这意味着每个 bucket 都要扫描数十亿条文档。没有任何硬件能够让这种方式持续可用。datafeed 永远在追赶实时数据,却永远无法追上。
解决方案是将默认的基于 scroll 的 datafeed 配置切换为基于聚合( aggregation )的 datafeed 配置:让数据节点在本地完成汇总,只将紧凑的 bucket 结果发送到 ML 节点。相同的检测逻辑,但负载只是其中的一小部分。性能提升可能非常显著,甚至超出预期。具体数值将在下一部分给出,而这种巨大差距背后的原因则会在文章末尾解释,供希望理解机制的读者参考。
有一个需要提前注意的点:这种切换需要创建一个新的作业。旧模型无法迁移;已经学习了数周的基线会丢失。因此,最佳切换时机是在作业运行数月之前,而不是之后。这也是为什么在部署之前阅读这篇内容很重要。
有多快?ML 作业中 - scroll vs. aggregation datafeeds
我们在生产数据上用两种方式运行了同一个作业:先是基于 scroll 的方式,然后是基于 aggregation 的方式。该作业覆盖 13 个月的历史数据,在多个集群中按 15 分钟桶监控每小时 836,000 条日志事件。
在历史数据训练阶段,使用 scroll 配置:耗时 5 天(实际墙钟时间)、790 万次顺序请求、传输 3.5 TB 数据;而使用 aggregation 配置:2.3 分钟、23 次请求、34 MB(提升 3,374 倍)。可以这样理解:如果你在周一上午 9 点启动 scroll 回填,它会在周六早上完成;而 aggregation 版本在 9:02 就已经完成。
在实时数据上,这种差异没有那么极端,但仍然很显著:每个 tick 的请求数量减少约 20 倍。在 datafeed 每隔几分钟持续运行的情况下,这种节省会迅速累积。
开始之前
在进入配置前,有三点需要了解。
这不是 “向导式” 操作。标准 Kibana 作业向导(Single Metric、Multi-Metric、Population)不支持 aggregation 配置。要创建基于 aggregation 的作业,你需要使用 Elasticsearch API,或者使用 Kibana 的 Advanced Job Wizard,并手动编辑 JSON。下面的示例提供了最实用的路径:在 Multi-Metric Wizard 中配置作业,然后点击 Convert to advanced job,再创建作业。这样你会得到一个已填充的 JSON 起点,而不是空白编辑器。
该配置非常严格,而且大多不会报错。没有 schema 校验来捕捉 aggregation key 拼写错误,或 fixed_interval 与 bucket_span 不匹配的情况。作业会正常运行,anomalies 会触发,但不会有任何迹象表明结果基于错误的数据。这也是为什么存在“五步模式”,以及为什么每次都应该使用 Preview 标签页:在训练前发现配置错误只需要 30 秒,而一周后才发现则会糟糕得多。
Single Metric Viewer 对聚合作业有一个已知限制。该视图通过重新查询索引来重建 “实际值” 曲线,但无法复现任意用户自定义的 aggregation,因此 actual-value 线通常缺失或只是近似值。Anomaly Explorer 不受影响:anomaly scores、swim lanes 和 influencer attribution 都能正常工作。只是不要依赖 Single Metric Viewer 的图表来验证模型看到的数据。
我们可以和不能聚合的内容
几乎所有 ML 函数都可以与基于 aggregation 的 datafeeds 配合使用,但合适的 aggregation 模式取决于具体函数。
| 函数 | 模式 |
|---|---|
count, mean, high_mean, low_mean, sum, max, min, varp | 标准:date_histogram → terms → metric aggregation |
time_of_day, time_of_week | 最小模式:仅 date_histogram,不需要 terms 或 metric |
rare, freq_rare, info_content | 复合模式:顶层 composite,以 date_histogram 作为 source |
categorization | 对 categorization 字段的 .keyword 子字段做 terms |
lat_long | 仅 scroll |
lat_long 是唯一真正的例外。该配置虽然是被允许的,但 geo_centroid 会计算一个 bucket 内所有坐标的算术平均值:如果同一个实体在同一个 bucket 内同时出现在纽约和伦敦,那么质心最终会落在大西洋中间,这在使用场景中通常没有意义。因此,lat_long 类型的作业应保持使用基于 scroll 的 datafeeds。
下一节中的五步模式适用于标准情况。我们将在文章末尾再讲解其余模式。
标准五步模式:从 scroll-based 到 aggregation datafeed
将任何基于 scroll 的作业转换为基于 aggregation 的 datafeed 遵循相同的五个步骤。一旦理解了这个模式,将其应用到任何兼容作业通常只需要大约 10 分钟。
步骤 1:在 analysis 配置中添加 summary_count_field_name: ["doc_count"](www.elastic.co/docs/refere… ""doc_count"")。这会告诉 ML 引擎输入数据已经是预先汇总的。如果不这样做,引擎会把每个聚合后的 bucket 当作一条原始文档,从而导致错误的异常评分。
步骤 2:选择 bucket 包装拓扑结构。对于大多数函数(count、mean、sum、max、min、varp、time_of_day、time_of_week 以及 categorization),在最顶层使用 date_histogram,并确保 fixed_interval 与 bucket_span 完全一致,以保证分析结果正确。对于 rare、freq_rare 和 info_content,则在最顶层使用 composite,并将 date_histogram 作为其 source 之一。这会将 datafeed 路由到 composite extractor,使其能够遍历所有字段值组合,而不是截断为 top-N。
步骤 3:在 @timestamp 上添加 max 聚合。ML 引擎需要它来确定每个 bucket 的精确结束时间。在标准拓扑(步骤 2 的 date_histogram 外层)中,它位于 histogram 的 aggregations 内部;在 composite 拓扑中,它与 composite 聚合并列。
步骤 4:将每个分析字段映射为 terms 聚合,并且名称必须与 analysis 配置中的字段名完全一致。一个分类字段 → 一个嵌套 terms;两个或多个分类字段 → 在 date_histogram 内嵌套 composite 聚合,并且每个字段对应一个 terms source。对于 categorization 作业,在 categorization_field_name 的 .keyword 子字段上使用 terms 聚合。命名规则是严格的:聚合 key 必须与 analysis 配置中的字段名完全一致;ML 引擎依赖的是聚合名称而不是 field 参数来查找值。任何不匹配都会导致静默错误——作业看似正常运行,但关键数据全部缺失。
步骤 5:将每个 detector 的 metric 字段映射为其对应的 Elasticsearch 聚合:
| ML function | Elasticsearch aggregation |
|---|---|
mean / high_mean / low_mean | avg |
sum | sum |
max | max |
min | min |
varp | extended_stats |
对于 count、rare、freq_rare、info_content、time_of_day、time_of_week 以及 categorization 作业,ML 引擎只基于 doc_count 工作;不需要 metric 聚合,因此这一步骤可以跳过。
逐步示例:在 Kibana 中构建基于 aggregation 的 ML 作业
让我们用 Kibana 的 sample web logs 从头构建一个完整流程。如果你还没有加载这些数据,可以进入 Kibana 首页,点击 Integrations → Sample data → Sample web logs → Add data。这会提供一个名为 Kibana Sample Data Logs 的 data view,以及一个名为 kibana_sample_data_logs 的 index,其中包含 @timestamp、bytes(响应大小)以及 geo.dest(目标国家)等字段。
我们将构建一个用于检测异常大响应大小的作业:对 bytes 使用 high_mean,并按目标国家( geo.dest )进行分区,bucket span 为 1 小时。
使用 Multi-Metric Wizard 创建作业
这是实际中创建大多数作业的方式。进入 Machine Learning → Anomaly Detection → Manage Jobs → Create job。
选择 “Kibana Sample Data Logs” data view,并将时间范围设置为覆盖整个 sample dataset。在 job type 页面中选择 Multi-metric。
在 Multi-Metric Wizard 中配置 detector:
- 对 bytes 计算 high mean
- 按 geo.dest 进行数据拆分
- bucket span:1h
给这个作业设置一个 ID,其余所有配置保持默认,但暂时不要点击 Create。在最后一步配置页面中,点击 Preview JSON,并查看 datafeed 部分。你会看到的是一个普通的基于 scroll 的 datafeed,没有任何 aggregations,只有一个 index pattern 和一个 match_all 查询。
这是所有向导默认生成的配置。在小型集群上它运行良好。但在大型集群中,配合 CCS 和宽泛的索引模式时,这种 datafeed 会在每次运行时扫描所有原始文档,并且永远无法追上实时数据。
不要点击 Create,而是点击 Convert to advanced job。这样会保留你刚刚配置的所有内容(detector、partition 字段、bucket span),并直接进入 Advanced Wizard,在那里我们可以应用五步模式。
分析配置
转换后会预填 detector、partition 字段以及 bucket span。这里唯一需要做的修改是模式中的第 1 步:打开 Edit JSON 视图,并添加 summary_count_field_name,告诉 ML 引擎输入数据已经是预先汇总的:
`
1. {
2. "bucket_span": "1h",
3. "summary_count_field_name": "doc_count", // Step 1
4. "detectors": [
5. {
6. "function": "high_mean",
7. "field_name": "bytes",
8. "partition_field_name": "geo.dest"
9. }
10. ],
11. "influencers": ["geo.dest"]
12. }
`AI写代码
datafeed 配置
切换到 Datafeed 标签页。这里会汇总模式中的步骤 2 到步骤 5。删除 scroll_size(如果存在),然后输入 aggregations:
`
1. {
2. "buckets": {
3. "date_histogram": { // Step 2: bucket wrapper, interval = bucket_span
4. "field": "@timestamp",
5. "fixed_interval": "1h"
6. },
7. "aggregations": {
8. "@timestamp": { // Step 3: max timestamp anchor
9. "max": { "field": "@timestamp" }
10. },
11. "geo.dest": { // Step 4: partition field, name must match exactly
12. "terms": {
13. "field": "geo.dest",
14. "size": 1000
15. },
16. "aggregations": {
17. "bytes": { // Step 5: metric field → avg aggregation
18. "avg": { "field": "bytes" }
19. }
20. }
21. }
22. }
23. }
24. }
`AI写代码
关于这个配置,有几点需要注意:
- 步骤 2:date_histogram 使用 fixed_interval: "1h",必须与 bucket_span 完全一致。不一致会导致 bucket 时间错误。
- 步骤 3:@timestamp 上的 max 聚合必须命名为 @timestamp,并放在 histogram 的 aggregations 内部;否则 ML 节点无法确定每个 bucket 的精确结束时间。
- 步骤 4:partition 字段的 terms 聚合必须严格使用字段名本身作为名称:geo.dest,而不能用 geo.dest_grouping 或任何别名。ML 引擎使用的是 aggregation 的名称,而不是 field 参数来识别分区值。如果不匹配,会导致分区字段在结果中被静默丢失。
- 步骤 5:metric 聚合 key bytes 必须与 detector 中的 field_name 完全一致。任何不匹配都会导致异常评分出现静默错误。
验证方式
在创建作业之前,使用 Preview 标签页。该步骤会在真实数据上执行 aggregation,并展示 ML 节点实际接收到的数据,是提交前非常重要的校验手段。
添加影响因子字段(influencer fields)
在上面的示例中,geo.dest 是分区字段。ML 模型会为每个目标国家学习单独的基线,并按国家分别报告异常。但你可能还希望 machine.os 在异常结果中作为 influencer 出现:当 detector 触发时,你希望看到“对于 geo.dest: CN 和 machine.os: win 来说,这看起来是异常的,这些因素共同促成了结果”。influencers 不参与异常检测本身;它们只是为已发现的异常提供上下文。
为了在分区字段之外支持 influencer,analysis 配置中需要添加 influencers 数组:
`
1. {
2. "bucket_span": "1h",
3. "summary_count_field_name": "doc_count",
4. "detectors": [
5. {
6. "function": "high_mean",
7. "field_name": "bytes",
8. "partition_field_name": "geo.dest"
9. }
10. ],
11. "influencers": ["geo.dest", "machine.os"]
12. }
`AI写代码
现在 datafeed 需要同时对这两个字段进行聚合。一个 terms 嵌套在另一个 terms 之内是行不通的;因为内层 terms 只会在每个外层 bucket 中返回该字段的 top-N 值,这会导致某些组合被静默丢失。
因此,应当使用 composite aggregation,在每个字段上各使用一个 terms source,并将其嵌套在 date_histogram 内部:
`
1. {
2. "buckets": {
3. "date_histogram": {
4. "field": "@timestamp",
5. "fixed_interval": "1h"
6. },
7. "aggregations": {
8. "@timestamp": {
9. "max": { "field": "@timestamp" }
10. },
11. "group_by_fields": {
12. "composite": {
13. "size": 1000,
14. "sources": [
15. { "geo.dest": { "terms": { "field": "geo.dest" } } },
16. { "machine.os": { "terms": { "field": "machine.os" } } }
17. ]
18. },
19. "aggregations": {
20. "bytes": {
21. "avg": { "field": "bytes" }
22. }
23. }
24. }
25. }
26. }
27. }
`AI写代码
composite 会为 (geo.dest, machine.os) 的每一个唯一组合生成一个 bucket。ML 节点会看到所有这些组合,并能够正确判断在某个国家响应大小激增时,哪个操作系统在其中起到了贡献作用。可以通过 preview 来确认是否出现了所有不同的 pair。如果你只看到很少的行,而预期应该有很多,则可能需要提高 composite 的 size 参数。
需要注意的是,这个 composite 是嵌套在 date_histogram 内部的,这与下面 rare、freq_rare 和 info_content 使用的顶层 composite 结构不同。这种区别非常重要:嵌套在 date_histogram 内的 composite 会使用标准 extractor;而顶层 composite 会使用 composite extractor,它会跨时间分页遍历所有 value 组合。
分类(categorization)
categorization 可以与 aggregated datafeeds 一起使用:summary_count_field_name 和 categorization_field_name 可以在同一个作业中共存。五步模式可以直接应用。步骤 2 使用标准的 date_histogram 拓扑结构。步骤 4 有一个调整:不再使用 partition 字段,而是对文本字段本身使用 terms 聚合,并在其 .keyword 子字段上执行,名称必须与 categorization_field_name 完全一致。步骤 5 可以跳过,因为 count detector 只依赖 doc_count。
analysis 配置:
`
1. {
2. "bucket_span": "1h",
3. "summary_count_field_name": "doc_count",
4. "categorization_field_name": "message",
5. "detectors": [
6. {
7. "function": "count",
8. "by_field_name": "mlcategory"
9. }
10. ],
11. "influencers": ["mlcategory"]
12. }
`AI写代码
datafeed aggregations:
`
1. {
2. "buckets": {
3. "date_histogram": {
4. "field": "@timestamp",
5. "fixed_interval": "1h"
6. },
7. "aggregations": {
8. "@timestamp": {
9. "max": { "field": "@timestamp" }
10. },
11. "message": {
12. "terms": {
13. "field": "message.keyword",
14. "size": 1000
15. }
16. }
17. }
18. }
19. }
`AI写代码
datafeed 会为每一个唯一的 message.keyword 值发送一个 bucket,并为每个 bucket 提供 doc_count。ML 节点接收这些字符串,对其进行 categorization,并为每个值分配一个 mlcategory,而 count detector 则跟踪每个 bucket 中每个类别的文档数量。步骤 4 中的命名规则同样适用:terms 聚合必须命名为 message,并且要与 analysis 配置中的 categorization_field_name 完全一致。
需要注意的一点是:keyword 字段默认有 ignore_above: 256 的限制。超过 256 个字符的日志消息不会被索引为 .keyword,因此会被静默排除在聚合之外。如果日志消息较长,在使用该方法前需要检查字段 mapping。可能需要在 index template 中提高该限制。
最简模式
time_of_day 和 time_of_week 是最容易聚合的函数:它们只需要时间戳和文档计数。C++ 进程从 bucket timestamp 中提取时间成分,并构建正常活动的周期模型;doc_count 用于表示每个 bucket 中的事件数量。不需要 terms source,不需要 metric 聚合,也不需要 composite。
analysis 配置:
`
1. {
2. "bucket_span": "15m",
3. "summary_count_field_name": "doc_count",
4. "detectors": [
5. { "function": "time_of_day" }
6. ]
7. }
`AI写代码
Datafeed aggregations:
`
1. {
2. "time": {
3. "date_histogram": {
4. "field": "@timestamp",
5. "fixed_interval": "15m"
6. },
7. "aggregations": {
8. "@timestamp": { "max": { "field": "@timestamp" } }
9. }
10. }
11. }
`AI写代码
plain date_histogram 就足够了;不需要 composite。这使得 time_of_day 和 time_of_week 特别适合 CCS 场景:每个时间块一个请求,网络传输数据量极小。time_of_week 也使用相同结构;唯一变化只是函数名称。
如果你想添加 partition_field_name(例如按 service 建模一天中的时间模式),可以在 histogram 的 aggregations 中按照标准步骤 4 的模式加入 terms 聚合。
composite 模式
针对 rare、freq_rare 和 info_content 的 composite 模式都需要 composite extractor,即遍历所有唯一值组合而不是截断为 top-N 的那个组件。五步模式在这里适用,但步骤 2 的拓扑结构不同:composite 位于顶层(而不是 date_histogram),date_histogram 作为其内部 source。步骤 3 将 max @timestamp 聚合放在与 composite 同级的位置,步骤 5 可以跳过,因为这三种函数都只依赖 doc_count。
这三种函数的数据结构是统一的:顶层是 composite,内部包含一个 date_histogram source,并且每个分析字段各有一个 terms source。唯一变化在于 terms source 的字段选择:
- rare 需要一个 by_field_name 的 source
- freq_rare 需要 by_field_name 和 over_field_name 两个 sources
- info_content 需要 field_name,以及可选的 by_field_name 或 over_field_name sources
这三者都不需要 metric aggregation。
`
1. {
2. "buckets": {
3. "composite": {
4. "size": 10000,
5. "sources": [
6. { "@timestamp": { "date_histogram": { "field": "@timestamp", "fixed_interval": "5m" } } },
7. { "by_field": { "terms": { "field": "by_field" } } },
8. { "over_field": { "terms": { "field": "over_field" } } }
9. ]
10. },
11. "aggregations": {
12. "@timestamp": { "max": { "field": "@timestamp" } }
13. }
14. }
15. }
`AI写代码
几点说明:
- composite 聚合必须是顶层聚合,不能嵌套在 date_histogram 内。这正是将 datafeed 路由到 composite extractor 的关键。
- date_histogram 是 composite 内部的一个 source,而不是外层包装。其 fixed_interval 必须能被 bucket_span 整除。
- @timestamp 上的 max 聚合与 composite 同级(在 aggregations 内部),不能嵌套在 composite 里面。
- composite.size 控制每次往返的分页大小。设置较高值(如 10000)可以减少往返次数,这在 CCS 延迟场景中很重要。当存在多个 source 和高基数字段时,总组合数可能非常大;extractor 会自动分页处理。
为什么基于 aggregation 的 datafeed 在大规模下更快
性能差距是结构性的,而不是偶然的。scroll-based datafeed 是逐页读取原始文档:每 1,000 条文档对应一次请求,并且必须等待前一个请求完成后才能发起下一个。因此,请求数量与回填时间范围内的文档总量成正比。在每小时 836,000 条事件、持续 13 个月的情况下,总量约为 79 亿条事件,也就是约 790 万次顺序往返。每一次往返都要跨越 CCS 边界、等待 shard 响应,并完整传输匹配文档。整个过程没有并行性:datafeed 在远程集群上保持 scroll context,并逐页处理数据。
而 aggregation-based datafeed 的工作方式完全不同。数据节点在本地对数据进行汇总,按时间 bucket 和分类字段分组,只将 bucket 结果发送给 ML 节点。请求数量与字段基数相关,而不是与文档数量相关。在我们的例子中,两个 influencer 字段产生 6 种唯一组合,每个时间 bucket 只生成 6 行结果;datafeed 只需对这些结果进行分页,而不管每个 bucket 中有多少原始事件。摄入速率翻倍,scroll 请求数翻倍;但 aggregation 请求数保持不变。这就是为什么规模越大差距越明显:数据越多,scroll 越差,aggregation 越有优势。
在实时数据场景中情况不同,因为每个实时 tick 只处理一个新的 bucket:scroll 会按该 bucket 的数据量发起多个分页请求,而 aggregation 只需要一次请求。在 836,000 events/hour、15 分钟 bucket span 的条件下,20× 的差异正是这个比例的体现。当(摄入速率 × bucket span)大于 scroll_size 时,scroll-based datafeed 就无法跟上实时数据,无论硬件如何扩展。低于这个阈值时,scroll 仍然可用,而 aggregation 只是优化;超过该阈值后,aggregation 成为唯一可持续方案。
scroll-based datafeed 是合理的默认值,Kibana wizards 在大多数部署中做出了正确选择。但在大规模场景(更多 shard、更宽索引模式、跨层 CCS)下,切换到 aggregation-based datafeed 是自然演进:数据节点在数据所在地做汇总,ML 节点处理压缩后的结果,检测逻辑保持不变。唯一需要提前注意的成本是模型状态:切换需要新建作业,因此越早迁移,损失越小。
如果遇到本文未覆盖的情况,例如某些 aggregation 无法映射或 composite 行为异常,可以到 Elastic Discuss 社区继续讨论。