Elastic 机器学习预测你的磁盘何时会填满:如何让它向你发出告警

作者:来自 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_lowerforecast_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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

为了本次演示,我们创建了一个稍微更高级的任务,该任务额外提供挂载点信息。这样,告警中不仅会包含哪个主机正在耗尽存储空间,还会包含受影响的挂载点。

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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

以下是几个值得说明的选择:

按主机和挂载点进行双重拆分。 partition_field_name: host.nameby_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 告警中提供更精确的超限时间戳。在生产环境中,许多团队更倾向于使用 6h1d 进行磁盘容量监控,因为这样可以减少结果索引的数据量,并平滑短期噪声。

调整索引模式( 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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)收起代码块![](https://csdnimg.cn/release/blogv2/dist/pc/img/arrowup-line-top-White.png)

运行它之前,以下几点 YAML 内容值得理解:

elasticsearch.request 在这里才会实际返回完整响应。在 9.4.2 测试中,elasticsearch.search 会移除聚合结果,只暴露 tooktimed_out_shardshits。由于 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_limitforecast_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+ 分区的生产集群,6h1d 是更合适的默认值。但这需要克隆并重新训练任务。

除了调整 bucket span,还有两个其他选择:

  • 缩短预测范围。 7 天预测结合 6h 的 bucket span 时,每个分区每次运行只产生 28 个文档,比博客中的默认配置减少 12 倍。大多数容量事件都会提前超过一周发出信号;14 天只是更充裕的缓冲时间,并不是硬性要求。
  • 拆分任务。 单任务模式在超过约 100k 个分区后会失效。可以按照数据中心、云区域或团队进行拆分------每个任务负责一个有限基数范围。 Workflow YAML 在所有任务之间保持一致;唯一变化的是 consts.job_idconsts.results_index。你可以为每个任务运行一个 workflow,也可以通过 workflow.executeAsync 进行并行调用。

此外,还有一个硬性的计算限制:文档说明,需要超过 500 MB 工作内存( spool 上限)的预测任务根本无法启动。这是预测阶段模型复杂度的限制,而不是结果文档数量的限制。对于磁盘使用模式来说,这通常不是第一个遇到的限制------结果索引数据量以及 ML 节点用于异常检测任务本身的内存,通常会更早成为瓶颈。

对于已经使用 Elastic 进行可观测性的团队来说,这种模式可以让容量告警继续保持在与其他数据相同的技术栈中。少评估一个产品,少部署一个 agent。

如何在 Kibana 中启用磁盘容量预测告警

在 Kibana 中通过"堆栈管理( Stack Management ) > 工作流( Workflows )"启用 Workflows(需要 Kibana 9.4+ 和 manage_workflows 权限)。然后:

  1. 使用上面的配置创建 ML 任务,并启动数据流。
  2. 等待模型积累一周的磁盘指标数据。
  3. disc-capacity-forecast-alert.yamlconsts 部分设置 slack_connector_idslack_channel。将 YAML 导入 Kibana 并启用工作流。

在第一次早晨运行时,我们要么会收到一条 Slack 告警,其中列出需要调查的主机;要么会收到一条控制台日志,确认所有内容都低于阈值。不会再有令人烦恼的突发磁盘空间耗尽事件。

原文:Forecast disk capacity with Elastic ML and Kibana Workflows --- Elastic Observability Labs

相关推荐
Elasticsearch1 小时前
500 个服务时快 6 倍:我们如何将 Kibana APM 服务地图从 Canvas 重构到 React DOM
elasticsearch
Ai拆代码的曹操13 小时前
Git 集成:AI 代理中的版本控制工作流设计
大数据·git·elasticsearch
Elasticsearch16 小时前
Elastic Observability 更新后的指标定价:业界领先的指标能力 —— 现在成本也更低!
elasticsearch
Elasticsearch16 小时前
Elastic 新的指标能力将显著提升公共部门 IT 的正常运行时间
elasticsearch
Elasticsearch19 小时前
Elastic 作为创始成员加入 Open Secure AI Alliance,与 NVIDIA 及行业领导者共同推动 AI 安全与保障
elasticsearch
Elasticsearch19 小时前
你的 agents 一直在记录凭证:将 Elastic Agent Builder 内置的 OTel traces 转换为 Kibana 中的 token 成本
elasticsearch
Elasticsearch20 小时前
一个提示词,一个完整工作流:Elastic 的 AI agent 为你编写自动化流程
elasticsearch
暖和_白开水21 小时前
数据分析agent(十三_7):meta_demo:ES
elasticsearch·数据挖掘·数据分析
Elasticsearch1 天前
快 17% 的搜索,零配置:Elasticsearch 中的自动校准向量量化
elasticsearch