prometheus 指标数据缺失nodata的告警处理

无论哪种监控系统,都会基于采集的数据进行告警规则的配置。无论是传统监控系统的代表zabbix,还是云原生时代的监控利器prometheus,以及其他监控系统,数据缺失问题(即nodata)是都需要面对的问题

01 问题

首先解释一下本文提到的指标缺失场景,在使用过程中有两种情况:

  • 一种是在配置告警规则的时候,该指标数据就不是全量数据;例如配置宕机告警时,使用up==0作为告警规则,某一台主机由于各种原因,指标 up 的数据一直不存在。 这种带来的问题就是缺失的指标永远不会告警
  • 一种则是在告警规配置的时候,指标数据是完整的,当在告警生效一段时间以后,且发出异常告警之后,出现指标数据缺失的问题。例如配置 mem_used_percent > 90(内存使用率超过90%告警),某台设备已经超限并且触发了告警规则,此时服务器由于网络原因,没能上报数据,导致数据缺失。 这种带来的问题就是可能出现假恢复的情况。

02 解决方法

zabbix系统中一般采用nodata触发器,当监控项出现nodata,通过设置触发器来触发报警或执行其他操作。open-falcon中则是有对指定指标进行赋值,即在出现数据终端时填充配置的值,一般配置-1,即配置一个该指标正常情况下不可能出现的数据。

在prometheus中目前还没有提供这种功能,因此我们只能从告警规则入手,希望通过告警规则的一些额外的配置,尽可能达到解决nodata的问题;或者进行其他一些告警后处理的工作;特殊场景下的一些处理。以下将从笔者的生产角度来描述是如何解决这类问题的。

1) 从规则入手

从规则入手解决nodata的一个核心问题就是如何获取全量数据,所谓全量数据就是能够覆盖nodata的数据,即告警规则中必定包含一个全量的指标,这个指标一般不会缺失。

这种情况下用到的主要prometheus的unless方法。

vector1 unless vector2

会产生一个新的向量,新向量中的元素由vector1中没有与vector2匹配的元素组成。

在创建规则的时候vector1一般表示全量指标,及一般不会有数据缺时的情况,例如对于服务器宕机告警,我们结合CMDB创建一个全量的指标,nodata_up。

nodata_up{ip="192.168.1.1"} 0
nodata_up{ip="192.168.1.2"} 0
nodata_up{ip="192.168.1.3"} 0

当up查询的时候返回的结果为

up{ip="192.168.1.1"} 1
up{ip="192.168.1.2"} 1

即192.168.1.3目前没有数据,则通过

nodata_up unless on(ip) up or up !=1

即可以获得数据缺失的节点,这样可以达到当节点宕机时进行正常,当节点192.168.1.3没有数据时也可以触发告警。

nodata_up{ip="192.168.1.3"} 0

2) 告警后处理

如果有一定开发能力或者会简单脚本处理的,可以看看这一部分,这里主要是用于告警异常已经发生以后,由于数据缺失造成假恢复的情况,例如我们对服务器上内存使用率进行监控告警,并用指标sys_mem_used_percent表示内存使用率,并配置了如下的告警规则,当内存使用率高于90%时告警。

sys_mem_used_percent{ip="192.168.1.3"} > 90

当触发告警规则并告警以后,在中间的某一个时间段内如果出现sys_mem_used_percent指标没有数据,prometheus规则会认为告警已经恢复,因此会出现告警假恢复的情况。

prometheus的源码中是这样的:

func (r *AlertingRule) Eval(ctx context.Context, ts time.Time, query QueryFunc, externalURL *url.URL, limit int) (promql.Vector, error) {
  res, err := query(ctx, r.vector.String(), ts){
  ...
  for fp, a := range r.active {
    if _, ok := resultFPs[fp]; !ok {
      // If the alert was previously firing, keep it around for a given
      // retention time so it is reported as resolved to the AlertManager.
      if a.State == StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > resolvedRetention) {
        delete(r.active, fp)
      }
      // 处理已经处于告警状态的告警转为恢复的状态
      if a.State != StateInactive {
        a.State = StateInactive
        a.ResolvedAt = ts
      }
      continue
    }
    numActivePending++


    if a.State == StatePending && ts.Sub(a.ActiveAt) >= r.holdDuration {
      a.State = StateFiring
      a.FiredAt = ts
    }


    if r.restored.Load() {
      vec = append(vec, r.sample(a, ts))
      vec = append(vec, r.forStateSample(a, ts, float64(a.ActiveAt.Unix())))
    }
  }
  ...
  }

r.ative表示前一次的告警,resultFPs为最新的扫描的异常数据,由源码可清晰可见其处理的逻辑与实际表现是一致的。

我们的对待这个问题的一个处理逻辑是更改源码相关逻辑,但是这显然不是最佳选择。因此我们选择了另一种处理逻辑来处理这种假恢复的情况。

具体逻辑为,当出现恢复时,我们根据触发的规则从规则中提取出需要的指标,然后结合告警的标签,重新构造一维的查询表达式,然后去查询prometheus,判断是否存在数据,如果存在则为正常恢复,否则即为数据缺失造成的假恢复

实现的大致代码如下:

func (a *Arbitrator) nodata(alert promRule.Alert, du time.Duration) bool {
  metrics, er := lib.ExtractVectorsForGraph(a.Expr)
  if er != nil {
    _ = level.Warn(g.Logger).Log("module", "judge", "msg", er.Error())
    return false
  }
  excludeKeys := map[string]struct{}{
    "alertname": {},
    "__name__":  {},
  }
  for k := range a.ExtLabels {
    excludeKeys[k] = struct{}{}
  }
  for _, metric := range metrics {
    var matches []*labels.Matcher
    for k, v := range alert.Labels.Map() {
      if _, ok := excludeKeys[k]; !ok {
        matches = append(matches, &labels.Matcher{
          Name: k, Type: labels.MatchEqual, Value: v,
        })
      }
    }
    expr, er := lib.AddExprTags(metric, matches)
    if er != nil {
      _ = level.Warn(g.Logger).Log("module", "judge", "msg", er.Error())
      return false
    }
    if exist, _ := hasLatestData(expr); er == nil && !exist {
      _ = level.Warn(g.Logger).Log("module", "judge", "msg", "nodata", "detail", expr)
      return true
    }
  }
  return false
}

在告警环节加入代码判断

if alert.State == promRule.StateInactive && !alert.ResolvedAt.IsZero() && time.Now().Sub(alert.ResolvedAt) > du {
  if a.nodata(alert, du) {
      return
    }
  }

本段仅为二次开发的核心代码,通过对告警消息的二次判断,去除nodata的假恢复情况。

3) 其他方法

这里主要是利用promethues的absent函数实现,


absent(v instant-vector)

如果传递给它的向量参数具有样本数据,则返回空向量;如果传递的向量参数没有样本数据,则返回不带度量指标名称且带有标签的时间序列,且样本值为1。 使用 absent 方法对告警中处理nodata的情况也是非常有用的。

对于某个确定的指标,如果确定应该有且仅有一组数据的时候,使用absent进行nodata告警。例如,如下配置可以实现对192.168.1.3在无数据时进行告警。

absent(up{ip="192.168.1.3"})

03 总结

通过prometheus本身的unless, absent 方法实现nodata问题的处理,unless的核心在于全量数据的确认。另外通过二开实现,主要对已经出现了告警,后期由于缺失数据造成的假恢复的情形的处置。当然了,对于nodata的处理方式可能还有一些好的方法,本文旨在为读者提供几种一般的处理方法,为解决nodata问题提供一些思路。

如果大家对运维技术有兴趣,欢迎入群交流,扫码添加好友,邀你入群!

举报
评论 0