阈值告警的困境在于阈值本身:定得太松漏报,定得太紧误报,而业务量本身有周期、有趋势、有突发,一个静态数字根本无法同时适配工作日与节假日。机器学习异常检测换了个思路:不问你「超过多少算异常」,而是让模型学习历史规律,再告诉你「这个点偏离规律有多远」。本文从无监督异常检测的原理讲起,覆盖作业与检测器的配置、数据源与分桶设计、模型训练与结果解读、调优手段,最后落到告警联动与运维容量。
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 调优如何决定节点的稳定性上限。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。