机器学习异常检测:作业与检测器、分桶分析、结果解读与告警联动

系统讲解 Elasticsearch 机器学习异常检测:作业与检测器的配置方式、数据源与分桶设计、模型训练与结果解读、异常分数调优,以及与告警体系的联动落地。

阈值告警的困境在于阈值本身:定得太松漏报,定得太紧误报,而业务量本身有周期、有趋势、有突发,一个静态数字根本无法同时适配工作日与节假日。机器学习异常检测换了个思路:不问你「超过多少算异常」,而是让模型学习历史规律,再告诉你「这个点偏离规律有多远」。本文从无监督异常检测的原理讲起,覆盖作业与检测器的配置、数据源与分桶设计、模型训练与结果解读、调优手段,最后落到告警联动与运维容量。

1. 异常检测的基本原理

一句话总结: 异常检测用无监督模型学习时间序列的正常模式,再按偏离程度给出 0 到 100 的异常分数。

1.1 与阈值告警的区别

阈值告警:value > 1000          → 触发
异常检测:value 偏离模型预测的分布 → 给出异常分数

假设某服务的错误数在白天是 500 上下、凌晨是 50 上下。阈值设为 800 会漏掉凌晨的 300(相对正常值已经异常),也会误报白天的 900(其实在正常波动内)。异常检测能同时识别这两种情况,因为它对每个时间点都有一个基于上下文的期望值。

1.2 无监督与有监督

这意味着它不需要「历史异常样本」作为训练集,冷启动成本低。代价是它只能发现「与自身历史不符」的异常,无法识别「历史上一直如此但业务上确实错误」的情况。

1.3 典型适用场景

  • 服务错误率、响应时间的异常抬升。
  • 订单量、支付量的突然下跌或暴涨。
  • 登录失败次数、特定 IP 请求量的突增(安全场景)。
  • 主机 CPU、磁盘 IO 的偏离常态。

2. 作业与检测器

一句话总结: 作业(job)是检测的容器,检测器(detector)定义具体监控哪个字段、用什么函数。

2.1 作业与检测器的关系

curl -X PUT "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly" -H 'Content-Type: application/json' -d '
{
  "description": "Nginx 请求量与响应时间异常检测",
  "analysis_config": {
    "bucket_span": "15m",
    "detectors": [
      { "detector_description": "每分钟请求数异常", "function": "count",
        "by_field_name": "status_family" },
      { "detector_description": "响应时间均值异常", "function": "mean",
        "field_name": "response_time_ms", "by_field_name": "status_family" }
    ],
    "influencers": ["client_ip", "upstream_host"]
  },
  "data_description": { "time_field": "@timestamp" }
}'

by_field_name 表示按该字段拆分独立建模:200 与 500 各自的请求数分别建模,而不是混在一起。

2.2 常用函数

函数含义
count分桶内文档条数
mean指定字段的均值
sum指定字段求和
min指定字段最小值
max指定字段最大值
high_mean只检测高于期望的偏离
low_count只检测低于期望的条数
metric对字段做单值聚合
time_of_day检测事件发生时间本身的异常

high_mean 与 low_mean 这类单侧函数能显著降低误报,因为很多场景我们只关心单向偏离,例如响应时间只关心变慢。

2.3 分桶间隔的选择

bucket_span 的经验值是「业务周期的百分之一到五十分之一」。日报类指标用 1h,实时监控用 1m 到 15m。粒度过细会让模型把噪声当异常,粒度过粗会让异常被平均掉。

2.4 数据馈送与作业开启

curl -X PUT "localhost:9200/_ml/datafeeds/datafeed-nginx-traffic" -H 'Content-Type: application/json' -d '
{
  "job_id": "nginx-traffic-anomaly",
  "indices": ["nginx-access-*"],
  "query": {
    "bool": {
      "filter": [
        { "range": { "@timestamp": { "gte": "now-90d" } } }
      ]
    }
  },
  "scroll_size": 1000,
  "chunking_config": {
    "mode": "auto"
  }
}'

之后依次启动 datafeed 与作业:

curl -X POST "localhost:9200/_ml/datafeeds/datafeed-nginx-traffic/_start?pretty"
curl -X POST "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/_open?pretty"

3. 数据源与分桶

一句话总结: 数据源的质量决定检测的上限:时间字段必须准确、指标必须稳定、基数必须可控。

3.1 时间字段与时区

若数据里的时间字段是「入库时间」而非「事件时间」,批量补数据时会出现时间堆积,模型会学到假的周期。建议始终保留真实事件时间字段。

3.2 指标字段的类型

# 检查字段类型是否可用于聚合
curl -s "localhost:9200/nginx-access-*/_mapping/field/response_time_ms?pretty"

若字段被映射成 keyword,需要重建索引或使用运行时字段(runtime field)做类型转换。

3.3 分桶与基数控制

# 检查候选拆分字段的基数
curl -X GET "localhost:9200/nginx-access-*/_search?pretty" -H 'Content-Type: application/json' -d '
{
  "size": 0,
  "aggs": {
    "cardinality_ip": { "cardinality": { "field": "client_ip" } }
  }
}'

若 client_ip 基数是百万级,用它做 by_field_name 会产生百万个模型,作业必然失败。高基数字段应作为 influencers(影响因素)而非拆分维度。

3.4 数据预处理

curl -X PUT "localhost:9200/_ingest/pipeline/ml-prep" -H 'Content-Type: application/json' -d '
{
  "processors": [
    {
      "script": {
        "lang": "painless",
        "source": "if (ctx.response_time_ms == null) { ctx.response_time_ms = 0; }"
      }
    },
    {
      "set": {
        "field": "status_family",
        "value": "5xx",
        "if": "ctx.status != null && ctx.status >= 500"
      }
    }
  ]
}'

4. 模型训练与结果

一句话总结: 作业启动后先进入学习期,用历史数据建立基线,之后逐桶输出结果与异常分数。

4.1 学习期

对于有周周期的指标,学习期至少要两周;有月周期的指标需要两个月。学习期过短会让模型把「正常的高峰」学成「异常」。

4.2 结果写入的索引

curl -X GET "localhost:9200/.ml-anomalies-nginx-traffic-anomaly/_search?pretty" -H 'Content-Type: application/json' -d '
{
  "size": 5,
  "sort": [ { "record_score": "desc" } ],
  "query": { "bool": { "filter": [
    { "term": { "result_type": "record" } },
    { "range": { "record_score": { "gte": 75 } } }
  ] } }
}'

结果分三类:bucket 是整桶汇总,record 是单个实体的异常记录,influencer 是影响因素记录。

4.3 关键分数

分数含义
record_score单条记录的异常分数,0 到 100
initial_record_score首次出现时的分数,用于识别新出现的实体
anomaly_score作业级别的归一化异常度
typical该时间点的期望值
actual该时间点的实际值
probability实际值在模型分布中的概率

typical 与 actual 的对比是解读的核心:分数高且 actual 明显偏离 typical,才是真异常。

4.4 用 API 查看结果

curl -X GET "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/results/records?pretty" -H 'Content-Type: application/json' -d '
{
  "sort": "record_score",
  "desc": true,
  "start": "now-24h",
  "end": "now"
}' | head -40

5. 结果解读与调优

一句话总结: 调优的目标是让高分数对应真异常,手段包括调检测器、调分桶、加影响因素与配置排除规则。

5.1 误报的常见原因

  • 数据断流:某个分桶没有数据,count 检测器会报「低于期望」,其实是采集断了。
  • 粒度太细:1m 分桶在低流量时段噪声极大,建议改 15m。
  • 业务突变:大促、版本发布等真实变化被当成异常,需要配置 model_plot 或调整学习期。

5.2 检测器级别的排除规则

curl -X POST "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/_update?pretty" -H 'Content-Type: application/json' -d '
{
  "detectors": [
    {
      "detector_index": 0, "detector_description": "每分钟请求数异常",
      "function": "count", "by_field_name": "status_family",
      "detector_rules": [
        { "actions": ["skip_result"], "scope": { "status_family": "4xx" },
          "conditions": [
            { "applies_to": "actual", "operator": "lt", "value": 100 }
          ] }
      ]
    }
  ]
}'

上面的规则表示:4xx 状态下实际值低于 100 时不输出结果,专门抑制低流量时段的噪声。

5.3 影响因素分析

curl -X GET "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/results/influencers?pretty" -H 'Content-Type: application/json' -d '
{
  "sort": "influencer_score",
  "desc": true,
  "start": "now-24h",
  "end": "now"
}' | head -40

若异常的影响因素指向某个 upstream_host,说明问题很可能是该上游的故障,而不是整体流量变化。

5.4 分数阈值的选定

建议流程:先按 record_score >= 50 导出最近一个月的历史结果,人工标注哪些是真异常,再画出分数与精确率的曲线,选一个精确率可接受的阈值。经验上 75 到 90 之间是常用区间,但必须用自己数据验证。

6. 告警联动

一句话总结: 异常检测结果本身不是告警,需要接到告警引擎里做聚合、抑制与通知。

6.1 基于 Watcher 的告警

curl -X PUT "localhost:9200/_watcher/watch/ml-anomaly-alert" -H 'Content-Type: application/json' -d '
{
  "trigger": { "schedule": { "interval": "5m" } },
  "input": {
    "search": { "request": {
      "indices": [".ml-anomalies-nginx-traffic-anomaly"],
      "body": {
        "size": 10,
        "query": { "bool": { "filter": [
          { "term": { "result_type": "record" } },
          { "range": { "record_score": { "gte": 80 } } },
          { "range": { "timestamp": { "gte": "now-5m" } } }
        ] } },
        "sort": [ { "record_score": "desc" } ]
      }
    } }
  },
  "condition": { "compare": { "ctx.payload.hits.total": { "gt": 0 } } },
  "actions": {
    "notify_webhook": {
      "webhook": {
        "method": "POST",
        "url": "https://alert-gateway.internal/api/v1/events",
        "body": "{\"source\":\"ml-anomaly\",\"count\":{{ctx.payload.hits.total}}}"
      }
    }
  }
}'

6.2 与外部告警平台集成

# 告警平台直接查询异常索引(示意)
curl -s "localhost:9200/.ml-anomalies-nginx-traffic-anomaly/_search" -H 'Content-Type: application/json' -d '
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "result_type": "record" } },
        { "range": { "record_score": { "gte": 75 } } },
        { "range": { "timestamp": { "gte": "now-15m" } } }
      ]
    }
  }
}' | jq '.hits.hits | length'

这样可以把 ML 异常与普通阈值告警放在同一个通道里做去重与抑制。

6.3 抑制与降噪

常见策略:按 job_id 加 entity 做聚合,同一实体 30 分钟内只通知一次;对 record_score 在阈值附近抖动的记录,用连续 N 个桶都超阈值才告警的方式过滤。

6.4 闭环反馈

在告警平台上给每条异常加「确认为故障」与「标记为误报」两个按钮,定期统计误报率,把高频误报的模式转成 detector_rules 的排除条件。

7. 运维与容量

一句话总结: ML 作业消耗内存与 CPU,需要监控作业状态、模型大小与结果索引体积。

7.1 作业状态监控

curl -s "localhost:9200/_ml/anomaly_detectors/_stats?pretty" | head -40
curl -s "localhost:9200/_cat/ml/anomaly_detectors?v&h=id,state,buckets.count,model_size,memory_status"

memory_status 若长期为 soft_limit 或 hard_limit,说明模型太大,需要降低拆分基数或调整 model_memory_limit。

7.2 模型内存限制

curl -X PUT "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/_update?pretty" -H 'Content-Type: application/json' -d '
{
  "analysis_limits": {
    "model_memory_limit": "512mb"
  }
}'

修改内存限制需要先关闭作业。注意该值不能超过节点 JVM 堆的合理比例。

7.3 结果索引的生命周期

curl -X PUT "localhost:9200/_ilm/policy/ml-anomalies-policy" -H 'Content-Type: application/json' -d '
{
  "policy": {
    "phases": {
      "hot": { "actions": { "rollover": { "max_age": "30d" } } },
      "delete": { "min_age": "180d", "actions": { "delete": {} } }
    }
  }
}'

7.4 作业的启停与迁移

curl -X POST "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/_close?pretty"
curl -X POST "localhost:9200/_ml/anomaly_detectors/nginx-traffic-anomaly/_open?pretty"

升级或迁移集群时,需要先关闭 datafeed 与作业,迁移完成后再启动,避免数据重复消费。

8. 总结

环节要点
基本原理无监督学习时间序列正常模式,输出 0 到 100 的异常分数
作业与检测器一个作业多个检测器,by_field 拆分实体,influencers 记录影响因素
数据源时间字段用真实事件时间,指标须为数值型,高基数字段不能做拆分
分桶设计bucket_span 取业务周期的百分之一到五十分之一,过细噪声大
模型训练学习期至少覆盖两个业务周期,期间结果不可靠
结果解读关注 record_score 与 typical 和 actual 的差距,用 influencers 定位根因
调优手段detector_rules 抑制已知误报,用历史回放确定阈值
告警联动接 Watcher 或外部告警平台,做聚合抑制与误报反馈闭环
运维容量监控 memory_status 与模型大小,用 ILM 清理结果索引

异常检测把「定阈值」这件难事交给了模型,但模型不是免维护的:数据质量、分桶粒度、排除规则、告警抑制,每一项都需要持续投入。把它当成一个需要调教的同事而非一次性配置,才能让误报率降下来、让真异常浮出来。下一篇我们回到集群自身,讲 JVM 堆与 GC 调优如何决定节点的稳定性上限。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 可搜索快照与冻结层:把冷数据放进对象存储还能查
  2. 分页与深度分页:from/size、search_after、PIT 与 scroll
  3. 嵌套与父子关联查询:nested、join 字段与性能取舍