0. 概述

在日常的工作中,我经常被问到的一个问题是我的系统指标明明已经达到报警条件了,为什么没有触发报警?还有,为什么这个报警的触发时间会比系统的真实状态差这么多?本文就来探讨一下这个问题。

1. 数据采集,计算和报警

当你看这篇文章的时候,我想你应该是知道 Prometheus 使用的是 Go 程序语言,不知道也没关系。如果你知道的话,那么我先告诉你,在 Prometheus 内部,例如各种数据的采集和判断一些监控数据是否符合你设置的报警规则,以及将这些产生出来的报警发送出去的功能都是通过不同的 goroutine 来完成的;如果你不知道 Go 语言,那么可以理解为是通过类似于其他语言中多线程(协程)的形式实现的。

为什么要说这个?因为这在很多时候都是造成延迟的关键所在,在定位问题的时候你需要时刻记住这个机制,这样才能发现真正的问题所在。

在 Prometheus 内部,数据的采集都是通过按照固定的周期:scrape_interval 进行的,这个周期在全局默认是 1m(一分钟),如果你针对特定的 exporter(一个应用用于暴露指标的 endpoint)没有特别指定采集周期,那么就是使用的全局默认的周期;Prometheus 允许你在配置文件中修改全局的默认采集周期,同时,你也可以针对单个 exporter 设置特定的采集周期,这个时候这个采集周期就会作为这个 exporter 的采集周期而被采用。

当数据被采集回来之后,就存放在 Prometheus 的数据存储(部分缓存在内存)中。当你定义了一些报警规则(Alerting Rule)之后,Prometheus 会运行另外的一些 goroutine 来定时判断你的报警规则是否得到满足,报警规则条件成立,那么就会触发出报警(Alert)。这里 Prometheus 判断报警规则也是周期进行的,在 Prometheus 的全局设置中有 evaluation_interval 用于配置,默认也是 1m,同样的,也是可以自定义修改,并且全局生效。和采集一样,你也可以针对每个不同组(不是单个报警规则)的报警规则设置不同的判断间隔,从而达到不同的报警敏感度。

当报警条件满足触发报警之后,有时并不是马上就发给定义的报警管理器,而是根据你报警规则的设置有所不同。在 Prometheus 内部,一个报警(Alert)存在 3 种状态,分别为:

这里的 pending 状态是说报警规则已经得到满足了,但是持续时间还不够,这里的持续时间是通过配置文件中的 for 来设置;如果没有设置,那么就不存在 pending 状态,而是报警规则一得到满足就会发送报警;如果设置了这个字段,那么就会进入 pending 状态,并且在持续时间足够的时候,才会发送出去。这里之所以出现一个 pending 状态是为了减少一些误报警(然而似乎有点多余),但是,这也是你在定位报警延迟的时候需要非常关心的一个点。

2. 报警的生命周期

在第一段中我已经向你介绍了一个报警从数据采集已经报警规则执行到发送报警。那么在这一节我就以一个真实的实例配置来介绍一下可能存在的延迟问题。

首先先来看下我这里有一个采集的 exporter 设置:

[[email protected]]# cat prometheus.yml
global:
  scrape_interval: 1m
  scrape_timeout: 10s
  evaluation_interval: 1m
- job_name: prometheus
  static_configs:
  - targets:
    - 127.0.0.1:9090

这里我添加了一个 Prometheus 监控自己的配置项,因为这里我没有针对 exporter 设置 scrape_interval,所以它使用的是默认的采集周期,是一分钟采集一次。然后我再根据这个 exporter 添加一条报警规则:

[[email protected]]# cat alerting_rules.yml
groups:
- name: exporter_endpoint
  interval: 30s
  rules:
  - alert: exporter_endpoint_down
    expr: up == 0
    for: 2m
    labels:
      severity: critical
    annotations:
      summary: Help!

这里定义了一个 exporter 的报警规则,如果 exporter 的保持不健康超过 5 分钟之后,那么就触发报警。这里还有一个点就是,我这里为这个报警规则组设置了一个时间间隔:30s,那么就意味着我覆盖了前面默认判断周期:evaluation_interval: 1m。在这个例子中如果发现了一个报警的话,那么可能会出现的一个时间轴为:

图 1:报警的时间轴

可以看到,从采集到异常数据到报警发出,这里经历的时间总共为:

2m(报警持续时间)+ (0-30s的执行周期)

这里还只是 Prometheus 的视角,其实还有两个视角,那就是 exporter 应用内部以及报警管理器(Alert Manager)。这里先来看 exporter 内部,可能在上一个采集周期之后就异常了,那么这里就可能存在(0-1m)的采集周期的延迟响应。

3. Alert Manager

下面就来说下最后一个关键组件:报警管理器(Alert Manager)。和很多报警系统不同,Alert Manager 支持一些聚合规则,出现这个的原因是在一些情况,例如系统网络故障或者系统初始化的时候,可能会出发一大堆的报警,如果将所有的报警都推送出去,很可能并不能帮助我们解决问题,而且还会分散我们的注意力。所以,对于同类的报警,可以将报警组合一下,提示一次就好了。

这个就是 Alert Manager 的 group 功能,在 group 功能中,关于时间差的参数有两个:

这里额外提一个需要区分的配置是:repeat_interval

区别在于 repeat_interval 前后发送的报警签名是相同的;而 group_interval 前后发送的报警签名是不同的。

所以,如果报警已经从 Prometheus 送到 Alert Manager 了,那么最多可能等待 max(group_waitgroup_interval)的时间。

4. 总结

从前面的解析中可以知道,因为 Prometheus 异步的设计方式以及分组件的运行方式,在获得灵活性的前提下失去了一些实时性,从系统真正出现异常到用户收到报警通知之间的延迟可能会比较大,所以需要根据自己的实际情况来选择。

这里需要提醒一下的是,报警的灵敏度高同时也伴随着误报率的提升,所以在设计的时候需要多加考量和尝试;这里我推荐一下,在 Promehteus 2.5 版本中加入的 Alerting Rule 单元测试框架,非常适合一些不确定的场景:Unit Testing for Rule

5. Ref