Prometheus 与 nodata 告警

1,494 阅读9分钟

背景

随着云原生和高动态服务端的发展,在运维领域,以 Prometheus 为代表的现代时间序列存储正在加速替代以 Zabbix 为代表的传统监控系统。运维领域在享受时间序列技术发展红利的同时,也面临时间序列管理思路上的转变和监控系统实际应用的上一些难点 —— nodata 告警便是其中之一。nodata 告警是传统监控系统的必备功能,但却缺席了几乎所有现代时间序列存储实践,这给运维监控带了诸多缺陷。本文尝试分析其中原因,并给出一些可能的解决方法。

nodata 告警触发器的特殊性与必要性

nodata 告警触发器(Trigger)与普通告警触发器相比具有原生的特殊性。普通告警触发器的作用是对一组监控指标(Metric)的过滤,通常是基于数值大小的过滤。                                              
如果存在如下表示的监控数值集合 _M_

告警触发器 ![](https://juejin.cn/equation?tex= T_ {gt100})  可对集合 _M_中的元素做『大于 100』的过滤

普通告警触发器触发的告警集合 _A _为

某个普通告警触发器作用于某组监控数值后,产生普通告警集合的过程如上文所述。nodata 告警触发器的工作需要引入额外的全集 _U_

nodata 告警触发器  ![](https://juejin.cn/equation?tex= T_ {nodata})对集合 _M_ 求绝对补集

nodata 告警触发器触发的告警集合 ![](https://juejin.cn/equation?tex= A_ {nodata})

一般来说,运维监控场景下我们希望得到的完整告警集合  ![](https://juejin.cn/equation?tex= A_ {all}) 

从上文看出,nodata 告警触发器与普通告警触发器最大的区别是前者需要引入『全集』_U _ ,全集应当从监控系统之外获得,以保证监控系统本身的有效性。                                                         
运维监控场景下,发生 nodata 告警最大的可能性是监控系统本身的失效,比如采集点失效或采集对象失效,在我们的实践中,服务器意外下线、磁盘故障、服务崩溃等都会导致 nodata 告警;另外还有一类监控指标,这类指标以 nodata 为『正常状态』,如 5xx code 产生的速率,在没有 5xx code 产生时,虽然我们希望指标的数值为 0 (而不是 nodata) ,但在实践中往往很难保证,对于这类指标有效性的保证,我们会在其他文章中详细说明。                                      
nodata 告警触发器的难点之一在于全集 _U _的获取。在高动态的服务端环境中,往往很难得到『全部服务器集合』、『全部 IP 地址集合』、『全部 Pod 集合』、『某服务全部运行实例集合』这样的全集。所以,在数值型的监控采集之外,必须建设更加结构化的信息组织方式,并配以自动、半自动与人工相结合的信息维护方法。假如在结构化信息中很难方便准确地获取『全部某某集合』这样的信息,就无法制作真正有效地 nodata 告警触发器。                               
nodata 告警触发器的另一个难点是计算的开销大。普通告警触发器对指标的数值过滤,可以通过『带条件的查询』做到,这本质上是将告警计算的开销一次性卸载到时间序列存储系统中,而现代的时间序列存储系统一般都支持这样做。由上文对 nodata 告警触发器的定义可以得到,nodata 的计算必须在数值过滤之前,也就是说 nodata 告警计算的计算对象是全量的监控指标,对全量监控指标求补集本身是一个开销巨大的计算。另外全集 _U _并不存在于时间序列存储中(否则 nodata 告警就失去了客观性),把全集 _U_ 带入 nodata 告警计算可会给时间序列存储带来额外的传输与计算压力。                                                                                                                          
虽然有诸多困难,但 nodata 告警的重要性不言而喻。如果没有 nodata 告警,监控指标的失效是静默的,监控系统本身的有效性无法得到保证。对于云原生的服务端环境,监控对象的动态化程度更高,虽然可以制作更加宏观的监控指标(如某类 Pod 的总实例数),但 nodata 告警可以帮助我们获悉更加微观的服务端运行工况。                                                                                   
在 OpsMind 的实践中,我们使用 CMDB 和经典的 CMDB 方法来获得全集 _U_,并改造 Prometheus ,将 nodata 计算卸载到存储层。下文结合我们的实践,并尽可能剥离我们特殊的业务场景,以 Prometheus 为例,介绍几个相对通用的 nodata 告警触发器的实现思路。

单一维度的 nodata

单一维度的 nodata 是最常见的 nodata 告警触发器,Zabbix 等传统监控系统提供的也是这类 nodata 功能。以服务器 Load 监控为例                                                                                              
存在服务器集合 _H_

存在监控点 _load_

便可以得到 Load 监控指标 _L_

当监控指标 _L_中存在失效的监控点时,_L _变为不完整的指标 _L'_

如果集合 _H_ 在时间序列存储之外(例如,存储于 CMDB 中),就可以将 _H_ 认定为 nodata 的全集 _U_ ,而 nodata 的告警集合  ![](https://juejin.cn/equation?tex= A_ {nodata}) _L' _的绝对补集

在我们的实践中,为了将 nodata 的补集运算卸载到 Prometheus,我们将 CMDB 作为一个监控点,由 Prometheus 向 CMDB 拉取全集 _H_,具体的指标类似于
1 nodata_hosts{host="h1", nodata="True"} 1
2 nodata_hosts{host="h2", nodata="True"} 1
3 nodata_hosts{host="h3", nodata="True"} 1
4 ...
同时,假设 Load 监控指标 _L_ 类似于
1 host_cpu_load5{host="h1"} 42
2 host_cpu_load5{host="h2"} 43
3 host_cpu_load5{host="h3"} 44
4 ...
我们针对 _L_ 生成如下告警触发器
1 host_cpu_load5{host=~"h.*"} > 42
则卸载 nodata 之后的运算可表示为
1 host_cpu_load5{host=~"h.*"} or on(host) nodata_hosts{host=~"h.*"} * 1/0 > 42
此告警触发器可以生成如下的告警信息
1 host_cpu_load5{host="h2"} 43
2 host_cpu_load5{host="h3"} 44
3 host_cpu_load5{host="hx", nodata="True"} +inf
这里有如下几个关键点
  1. 将 CMDB 中的结构化信息转储到 Prometheus
  2. 使用 or 运算符做补集运算
  3. on( ) 的 label 为 nodata 的单一维度
  4. 普通指标与 nodata 指标在 nodata 维度上的查询条件一致
  5. or 之后的表达式通过 * 1/0 转为 +inf 以保证数值条件成立
我们通过将全集 _H_ 转为监控指标,并通过 or 运算符做补集运算实现了单一维度的 nodata 告警触发器。

多维度正交的 nodata

多维度正交 nodata 也是运维监控场景中的常见需求。假设有 n 台服务,每台服务器上都运行相同的一组 m 个服务实例,那么对于服务的监控指标,就需要在服务器和服务两个维度上做 nodata 计算。问题描述如下

服务器集合

服务实例集合 

监控点 _qps _用来获取服务实例的每秒访问次数

得到 Qps 监控指标 _Q_

当监控指标 _Q_ 存在监控点失效时,_Q_ 变为不完整的监控指标 _Q'_

与单维度类似,我们转储服务的全集指标
1 nodata_services{service="s1", nodata="True"} 1
2 nodata_services{service="s2", nodata="True"} 1
3 nodata_services{service="s3", nodata="True"} 1
4 ...
假设监控指标 _**Q **_类似于
1 service_qps{host="h1", service="s1"} 42
2 service_qps{host="h1", service="s2"} 43
3 service_qps{host="h2", service="s1"} 44
4 ...
则对于监控指标 _**Q**_ 的一个告警触发器类似于
1 service_qps{host=~"h.*", service=~"s.*"} > 42
卸载 nodata 计算之后的告警触发器
1 service_qps{host=~"h.*", service=~"s.*"} or on(host, service)
  absent(nodata_hosts{host=~"h.*"}, nodata_services{service=~"s.*"}) * 1/0 > 42
此告警触发器可以生成如下告警信息
1 service_qps{host="h1", service="s2"} 43
2 service_qps{host="h2", service="s1"} 44
3 service_qps{host="hx", service="s1", nodata="True"} +inf
4 service_qps{host="h1", service="sx", nodata="True"} +inf
5 ...
除了与单一维度 nodata 类似的关键点之外,这里还有如下几个关键点:
  1. 重写 Prometheus 的 absent 函数支持多 vector 的正交计算
  2. on( ) 的 label 为多个 nodata 的计算维度
我们通过与单一维度 nodata 类似的手法实现了支持多维度正交的 nodata 告警触发器。

更一般的多维度 nodata

多维度 nodata 更一般的表述是多维度之间无法形成正交关系的情况。这些情况较难处理,需要 CMDB 与时间序列存储建立较密切的联系(而非简单的数据转储),但如果可以处理得当,可以大大增强监控系统的能力。
假设有服务器集合

服务器  ![](https://juejin.cn/equation?tex= h_ {i}) 上的服务实例集合

监控指标 _Q_

针对这个监控指标的一个告警触发器
1 service_qps{host=-"h1,h2,h3"service=-"s1,s2"} > 42
注意这里我们扩展了 PromQL 的语法,支持『列表匹配』=-,关于这个语法带来的功能和性能的优化本文暂不赘述。                                                                                                                         
为了对 _**Q**_ 具有这类复杂多维度关系的指标,我们需要在 CMDB 中建立服务器与服务实例的关系表

通过 CMDB 中的关系表,将 nodata 卸载后的告警触发器为
1 service_qps{host=-"h1[0,1],h2[2],h3[3]"service=-"s1[0,2,3],s2[1,3]"} > 42
这里我们扩展了列表匹配的语法,支持 "nodata key" [0,1],表示某个列表项在全集中的 ID。Promethues 查询时会针对每个 label 计录缺失列表项的 nodata key,并将多个 label 记录下来的 nodata key 求并集。                                                                                                                        
对本例来说,假设 h1 上的 s2 和 h2 上的 s1 监控失效,则 host label 缺失的列表项为 h2(并不包含 h1,因为 h1 下的 s1 有数值),对应的 nodata key 为

service label 上缺失的列表项为 s2(并不包含 s1,因为 h1 下的 s1 有数值),对应的 nodata key 为

并集的结果为

这表示全集中 ID 为 1、2  的条目在时间序列中没有数值,需要做 nodata 告警,与我们的假设相符。

结语

本文分析了现代时间序列管理方案中 nodata 的特殊性与必要性,并以 Prometheus 为例尝试给出几个解决方案。可以看到,现代时间序列管理方案中的 nodata 处理是十分复杂的,我们认为这和云原生环境下其他的监控难题一样具有原生复杂性,这只是云原生给运维带来的诸多根本性挑战的外在表现形式,这些挑战需要系统性地分析和解决。OpsMind 为应对云原生的挑战做了大量的技术研判和产品包装,我们将在其他文章中与大家陆续分享。