使用中位数绝对偏差进行异常检测

导航至

当您想要发现与其他不同的容器、虚拟机(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算法常用于此类异常检测,因为它非常有效且高效。在某个时间点所有时间序列的中位数或“中间”值描述了该时间戳所有时间序列的正常行为。每个时间序列与中位数的大偏差表明该序列是异常的。

这是从这篇文章(关于中值绝对偏差)中编辑的原始图像,该文章是此Flux包的灵感来源。中位数(粉红色)在每个时间戳确定。计算每个时间序列(灰色)与中位数的偏差。当偏差过大时,该序列被识别为异常(红色)。

上述描述是算法工作原理的简化概述。实际上,一个点通过以下公式被标记为异常:

  (1)

其中:

是时间点   (2)

是异常点。  (3)

是样本中位数,即跨系列点的批次中的中间值。  (4)

是用户定义的截止值,通常为2.5或3.0  (4)

并且 是中值绝对偏差。  (5)

其中,=1.4826 是一个缩放因子,它假设数据是正态分布的。

如果你不喜欢数学,这个算法可能看起来很复杂,但实际上非常简单。让我们用一个数值例子来分解到底发生了什么。

中位数绝对偏差的数值例子

在这个例子中,我们将查看来自不同主机的一些数据。我们的数据如下,有图表视图(右)和表格视图(左)

记住,MAD会将与中位数偏差较大的点标记为异常。因此,绿色线,主机3,在第3个时间点明显存在异常。让我们看看算法是如何确认这一点的。

今天我们将只关注第3个时间点发生的转换,因为MAD是在同一时间戳计算所有系列的异常。

  1. 按时间戳分组数据并排序我们的数据
  2. 找到中值或中间值 
  3. 获取每个系列中位数与中位数之间的绝对差异
  4. 排序并找到中位数绝对偏差  
  5. 乘以缩放因子 ,得到 = 0.14826。最后,我们将步骤3中计算出的值除以MAD。如果这个值超过了我们的阈值,那么我们就有了一个异常点。 

由于输出超过了我们的阈值,我们可以轻松地识别出主机3在第3个时间点的异常点。

识别异常系列

异常检测的一个挑战是减少误报和噪声警报的数量。幸运的是,在应用MAD于时间序列数据时,有一个非常简单的解决方案来解决这个问题。我们不需要对每个异常点发出警报,而是应该在一个指定的窗口内监控 mad() 的输出。如果某个单个系列在窗口内输出了一定比例的异常点,那么该系列在较长时间内表现出异常行为,这为我们分类该系列为异常提供了信心。

使用Flux计算MAD并标记异常

我们正在分析的数据集包含10个常规时间序列,时间跨度为2020年1月1日至2020年5月1日。您可能会发现,代表深紫色线的宿主在3月初表现出异常行为。

以下是本博客开头部分的图表副本。您可能会通过视觉检查发现3月初存在一些异常(深紫色)。

为了突出MAD的敏感性,我们将重点关注数据集的最后一周,在那里通过视觉检查无法明显发现异常行为。

InfluxDB UI中的线形图,展示了一个包含人为异常序列的模拟正弦波形时间序列数据集。

以下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")

输出结果如下所示

InfluxDB UI中的线形图,显示了由mad() Flux函数标记为异常的点。

我们看到一些序列表现出假阳性,例如“主机”12并不应该被标记为异常。然而,以下考虑因素很重要

  • 这个时间序列特别棘手,因为所有序列的标准差都很相似。
  • 请记住,一个敏感的异常检测系统比一个无响应的系统要好。通常最好是出现假阳性而不是假阴性。

我们有几种减少假阳性的方法

  • 我们可以降低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)

我们可以看到我们的异常序列具有最多的异常。

InfluxDB UI中的表格视图,显示了我们具有最多异常的序列。我们的异常序列被mad() Flux函数正确识别。

在大型数据集上使用MAD以协助根本原因分析

既然我们已经确信MAD函数对异常非常敏感,让我们看看它在大型数据集上的性能。在这个例子中,我们假设我们正在监控Web应用的响应时间。我们收集了包含各种用户响应时间的事件数据。标签描述了用户的地区和浏览器。在以下窗口中,我们有大约90k个点。

InfluxDB UI中的线形图,显示90k个点和15k个序列的响应时间。正常响应时间(橙色)。异常响应时间(洋红色)。

然后我们应用以下脚本的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")

InfluxDB UI中的散点图,显示由mad() Flux函数标记的异常响应时间。异常按时间分组。

现在我们可以通过按地区分组并计算具有

mydata |> mad(threshold:3.0)
       |> filter(fn: (r) => r.level == "anomaly")
       |> group(columns: ["region"])
       |> count()

我们得出了一些结论。使用IE6的用户响应时间较慢,在us-east-2地区可能存在一些问题,因为大部分异常都发生在这里。

在我们的InfluxDB UI中按区域分组的异常系列视图

一个请求和结论

在这篇文章中,我们学习了如何使用Flux编写简单但功能强大的异常检测算法。我们了解到它既非常敏感又高效。然而,如果我不概述后续的自然步骤和额外的工具,我会感到疏忽。

在InfluxDB 2.0和Flux中完成异常检测管道

  1. 编写一个任务,使用to()函数将这些异常写入新桶。
  2. 对异常进行警报
  3. 利用http.post()函数触发脚本并自动对警报采取纠正措施。

重要提示:由于此函数执行按时间分组的计算,您可以为MAD函数创建一个频繁运行的任务 - 您可以尝试模仿数据收集频率/间隔,而不是等待更长的时间范围。更频繁地执行MAD函数可以减少输入数据并提高性能,以适应更大的数据集。

使Flux和InfluxDB堆栈特别强大的一点是它们都是开源的。没有社区的帮助,InfluxDB和Telegraf不会是今天的样子。虽然Flux在测试版中已经非常强大,但它也需要您的帮助。这里来自anaisdg/anomalydetection Flux包的MAD函数是特殊的,因为它作为一个第三方Flux包被贡献。如果您编写了用于执行异常检测或其他预测的自定义Flux函数,我鼓励您贡献它们。让他人从您的辛勤工作中受益,并为您唱赞歌!

要了解更多关于贡献第三方Flux包的信息,请阅读这篇博客。一如既往,请在评论部分、我们的社区网站或我们的Slack频道中分享您的想法、关注点或问题。我们很乐意获取您的反馈并帮助您解决遇到的问题!再次感谢!