作者:来自 Elastic Valeriy Khakhutskyy

使用单个 Kibana Workflows YAML 每天运行磁盘使用情况的 机器学习 预测,并获取 Slack 告警,其中列出哪些主机将在何时达到容量上限。
Elastic ML 已经可以提前 14 天预测磁盘使用情况,并提供上下限范围的置信区间。 Elastic 9.4 中的 Kibana Workflows 将该预测纳入每日自动化流程。一个 YAML 文件即可检查置信区间是否超过 90% 阈值,并发送 Slack 告警,在容量超限发生之前列出即将达到阈值的每个主机和挂载点。
以下是我们正在构建的完整流程:

什么是 Elastic ML 预测?
Elastic 的异常检测任务会对时间序列的正常行为进行建模。当模型学习到模式后,预测 API 可以基于该模式向未来进行预测,并提供置信区间。调用 POST /_ml/anomaly_detectors/{job_id}/_forecast 会在指定时间范围内运行预测,并将结果以 model_forecast 文档的形式写入任务结果索引中。
每个文档包含三个值: forecast_prediction(点估计值)、 forecast_lower 和 forecast_upper(95% 置信区间的上下限)。
对于容量告警场景,我们需要关注的是 forecast_upper,而不是 forecast_prediction。可以把它想象成天气预报:我们宁愿在最终是晴天的一天带上一把伞,也不希望因为相信过于乐观的预测而在下雨时被淋湿。

设置 ML 任务
ML 任务是一次性设置。它可以在集群的整个生命周期内持续运行。在预测结果变得有意义之前,它至少需要几天的数据;对于磁盘容量趋势分析,建议准备一到两周的数据。
我们可以直接使用 Kibana 提供的 异常检测 配置中内置的任务 max_disk_utilization_ecs。

为了本次演示,我们创建了一个稍微高级一些的任务,该任务额外提供挂载点信息。这样,告警中不仅会包含哪个主机正在耗尽存储空间,还会包含受影响的具体挂载点。
bash
`
1. {
2. "job_id": "disk-usage-forecast",
3. "analysis_config": {
4. "bucket_span": "1h",
5. "detectors": [
6. {
7. "function": "max",
8. "field_name": "system.filesystem.used.pct",
9. "partition_field_name": "host.name",
10. "by_field_name": "system.filesystem.mount_point"
11. }
12. ],
13. "influencers": ["host.name", "system.filesystem.mount_point"]
14. },
15. "data_description": {
16. "time_field": "@timestamp"
17. },
18. "datafeed_config": {
19. "datafeed_id": "datafeed-disk-usage-forecast",
20. "indices": ["metrics-system.filesystem-*"],
21. "query": {
22. "bool": {
23. "filter": [
24. { "exists": { "field": "system.filesystem.used.pct" } },
25. { "exists": { "field": "host.name" } },
26. { "exists": { "field": "system.filesystem.mount_point" } }
27. ]
28. }
29. }
30. },
31. "model_plot_config": { "enabled": false },
32. "results_index_name": "disk-usage-forecast",
33. "results_retention_days": 28
34. }
`AI写代码
为了本次演示,我们创建了一个稍微更高级的任务,该任务额外提供挂载点信息。这样,告警中不仅会包含哪个主机正在耗尽存储空间,还会包含受影响的挂载点。
bash
`
1. {
2. "job_id": "disk-usage-forecast",
3. "analysis_config": {
4. "bucket_span": "1h",
5. "detectors": [
6. {
7. "function": "max",
8. "field_name": "system.filesystem.used.pct",
9. "partition_field_name": "host.name",
10. "by_field_name": "system.filesystem.mount_point"
11. }
12. ],
13. "influencers": ["host.name", "system.filesystem.mount_point"]
14. },
15. "data_description": {
16. "time_field": "@timestamp"
17. },
18. "datafeed_config": {
19. "datafeed_id": "datafeed-disk-usage-forecast",
20. "indices": ["metrics-system.filesystem-*"],
21. "query": {
22. "bool": {
23. "filter": [
24. { "exists": { "field": "system.filesystem.used.pct" } },
25. { "exists": { "field": "host.name" } },
26. { "exists": { "field": "system.filesystem.mount_point" } }
27. ]
28. }
29. }
30. },
31. "model_plot_config": { "enabled": false },
32. "results_index_name": "disk-usage-forecast",
33. "results_retention_days": 28
34. }
`AI写代码
以下是几个值得说明的选择:
按主机和挂载点进行双重拆分。 partition_field_name: host.name 和 by_field_name: system.filesystem.mount_point 会让每个主机-挂载点组合拥有独立的模型。一台主机有五个分区,就会有五条独立的趋势线。字段实现已经针对这种模式支持约 50,000 个拆分(大约 10,000 个主机 × 5 个挂载点)。达到这个规模时,需要进行 bucket span 调优和任务拆分(参见下面的扩展部分)。
model_plot_config.enabled: false。 Kibana UI 会在模型图( model plot )达到大约 100 个序列时发出警告;关闭模型图后,该警告不再适用。我们告警的是预测结果索引,而不是模型图数据,因此不需要启用它。
Bucket span。 配置使用 1h,这可以在 Slack 告警中提供更精确的超限时间戳。在生产环境中,许多团队更倾向于使用 6h 或 1d 进行磁盘容量监控,因为这样可以减少结果索引的数据量,并平滑短期噪声。
调整索引模式( metrics-system.filesystem-* ),使其匹配你的 Elastic Agent 或 Metricbeat 配置。
创建任务后,让我们打开它并启动数据流:
bash
`
1. POST /_ml/anomaly_detectors/disk-usage-forecast/_open
2. PUT /_ml/datafeeds/datafeed-disk-usage-forecast/_start
`AI写代码
然后等待模型处理足够的历史数据,再运行工作流。
为什么使用预测,而不是对当前磁盘使用情况进行告警?
当磁盘达到 90% 时,阈值规则会触发告警。此时,我们已经进入事件响应模式,需要手忙脚乱地释放空间,以避免服务发生故障。而预测可以提供提前准备的时间。
回到天气预报的类比:阈值告警就像一个雨水传感器,只有在我们已经被淋湿时才会触发。 ML 预测则像天气预报,它会提前告诉我们明天需要带伞。通过每天运行一次 14 天预测,我们有足够时间采取行动:增加存储、迁移数据,以及清理日志,从而避免任何故障发生。
需要注意的一点是: ML 预测假设趋势会持续。如果某个主机的使用情况因为新应用部署或日志轮转突然发生变化,预测结果会滞后于新的实际情况。每天运行预测,它会反映当天的数据。如果跳过重新生成预测,那么你实际上是在基于一个可能已经不存在的趋势进行告警。
用于磁盘容量告警的 Kibana Workflows YAML
所有三个活动步骤都位于一个 YAML 文件中。顶部的 consts 部分用于设置任务 ID、阈值以及 Slack 连接器。其余逻辑在不同环境之间保持不变。
yaml
`
1. name: disk-capacity-forecast-alert
2. description: >
3. Daily ML forecast on disk-usage-forecast job; alert Slack when the upper
4. confidence bound crosses 90% at a future timestamp.
5. enabled: true
6. tags:
7. - machine-learning
8. - capacity-planning
9. - observability
11. consts:
12. job_id: disk-usage-forecast
13. results_index: .ml-anomalies-disk-usage-forecast
14. forecast_duration: 14d
15. forecast_expires_in: 24h
16. forecast_max_model_memory: 200mb # capped at min(500mb, 40% of job model_memory_limit)
17. breach_threshold: 0.9
18. max_partitions_in_alert: 20 # cap the list; total count is reported separately
19. slack_connector_id: REPLACE_WITH_SLACK_CONNECTOR_ID
20. slack_channel: REPLACE_WITH_SLACK_CHANNEL_ID
22. triggers:
23. - type: scheduled
24. with:
25. rrule:
26. freq: DAILY
27. interval: 1
28. tzid: UTC
29. byhour: [6]
30. byminute: [0]
32. steps:
33. # Step 1: generate a fresh 14-day forecast
34. - name: run_forecast
35. type: elasticsearch.request
36. with:
37. method: POST
38. path: "/_ml/anomaly_detectors/{{ consts.job_id }}/_forecast"
39. body:
40. duration: "{{ consts.forecast_duration }}"
41. expires_in: "{{ consts.forecast_expires_in }}"
42. max_model_memory: "{{ consts.forecast_max_model_memory }}"
44. # Step 2: poll until the forecast finishes and its docs are searchable
45. # (a fixed sleep races at high cardinality; this waits exactly as long as needed)
46. - name: wait_for_forecast
47. type: while
48. condition: "steps.check_forecast.output.hits.total.value < 1"
49. max-iterations:
50. limit: 60
51. on-limit: fail
52. steps:
53. - name: backoff
54. type: wait
55. with:
56. duration: 2s
57. - name: check_forecast
58. type: elasticsearch.request
59. with:
60. method: POST
61. path: "/{{ consts.results_index }}/_search"
62. body:
63. size: 0
64. query:
65. bool:
66. filter:
67. - term: { job_id: "{{ consts.job_id }}" }
68. - term: { result_type: model_forecast_request_stats }
69. - term: { forecast_id: "{{ steps.run_forecast.output.forecast_id }}" }
70. - term: { forecast_status: finished }
72. # Step 3: find future buckets where the upper bound crosses the threshold
73. - name: search_breaches
74. type: elasticsearch.request
75. with:
76. method: POST
77. path: "/{{ consts.results_index }}/_search"
78. body:
79. size: 0
80. runtime_mappings:
81. host_mount:
82. type: keyword
83. script:
84. source: "emit(doc['partition_field_value'].value + '|' + doc['by_field_value'].value)"
85. query:
86. bool:
87. filter:
88. - term: { job_id: "{{ consts.job_id }}" }
89. - term: { result_type: model_forecast }
90. - term: { forecast_id: "{{ steps.run_forecast.output.forecast_id }}" }
91. - range: { forecast_upper: { gte: "{{ consts.breach_threshold }}" } }
92. - range: { timestamp: { gte: now } } # future buckets only
93. aggs:
94. total_affected: # true distinct host×mount count
95. cardinality: { field: host_mount }
96. top_partitions: # bounded, most-urgent-first
97. multi_terms:
98. size: "{{ consts.max_partitions_in_alert }}"
99. terms:
100. - field: partition_field_value
101. - field: by_field_value
102. order: { earliest_breach: asc }
103. aggs:
104. earliest_breach: { min: { field: timestamp } }
105. peak_forecast: { max: { field: forecast_upper } }
107. # Step 4: alert only when breaches exist
108. - name: notify_if_breach
109. type: if
110. condition: "steps.search_breaches.output.hits.total.value > 0"
111. steps:
112. - name: slack_alert
113. type: slack.postMessage
114. connector-id: "{{ consts.slack_connector_id }}"
115. with:
116. channel: "{{ consts.slack_channel }}"
117. text: |
118. Disk capacity forecast alert (job: {{ consts.job_id }})
119. Future breach buckets (forecast_upper >= {{ consts.breach_threshold }}): {{ steps.search_breaches.output.hits.total.value }} across {{ steps.search_breaches.output.aggregations.total_affected.value }} partition(s)
120. Horizon: {{ consts.forecast_duration }}
122. Showing top {{ steps.search_breaches.output.aggregations.top_partitions.buckets | size }} partition(s), soonest breach first:
123. {% for b in steps.search_breaches.output.aggregations.top_partitions.buckets %}- {{ b.key[0] }} {{ b.key[1] }} - peak {{ b.peak_forecast.value | times: 100 | round: 1 }}%, first crosses on {{ b.earliest_breach.value_as_string | date: "%Y-%m-%d %H:%M UTC" }} ({{ b.doc_count }} future bucket(s))
124. {% endfor %}
125. else:
126. - name: no_breach_log
127. type: console
128. with:
129. message: "No disk forecast breaches above {{ consts.breach_threshold }} for {{ consts.job_id }}"
`AI写代码收起代码块
运行它之前,以下几点 YAML 内容值得理解:
elasticsearch.request 在这里才会实际返回完整响应。在 9.4.2 测试中,elasticsearch.search 会移除聚合结果,只暴露 took、timed_out、_shards 和 hits。由于 Slack 消息是通过遍历聚合桶来构建的,因此需要在 elasticsearch.request 中使用 method: POST 才能获得所需结果。
消息模板使用 Liquid 运行。 Workflows 不支持 Handlebars,因此如果你来自使用 Handlebars 的技术栈,{{#each}} 会导致工作流验证失败。而 {% for %} 循环以及类似 | times: 100 | round: 1 和 | date 的过滤器在 9.4.2 中都可以正常工作。
这里会持续轮询,直到预测真正完成,而不是通过固定等待时间进行猜测。 POST _forecast 几乎会立即返回一个 forecast_id,但结果文档是异步写入的。在高基数场景下,固定等待 5 秒会产生竞态条件。如果预测尚未完成,无论实际数据如何,超限搜索都会返回 0 条结果。因此, while 步骤会持续轮询 model_forecast_request_stats,直到 forecast_status: finished,这样它会精确等待预测所需的时间,而不会额外等待。 max-iterations 配合 on-limit: fail 表示如果预测卡住,会明确失败,而不是一直运行到默认的 2000 次迭代。
超限聚合使用 multi_terms,因为 composite 无法根据子聚合指标进行排序。 composite 聚合会按字母顺序返回分区,而固定大小会悄悄隐藏最严重的问题。 multi_terms 会按照 earliest_breach 排序,并将列表限制为 max_partitions_in_alert 个结果;同时,单独的 total_affected 基数聚合会报告真实的不重复分区数量,因此消息显示的是"137 个中的前 20 个( top 20 of 137 )",而不是无提示地 截断 结果。 multi_terms 的桶 key 也会以数组形式返回,因此使用 b.key[0](主机)和 b.key[1](挂载点)进行索引。 timestamp >= now 过滤条件同样很重要:如果没有它,查询会针对同一次预测运行中的历史桶触发告警,而不仅仅是未来桶。
模型内存和过期时间需要一起调整。预测的 max_model_memory 默认值为 20 MB,并限制为 min(500mb, 任务 model_memory_limit 的 40%)。如果预测超过该内存预算,数据会写入磁盘并且运行速度变慢,因此对于大型集群,应同时提高任务的 model_memory_limit 和 forecast_max_model_memory。每次每日运行还会向结果索引写入 horizon_buckets × partitions 个文档,这也是为什么 expires_in: 24h 能防止索引无限增长。
Slack 消息的样子
当工作流发现超限情况时,告警如下所示:
scss
`
1. Disk capacity forecast alert (job: disk-usage-forecast)
2. Future breach buckets (forecast_upper >= 0.9): 545 across 2 partition(s)
3. Horizon: 14d
5. Showing top 2 partition(s), soonest breach first:
6. - host-a / : peak 113.2%, first crosses on 2026-06-28 09:00 UTC (334 future bucket(s))
7. - host-b /data : peak 97.4%, first crosses on 2026-07-01 14:00 UTC (211 future bucket(s))
`AI写代码
"峰值 113.2%( Peak 113.2% )"看起来令人担忧,但它在数学上是正确的:当模型方差较大时,上置信区间可能会超过 100%。真正有用的数字是"首次超过阈值时间( first crosses on )"。这就是我们可以采取行动的提前时间。在大型集群中,列表会限制为 max_partitions_in_alert(并按照最早发生超限的顺序排列),而"影响 N 个分区( across N partition(s) )"仍然会报告真实总数,因此不会隐藏任何信息。
运维注意事项
基数。 当达到数万个分区(主机 × 挂载点)时,model_plot_config.enabled: false 不是可选项,而是必须关闭。在这种规模下,模型图数据量非常大,而对于告警场景并没有必要。超限搜索使用 multi_terms,限制为 max_partitions_in_alert(默认 20),按照最早超限时间排序,并通过基数聚合报告真实的不重复分区数量,因此即使数千个分区发生超限,告警仍然保持可读。
等待超时。 while 步骤中的 max-iterations 限制(60 次迭代,每次 2 秒)意味着预测最多等待 2 分钟完成。在非常高基数场景下,这可能不足;如果你的预测任务确实运行时间更长,请提高该限制。
预测质量。 ML 预测最适合具有稳定、可学习趋势的指标。磁盘使用情况通常符合这一特点。内存使用情况噪声更大,尤其是在工作负载变化较大的主机上,可能产生较宽的置信区间。如果 Slack 告警经常显示"峰值 400%( peak 400% )",说明模型可能在某些分区上存在欠拟合,此时可以对 forecast_upper 的最大值设置更严格的过滤条件,以减少噪声。
扩展到超过约 1,000 个分区
扩展指南明确给出了计算公式:
预测会针对每个 bucket 中的每个预测元素,向结果索引写入一个新文档。
因此:
每次运行的结果文档数量 = 预测时间范围中的 bucket 数量 × (主机数量 × 挂载点数量)。
按照本文中的 1h bucket span 和 14 天预测范围计算,每个分区每天运行一次预测会产生 336 个文档:
| 集群规模 | 分区数量 | 每次运行文档数 | 结果索引(24 小时 TTL) |
|---|---|---|---|
| 小型 PoC | 100 | 34 k | 约 17 MB |
| 约 300 个主机 × 10 个挂载点 | 3,000 | 1 M | 约 470 MB |
| 约 10k 个主机 × 3 个挂载点 | 30,000 | 10 M | 约 5 GB |
| 约 100k 个主机 × 3 个挂载点 | 300,000 | 100 M | 约 47 GB |
当每次运行产生约 10 M 个文档时,即使设置了 expires_in: 24h,索引负载和结果索引大小也会变得非常显著。 search_breaches 查询仍然保持快速(因为它的 size: 0,完全基于聚合),但每天向 .ml-anomalies-* 写入 10 M 个文档,同时还要处理正常工作负载,会带来实际压力。
最大的优化手段是 bucket span 。磁盘容量规划不需要在 14 天预测周期内保持小时级精度。"星期二首次超限"是可执行的信息;"首次超限发生在 UTC 10:00 到 11:00 之间"通常没有太大意义。增加 bucket span 会按比例减少文档数量:
| Bucket span | Bucket 数量(14 天) | 30k 个分区时的文档数 | 结果索引 |
|---|---|---|---|
| 1h(博客默认值) | 336 | 10 M | 约 5 GB |
| 6h | 56 | 1.7 M | 约 0.8 GB |
| 1d | 14 | 420 k | 约 200 MB |
对于拥有 10k+ 分区的生产集群,6h 或 1d 是更合适的默认值。但这需要克隆并重新训练任务。
除了调整 bucket span,还有两个其他选择:
- 缩短预测范围。 7 天预测结合
6h的 bucket span 时,每个分区每次运行只产生 28 个文档,比博客中的默认配置减少 12 倍。大多数容量事件都会提前超过一周发出信号;14 天只是更充裕的缓冲时间,并不是硬性要求。 - 拆分任务。 单任务模式在超过约 100k 个分区后会失效。可以按照数据中心、云区域或团队进行拆分------每个任务负责一个有限基数范围。 Workflow YAML 在所有任务之间保持一致;唯一变化的是
consts.job_id和consts.results_index。你可以为每个任务运行一个 workflow,也可以通过workflow.executeAsync进行并行调用。
此外,还有一个硬性的计算限制:文档说明,需要超过 500 MB 工作内存( spool 上限)的预测任务根本无法启动。这是预测阶段模型复杂度的限制,而不是结果文档数量的限制。对于磁盘使用模式来说,这通常不是第一个遇到的限制------结果索引数据量以及 ML 节点用于异常检测任务本身的内存,通常会更早成为瓶颈。
对于已经使用 Elastic 进行可观测性的团队来说,这种模式可以让容量告警继续保持在与其他数据相同的技术栈中。少评估一个产品,少部署一个 agent。
如何在 Kibana 中启用磁盘容量预测告警
在 Kibana 中通过"堆栈管理( Stack Management ) > 工作流( Workflows )"启用 Workflows(需要 Kibana 9.4+ 和 manage_workflows 权限)。然后:
- 使用上面的配置创建 ML 任务,并启动数据流。
- 等待模型积累一周的磁盘指标数据。
- 在
disc-capacity-forecast-alert.yaml的consts部分设置slack_connector_id和slack_channel。将 YAML 导入 Kibana 并启用工作流。
在第一次早晨运行时,我们要么会收到一条 Slack 告警,其中列出需要调查的主机;要么会收到一条控制台日志,确认所有内容都低于阈值。不会再有令人烦恼的突发磁盘空间耗尽事件。
原文:Forecast disk capacity with Elastic ML and Kibana Workflows --- Elastic Observability Labs