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

561 阅读6分钟

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

01 问题

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

  • 一种是在配置告警规则的时候,该指标数据就不是全量数据;例如配置宕机告警时,使用up==0作为告警规则,某一台主机由于各种原因,指标 up 的数据一直不存在。

    这种带来的问题就是缺失的指标永远不会告警。

  • 一种则是在告警规配置的时候,指标数据是完整的,当在告警生效一段时间以后,且发出异常告警之后,出现指标数据缺失的问题。例如配置 mem_used_percent > 90(内存使用率超过90%告警),某台设备已经超限并且触发了告警规则,此时服务器由于网络原因,没能上报数据,导致数据缺失。

    这种带来的问题就是可能出现假恢复的情况。

02 解决方法

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

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

从规则入手

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

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

unless 用法:

vector1 unless vector2

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

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

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

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

up{ip="192.168.1.1"} 1up{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

告警后处理

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

sys_mem_used_percent{ip="192.168.1.3"} > 90

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

promethues的源码中是这样的:

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的假恢复情况。

其他方法

这里主要是利用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问题提供一些思路。