如何使用Prometheus检测GitLab中的异常


Prometheus查询语言的基本功能之一是时间序列的实时汇总您还可以使用Prometheus查询语言来检测时间序列数据中的异常。 

Mail.ru云解决方案团队已经翻译由GitLab基础架构团队的工程师的文章,在那里你会找到的代码示例,你可以尝试在你的系统中。

什么是异常检测?


确定GitLab异常检测的重要性的四个主要原因:

  1. 事件诊断:我们可以通过减少事件检测时间(MTTD)来检测哪些服务超出了正常范围,从而提供了更快的解决方案。
  2. 检测性能下降:例如,如果将回归引入到服务中,导致它过于频繁地访问另一个服务,则我们可以快速检测并解决此问题。
  3. 检测和消除滥用:GitLab提供了交付和集成机制(GitLab CI / CD)和托管(GitLab Pages),并且只有少数用户可以使用这些机制。
  4. 安全性:异常检测对于检测GitLab时间序列中的异常趋势非常重要。

由于这些和其他原因,本文的作者决定找出如何使用Prometheus查询和规则在GitLab时间序列中配置异常的定义。

正确的聚合级别是多少?


首先,必须正确汇总时间序列。在下面的示例中,我们使用标准的http_requests_total计数器来检索数据,尽管许多其他指标也可以这样做。

http_requests_total{
 job="apiserver",
 method="GET",
 controller="ProjectsController",
 status_code="200",
 environment="prod"
}

该测试指标具有多个参数:方法,控制器,状态码(status_code),环境以及Prometheus自身添加的参数,例如作业和实例。

现在,您需要选择正确的数据聚合级别。太多或太少-所有这些对于检测异常都很重要。如果数据汇总过多,则存在两个潜在问题:

  1. 您可以跳过异常,因为聚合隐藏了数据子集中出现的问题。
  2. 如果发现异常,则不付出额外努力就很难将其与系统的单独部分相关联。

如果数据聚合不充分,则可能导致误报次数增加,以及将有效数据错误解释为错误。

根据我们的经验,最正确的聚合级别是服务级别,也就是说,我们包括工作(工作)标签和环境(环境)标签,并丢弃所有其他标签。

我们将在本文中讨论的聚合包括:job-http_request,聚合-五分钟,这是根据五分钟内的工作和环境计算得出的。

- record: job:http_requests:rate5m
expr: sum without(instance, method, controller, status_code)
(rate(http_requests_total[5m]))
# --> job:http_requests:rate5m{job="apiserver", environment="prod"}  21321
# --> job:http_requests:rate5m{job="gitserver", environment="prod"}  2212
# --> job:http_requests:rate5m{job="webserver", environment="prod"}  53091

从上面的示例中可以明显看出,从工作和环境的上下文中选择了一系列http_requests_total子集,然后将其数目考虑了5分钟。

使用Z分数检测异常


统计的基本原理可用于检测异常。

如果您知道Prometheus系列的均值和标准差,则可以使用该系列中的任何样本来计算z得分。 

Z分数是对观测值或测量值的相对范围的度量,它显示了其范围相对于平均值的标准偏差。  也就是说,z分数= 0意味着z分数与具有标准分布的数据集中的平均值相同,而z分数= 1意味着与平均值的标准偏差= 1.0。



我们假设基本数据具有正态分布,这意味着99.7%的样本的z得分从0到3。z得分距离零越远,则其存在的可能性就越小。 

我们应用此属性来检测Prometheus数据系列中的异常:

  1. 我们使用样本量较大的数据计算指标的均值和标准差。在此示例中,我们使用每周数据。如果我们假设记录每分钟评估一次,那么一周内我们将有10,000多个样本。

    # Long-term average value for the series
    - record: job:http_requests:rate5m:avg_over_time_1w
    expr: avg_over_time(job:http_requests:rate5m[1w])
    
    # Long-term standard deviation for the series
    - record: job:http_requests:rate5m:stddev_over_time_1w
    expr: stddev_over_time(job:http_requests:rate5m[1w])

  2. 一旦获得聚合的平均值和标准偏差,我们就可以计算Prometheus查询的z得分。

    # Z-Score for aggregation
    (
    job:http_requests:rate5m -
    job:http_requests:rate5m:avg_over_time_1w
    ) /  job:http_requests:rate5m:stddev_over_time_1w


根据正态分布的统计原理,假定+3到-3范围之外的任何值都是异常。因此,我们可以创建有关此类异常的警告。例如,当我们的聚合超出此范围超过五分钟时,我们将收到警报。


48小时内每秒对GitLab页面服务的请求数的图表。+3到-3范围内的Z分数以绿色突出显示

,由于该值没有度量单位,因此 Z分数可能难以在图形上解释。但是,此图中的异常非常容易确定。超出绿色区域的所有内容均显示为异常值,绿色区域显示z得分为-3到+3的值的走廊。

如果数据分发不正常怎么办


我们假设数据分布是正常的。否则,我们的z得分将为false。

有很多统计技巧可以确定数据分布的正态性,但是最好的方法是检查数据的z得分是否在-4.0到+4.0范围内。

两个Prometheus查询,显示最小和最大z得分:

(
max_over_time(job:http_requests:rate5m[1w]) - avg_over_time(job:http_requests:rate5m[1w])
) / stddev_over_time(job:http_requests:rate5m[1w])
# --> {job="apiserver", environment="prod"}  4.01
# --> {job="gitserver", environment="prod"}  3.96
# --> {job="webserver", environment="prod"}  2.96

(
 min_over_time(job:http_requests:rate5m[<1w]) - avg_over_time(job:http_requests:rate5m[1w])
) / stddev_over_time(job:http_requests:rate5m[1w])
# --> {job="apiserver", environment="prod"}  -3.8
# --> {job="gitserver", environment="prod"}  -4.1
# --> {job="webserver", environment="prod"}  -3.2

如果您的结果在-20到+20的范围内,则意味着使用了过多的数据并且结果失真。还要记住,您必须使用汇总行。没有正态分布的度量标准包括错误率,延迟,队列长度等参数。但是,在固定警报阈值的情况下,这些指标中的许多指标会更好地工作。

统计季节性异常检测


尽管计算z分数与时序数据的正态分布效果很好,但是还有第二种方法可以给出更准确的异常检测结果。这是统计季节性的使用。 

季节性是时间序列度量标准的特征,当该度量标准经历定期且可预测的更改(在每个周期中重复进行)时。


从星期一到星期日连续四周的每秒请求数(RPS)

图表上面图表说明了从星期一到星期日连续7周的 7天的RPS(每秒请求数)。这七天的范围称为“偏移”,即用于测量的模式。

图表上的每个星期都有不同的颜色。数据的季节性由图表中所示趋势的顺序表示-每个星期一早晨,我们观察到RPS都有类似的增加,而在星期五晚上,我们总是观察到RPS下降。

使用时间序列数据中的季节性,我们可以更准确地预测异常的发生及其检测。 

如何使用季节性


普罗米修斯使用几种不同的统计机制来计算季节性。

首先,我们进行计算,将本周的增长趋势添加到前一周的结果中。增长趋势计算如下:从上周的移动平均值中减去上周的移动平均值。

- record: job:http_requests:rate5m_prediction
  expr: >
    job:http_requests:rate5m offset 1w          # Value from last period
    + job:http_requests:rate5m:avg_over_time_1w # One-week growth trend
    — job:http_requests:rate5m:avg_over_time_1w offset 1w

第一次迭代结果有些“狭窄”-我们使用本周和上周的五分钟窗口来获取预测。

在第二次迭代中,我们通过获取上一周的四个小时周期的平均值并将其与当前一周进行比较来扩展我们的覆盖范围。 

因此,如果我们尝试预测星期一早上八点的指标值,而不是前一周的相同五分钟窗口,我们将指标的平均值从上周一的早上六点选到十点。

- record: job:http_requests:rate5m_prediction
  expr: >
    avg_over_time(job:http_requests:rate5m[4h] offset 166h) # Rounded value from last period
    + job:http_requests:rate5m:avg_over_time_1w    # Add 1w growth trend
    - job:http_requests:rate5m:avg_over_time_1w offset 1w

该请求显示166小时,比整周(7 * 24 = 168)少两个小时,因为我们要根据当前时间使用四个小时,因此我们需要将偏移量比整周少两个小时。 


两周

内的实际RPS(黄色)和预测的(蓝色)将实际RPS与我们的预测进行比较表明,我们的计算相当准确。但是,该方法具有缺点。
 
例如,5月1日,周三使用GitLab的时间比平常少,因为这一天是休息日。由于估计的增长趋势取决于前一周使用该系统的方式,因此我们对下周(即5月8日(星期三))的预测给出的RPS低于实际值。

可以通过在5月1日星期三之前连续三周进行三个预测(即前三个星期三)来纠正此错误。该请求保持不变,但偏移量已调整。

- record: job:http_requests:rate5m_prediction
  expr: >
   quantile(0.5,
     label_replace(
       avg_over_time(job:http_requests:rate5m[4h] offset 166h)
       + job:http_requests:rate5m:avg_over_time_1w — job:http_requests:rate5m:avg_over_time_1w offset 1w
       , "offset", "1w", "", "")
     or
     label_replace(
       avg_over_time(job:http_requests:rate5m[4h] offset 334h)
       + job:http_requests:rate5m:avg_over_time_1w — job:http_requests:rate5m:avg_over_time_1w offset 2w
       , "offset", "2w", "", "")
     or
     label_replace(
       avg_over_time(job:http_requests:rate5m[4h] offset 502h)
       + job:http_requests:rate5m:avg_over_time_1w — job:http_requests:rate5m:avg_over_time_1w offset 3w
       , "offset", "3w", "", "")
   )
   without (offset)


5月8日之前的三个星期,图表上有3个预测,而5月8日,星期三的实际RPS则为预测。您可以看到这两个预测非常准确,但是5月1日这一周的预测仍然不准确,

此外,我们不需要三个预测,我们需要一个预测。平均值不是一个选择,因为我们从5月1日起扭曲的RPS数据将使平均值模糊。相反,您需要计算中位数。 Prometheus没有中位数查询,但是我们可以使用分位数聚合而不是中位数。

唯一的问题是我们试图将三个系列包括在汇总中,而这三个系列实际上是三周内的同一系列。换句话说,它们具有相同的标签,因此很难将它们放在一起。 

为避免混淆,我们创建了一个名为offset的标签,并使用label-replace函数为三个星期中的每个星期添加一个offset。然后,在分位数聚合中,我们丢弃这些标签,这使我们得到三个平均值。

- record: job:http_requests:rate5m_prediction
  expr: >
   quantile(0.5,
     label_replace(
       avg_over_time(job:http_requests:rate5m[4h] offset 166h)
       + job:http_requests:rate5m:avg_over_time_1w — job:http_requests:rate5m:avg_over_time_1w offset 1w
       , "offset", "1w", "", "")
     or
     label_replace(
       avg_over_time(job:http_requests:rate5m[4h] offset 334h)
       + job:http_requests:rate5m:avg_over_time_1w — job:http_requests:rate5m:avg_over_time_1w offset 2w
       , "offset", "2w", "", "")
     or
     label_replace(
       avg_over_time(job:http_requests:rate5m[4h] offset 502h)
       + job:http_requests:rate5m:avg_over_time_1w — job:http_requests:rate5m:avg_over_time_1w offset 3w
       , "offset", "3w", "", "")
   )
   without (offset)

现在,我们对三个聚合的中位数的预测变得更加准确。


中位数预测与实际RPS

如何发现我们的预测确实准确


要检查预测的准确性,我们可以返回z得分。它用于测量样本的差异及其在标准偏差中的预测。与预测的标准偏差越大,则特定值离群的可能性越大。


预计的偏差范围是从+1.5到-1.5。

您可以将Grafana图表更改为使用季节性预测,而不是每周移动平均值。一天中特定时间的正常值范围以绿色阴影显示。超出绿色区域的所有内容均视为异常值。在这种情况下,离群值发生在周日下午,当时我们的云提供商遇到了一些网络问题。

优良作法是将±2 z分数用于季节性预测。

如何使用Prometheus设置警报


如果要为异常事件设置警报,则可以应用相当简单的Prometheus规则,该规则检查指标的z得分是否在+2到-2之间。

- alert: RequestRateOutsideNormalRange
  expr: >
   abs(
     (
       job:http_requests:rate5m - job:http_requests:rate5m_prediction
     ) / job:http_requests:rate5m:stddev_over_time_1w
   ) > 2
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: Requests for job {{ $labels.job }} are outside of expected operating parameters

在GitLab中,我们使用自定义路由规则,该规则会在检测到任何异常时通过Slack发送警报,但不会与我们的支持团队联系。

如何使用Prometheus在GitLab中检测异常


  1. 普罗米修斯可以用来检测一些异常。
  2. 正确的聚集是发现异常的关键。
  3. 如果您的数据具有正态分布,则Z评分有效。
  4. 统计季节性是检测异常的有力机制。

Mail.ru Cloud Solutions支持翻译

仍然有用

  1. GitLab CI中的简单缓存方法:图片指南
  2. MCS市场中现成的和定制的GitLab CE工具。 
  3. 我们关于数字转换的电报频道

All Articles