无论哪种监控系统,都会基于采集的数据进行告警规则的配置。无论是传统监控系统的代表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问题提供一些思路。