使用中位数绝对偏差进行异常检测
作者:Anais Dotis-Georgiou / 产品,用例,开发者
2020年7月7日
导航至
当您想要发现与其他不同的容器、虚拟机(VM)、服务器或传感器时,您可以使用中位数绝对偏差(MAD)算法来识别时间序列何时“偏离群体”。在本教程中,我们将使用来自第三方 Flux 包的mad()
函数——MAD 的 Flux 实现——从名为 anaisdg/anomalydetection 的包中进行异常主机的识别。我们将找出这些时间序列中哪个系列是异常的。
此数据集与 Linux 内核的磁盘 I/O 相似。然而,我生成这个时间序列数据集是因为我想展示 MAD 的强大功能。我们能够轻松地用 mad() 检测到异常的“主机”,即使异常序列在视觉检查中并不立即或容易察觉。
异常检测的经济价值
DevOps监控使您的组织能够衡量对维护您的服务级别协议(SLA)至关重要的关键性能指标。理想情况下,站点可靠性工程师(SRE)使用Devops监控来解决操作问题、提高可靠性和指导基础设施设计。异常检测算法,如MAD,使SRE能够快速识别不健康的容器、虚拟机或服务器。SRE能够越早发现可疑行为,就越快诊断和修复基础设施问题。根本原因分析是一种高度复杂的迭代问题解决方法。它涉及提出深入的问题并采用五问法。找到问题是开始,解决操作问题可能更具挑战性,需要更多的创造力。虽然人工智能还不足以自主进行根本原因分析和解决基础设施问题,但异常检测仍然有价值。异常检测引导站点可靠性工程师(SRE)和系统管理员沿着正确的路径减少事件解决时间或平均解决时间(MTTR)。MTTR的降低使公司能够履行其服务级别目标(SLO),提供良好的用户体验并鼓励合同续签。通过允许您编写用户定义的函数并集成自定义异常检测算法,Flux提供了这种价值。InfluxDB v2警报和通知使SRE能够实时响应异常。
中值绝对偏差算法的工作原理
MAD算法常用于此类异常检测,因为它非常有效且高效。在某个时间点所有时间序列的中位数或“中间”值描述了该时间戳所有时间序列的正常行为。每个时间序列与中位数的大偏差表明该序列是异常的。
上述描述是算法工作原理的简化概述。实际上,一个点通过以下公式被标记为异常:
其中:
并且 是中值绝对偏差。 (5)
其中,=1.4826 是一个缩放因子,它假设数据是正态分布的。
如果你不喜欢数学,这个算法可能看起来很复杂,但实际上非常简单。让我们用一个数值例子来分解到底发生了什么。
中位数绝对偏差的数值例子
在这个例子中,我们将查看来自不同主机的一些数据。我们的数据如下,有图表视图(右)和表格视图(左)
记住,MAD会将与中位数偏差较大的点标记为异常。因此,绿色线,主机3,在第3个时间点明显存在异常。让我们看看算法是如何确认这一点的。
今天我们将只关注第3个时间点发生的转换,因为MAD是在同一时间戳计算所有系列的异常。
- 按时间戳分组数据并排序我们的数据
- 找到中值或中间值
- 获取每个系列中位数与中位数之间的绝对差异
- 排序并找到中位数绝对偏差
- 乘以缩放因子 ,得到 = 0.14826。最后,我们将步骤3中计算出的值除以MAD。如果这个值超过了我们的阈值,那么我们就有了一个异常点。
由于输出超过了我们的阈值,我们可以轻松地识别出主机3在第3个时间点的异常点。
识别异常系列
异常检测的一个挑战是减少误报和噪声警报的数量。幸运的是,在应用MAD于时间序列数据时,有一个非常简单的解决方案来解决这个问题。我们不需要对每个异常点发出警报,而是应该在一个指定的窗口内监控 mad()
的输出。如果某个单个系列在窗口内输出了一定比例的异常点,那么该系列在较长时间内表现出异常行为,这为我们分类该系列为异常提供了信心。
使用Flux计算MAD并标记异常
我们正在分析的数据集包含10个常规时间序列,时间跨度为2020年1月1日至2020年5月1日。您可能会发现,代表深紫色线的宿主在3月初表现出异常行为。
为了突出MAD的敏感性,我们将重点关注数据集的最后一周,在那里通过视觉检查无法明显发现异常行为。
以下Flux脚本识别哪些点是异常的
请注意:此Flux函数也是第三方Flux包的一部分,因此您无需自己编写函数,但我想在这个示例中分享这个函数。在下一个示例中,我们将查看如何导入MAD第三方Flux函数以方便使用。
import "experimental"
import "math"
mydata = from(bucket: "MAD_Example")
|> range(start: 2020-04-01, stop: 2020-05-01)
|> filter(fn: (r) => r["_measurement"] == "example_data")
mad = (table=<-, threshold=3.0) => {
// Step One: MEDiXi = med(x)
data = table |> group(columns: ["_time"], mode:"by")
med = data |> median(column: "_value")
// Step Two: diff = |Xi - MEDiXi| = math.abs(xi-med(xi))
diff = join(tables: {data: data, med: med}, on: ["_time"], method: "inner")
|> map(fn: (r) => ({ r with _value: math.abs(x: r._value_data - r._value_med) }))
|> drop(columns: ["_start", "_stop", "_value_med", "_value_data"])
// The constant k is needed to make the estimator consistent for the parameter of interest.
// In the case of the usual parameter a at Gaussian distributions b = 1.4826
k = 1.4826
// Step Three and Four: diff_med = MAD = k * MEDi * |Xi - MEDiXi|
diff_med =
diff
|> median(column: "_value")
|> map(fn: (r) => ({ r with MAD: k * r._value}))
|> filter(fn: (r) => r.MAD > 0.0)
output = join(tables: {diff: diff, diff_med: diff_med}, on: ["_time"], method: "inner")
// Step Five: Divide by MAD in Step Three and compare values against threshold
|> map(fn: (r) => ({ r with _value: r._value_diff/r._value_diff_med}))
|> map(fn: (r) => ({ r with
level:
if r._value >= threshold then "anomaly"
else "normal"
}))
return output
}
// Apply the mad() function to my data, specify the threshold and filter for anomalies.
mydata |> mad(threshold:3.0)
|> filter(fn: (r) => r.level == "anomaly")
输出结果如下所示
mad()
Flux函数标记为异常的点。我们看到一些序列表现出假阳性,例如“主机”1
和2
并不应该被标记为异常。然而,以下考虑因素很重要
- 这个时间序列特别棘手,因为所有序列的标准差都很相似。
- 请记住,一个敏感的异常检测系统比一个无响应的系统要好。通常最好是出现假阳性而不是假阴性。
我们有几种减少假阳性的方法
- 我们可以降低
mad()
阈值(例如,降至2.5或2.0)。 - 我们可以使用Flux计算每个序列在窗口内异常点的百分比。然后我们为数据集定义一个异常百分比阈值。如果一个序列在窗口内有高百分比的异常点,那么我们将其标记为异常。
- 我们只需监控异常最多的序列。如果我们把上面的查询加上
mydata |> mad(threshold:3.0)
|> filter(fn: (r) => r.level == "anomaly")
|> count(column: "level")
|> sort(columns: ["level"], desc: true)
|> limit(n:1)
我们可以看到我们的异常序列具有最多的异常。
mad()
Flux函数正确识别。在大型数据集上使用MAD以协助根本原因分析
既然我们已经确信MAD函数对异常非常敏感,让我们看看它在大型数据集上的性能。在这个例子中,我们假设我们正在监控Web应用的响应时间。我们收集了包含各种用户响应时间的事件数据。标签描述了用户的地区和浏览器。在以下窗口中,我们有大约90k个点。
然后我们应用以下脚本的MAD,在我本地机器上执行只用了6秒,其他进程正在运行。在这个例子中,我在编译Flux之后导入包。一旦拉取请求合并,您应该能够像这样应用MAD
import "contrib/anaisdg/anomalydetection"
mydata = from(bucket: "my-app-response")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "my-app")
|> filter(fn: (r) => r["_field"] == "response_time")
mydata |>anomalydetection.mad(threshold:3.0)
|> filter(fn: (r) => r.level == "anomaly")
mad()
Flux函数标记的异常响应时间。异常按时间分组。现在我们可以通过按地区分组并计算具有
mydata |> mad(threshold:3.0)
|> filter(fn: (r) => r.level == "anomaly")
|> group(columns: ["region"])
|> count()
我们得出了一些结论。使用IE6的用户响应时间较慢,在us-east-2地区可能存在一些问题,因为大部分异常都发生在这里。
一个请求和结论
在这篇文章中,我们学习了如何使用Flux编写简单但功能强大的异常检测算法。我们了解到它既非常敏感又高效。然而,如果我不概述后续的自然步骤和额外的工具,我会感到疏忽。
在InfluxDB 2.0和Flux中完成异常检测管道
- 编写一个任务,使用to()函数将这些异常写入新桶。
- 对异常进行警报。
- 利用http.post()函数触发脚本并自动对警报采取纠正措施。
重要提示:由于此函数执行按时间分组的计算,您可以为MAD函数创建一个频繁运行的任务 - 您可以尝试模仿数据收集频率/间隔,而不是等待更长的时间范围。更频繁地执行MAD函数可以减少输入数据并提高性能,以适应更大的数据集。
使Flux和InfluxDB堆栈特别强大的一点是它们都是开源的。没有社区的帮助,InfluxDB和Telegraf不会是今天的样子。虽然Flux在测试版中已经非常强大,但它也需要您的帮助。这里来自anaisdg/anomalydetection Flux包的MAD函数是特殊的,因为它作为一个第三方Flux包被贡献。如果您编写了用于执行异常检测或其他预测的自定义Flux函数,我鼓励您贡献它们。让他人从您的辛勤工作中受益,并为您唱赞歌!
要了解更多关于贡献第三方Flux包的信息,请阅读这篇博客。一如既往,请在评论部分、我们的社区网站或我们的Slack频道中分享您的想法、关注点或问题。我们很乐意获取您的反馈并帮助您解决遇到的问题!再次感谢!