本文共 9370 字,大约阅读时间需要 31 分钟。
警报是监控系统中必不可少的一块, 当然了, 也是最难搞的一块. 我们乍一想, 警报似乎很简单一件事:
假如发生了异常情况, 发送或邮件/消息通知给某人或某频道
一把梭搞起来之后, 就不免有一些小麻烦:
到最后我们还能总结出一个奇怪的规律:
这世界上只有两种警报,一种是疯狂报警但是没有卵用完全没人看的警报,一种是非常有效大家都想看但在用户反馈前从来都报不出来的警报。—— 鲁迅
玩笑归玩笑,但至少我们能看出,警报不是一个简单的计算+通知系统。只是,”做好警报”这件事本身是个综合问题,代码能解决的也只是其中的一小部分,更多的事情要在组织、人事和管理上去做。我掰不出那么有深度的文章,这篇文章就专注一点,只讲代码部分里的通知,也就是 Prometheus 生态中的 Alertmanager 这个组件。
我们先介绍一点背景知识,Prometheus 生态中的警报是在 Prometheus Server 中计算警报规则(Alert Rule)并产生的,而所谓计算警报规则,其实就是周期性地执行一段 PromQL,得到的查询结果就是警报,比如:
node_load5 > 20
这个 PromQL 会查出所有”在最近一次采样中,5分钟平均 Load 大于 20”的时间序列。这些序列带上它们的标签就被转化为警报。
只是,当 Prometheus Server 计算出一些警报后,它自己并没有能力将这些警报通知出去,只能将警报推给 Alertmanager,由 Alertmanager 进行发送。
这个切分,一方面是出于单一职责的考虑,让 Prometheus “do one thing and do it well”, 另一方面则是因为警报发送确实不是一件”简单”的事,需要一个专门的系统来做好它。可以这么说,Alertmanager 的目标不是简单地”发出警报”,而是”发出高质量的警报”。它提供的高级功能包括但不限于:
假如你很忙,那么读到这里就完全 OK 了,反正这类文章最大的作用就是让我们”知道有 X 这回事,大概了解有啥特性,当有需求匹配时,能想到试试看 X 合不合适“,其中 X = Alertmanager。当然,假如你是个好奇宝宝,那么还可以看看下面的解析。
先看官方文档中的架构图:
下面就分开讲一讲核心的两块:
Routing Tree 的是一颗多叉树,节点的数据结构定义如下:
// 节点包含警报的路由逻辑type Route struct { // 父节点 parent *Route // 节点的配置,下文详解 RouteOpts RouteOpts // Matchers 是一组匹配规则,用于判断 Alert 与当前节点是否匹配 Matchers types.Matchers // 假如为 true, 那么 Alert 在匹配到一个节点后,还会继续往下匹配 Continue bool // 子节点 Routes []*Route}
具体的处理代码很简单,深度优先搜索:警报从 root 开始匹配(root 默认匹配所有警报),然后根据节点中定义的 Matchers 检测警报与节点是否匹配,匹配则继续往下搜索,默认情况下第一个”最深”的 match (也就是 DFS 回溯之前的最后一个节点)会被返回。特殊情况就是节点配置了 Continue=true,这时假如这个节点匹配上了,那不会立即返回,而是继续搜索,用于支持警报发送给多方这种场景(比如”抄送”)
# 深度优先搜索func (r *Route) Match(lset model.LabelSet) []*Route { if !r.Matchers.Match(lset) { return nil } var all []*Route for _, cr := range r.Routes { // 递归调用子节点的 Match 方法 matches := cr.Match(lset) all = append(all, matches...) if matches != nil && !cr.Continue { break } } // 假如没有任何节点匹配上,那就匹配根节点 if len(all) ==0 { all = append(all, r) } return all}
为什么要设计一个复杂的 Routing Tree 逻辑呢?我们看看 Prometheus 官方的配置例子:为了简化编写,Alertmanager 的设计是根节点的所有参数都会被子节点继承(除非子节点重写了这个参数)
route: # 根节点的警报会发送给默认的接收组 # 该节点中的警报会按’cluster’和’alertname’做 Group,每个分组中最多每5分钟发送一条警报,同样的警报最多4小时发送一次 receiver:’default-receiver’ group_wait: 30s group_interval: 5m repeat_interval: 4h group_by: [cluster, alertname] # 没有匹配到子节点的警报,会默认匹配到根节点上 # 接下来是子节点的配置: routes: # 所有 service 字段为 mysql 或 cassandra 的警报,会发送到’database-pager’这个接收组 # 由于继承逻辑,这个节点中的警报仍然是按’cluster’和’alertname’做 Group 的 - receiver:’database-pager’ group_wait: 10s match_re: service: mysql|cassandra # 所有 team 字段为 fronted 的警报,会发送到’frontend-pager’这个接收组 # 很重要的一点是,这个组中的警报是按’product’和’environment’做分组的,因为’frontend’面向用户,更关心哪个’产品’的什么’环境’出问题了 - receiver:’frontend-pager’ group_by: [product, environment] match: team: frontend
总结一下,Routing Tree 的设计意图是让用户能够非常自由地给警报归类,然后根据归类后的类别来配置要发送给谁以及怎么发送:
group_interval 和 repeat_interval 的区别会在下文中详述
由 Routing Tree 分组后的警报会触发 Notification Pipeline:
每次触发 Notification Pipeline,AlertGroup 都会将组内所有的 Alert 作为一个列表传进 Pipeline, Notification Pipeline 本身是一个按照责任链模式设计的接口,MultiStage 这个实现会链式执行所有的 Stage:
// A Stage processes alerts under the constraints of the given context.type Stage interface { Exec(ctx context.Context, l log.Logger, alerts …*types.Alert) (context.Context, []*types.Alert, error)}// A MultiStage executes a series of stages sequencially.type MultiStage []Stage// Exec implements the Stage interface.func (ms MultiStage) Exec(ctx context.Context, l log.Logger, alerts …*types.Alert) (context.Context, []*types.Alert, error) { var err error for _, s := range ms { if len(alerts) ==0{ return ctx, nil, nil } ctx, alerts, err = s.Exec(ctx, l, alerts…) if err != nil { return ctx, nil, err } } return ctx, alerts, nil}
MultiStage 里塞的就是开头架构图里画的 InhibitStage、SilenceStage…这么一条链式处理的流程,这里要提一下,官方的架构图画错了,RoutingStage 其实处在整个 Pipeline 的首位,不过这个顺序并不影响逻辑。要重点说的是DedupStage和NotifySetStage它俩协同负责去重工作,具体做法是:
最近又被问到了 Prometheus 为啥不报警,恰好回忆起之前经常解答相关问题,不妨写一篇文章来解决下面两个问题:
我们首先需要一些背景知识:Prometheus 是如何计算并产生警报的?
看一条简单的警报规则:
- alert: KubeAPILatencyHigh annotations: message: The API server has a 99th percentile latency of { { $value }} seconds for { { $labels.verb }} { { $labels.resource }}. expr: | cluster_quantile:apiserver_request_latencies:histogram_quantile{job="apiserver",quantile="0.99",subresource!="log"} > 4 for: 10m labels: severity: critical
这条警报的大致含义是,假如 kube-apiserver 的 P99 响应时间大于 4 秒,并持续 10 分钟以上,就产生报警。
首先要注意的是由 for 指定的 Pending Duration。这个参数主要用于降噪,很多类似响应时间这样的指标都是有抖动的,通过指定 Pending Duration,我们可以 过滤掉这些瞬时抖动,让 on-call 人员能够把注意力放在真正有持续影响的问题上。
那么显然,下面这样的状况是不会触发这条警报规则的,因为虽然指标已经达到了警报阈值,但持续时间并不够长:
但偶尔我们也会碰到更奇怪的事情。
类似上面这样持续超出阈值的场景,为什么有时候会不报警呢?
类似上面这样并未持续超出阈值的场景,为什么有时又会报警呢?
这其实都源自于 Prometheus 的数据存储方式与计算方式。
首先,Prometheus 按照配置的抓取间隔(scrape_interval)定时抓取指标数据,因此存储的是形如 (timestamp, value) 这样的采样点。
对于警报, Prometheus 会按固定的时间间隔重复计算每条警报规则,因此警报规则计算得到的只是稀疏的采样点,而警报持续时间是否大于 for 指定的 Pending Duration 则是由这些稀疏的采样点决定的。
而在 Grafana 渲染图表时,Grafana 发送给 Prometheus 的是一个 Range Query,其执行机制是从时间区间的起始点开始,每隔一定的时间点(由 Range Query 的 step请求参数决定) 进行一次计算采样。
这些结合在一起,就会导致警报规则计算时“看到的内容”和我们在 Grafana 图表上观察到的内容不一致,比如下面这张示意图:
上面图中,圆点代表原始采样点:
由于采样是稀疏的,部分采样点会出现被跳过的状况,而当 Grafana 渲染图表时,取决于 Range Query 中采样点的分布,图表则有可能捕捉到 被警报规则忽略掉的”低谷“(图三)或者也可能无法捕捉到警报规则碰到的”低谷“(图二)。如此这般,我们就被”图表“给蒙骗过去,质疑起警报来了。
首先嘛, Prometheus 作为一个指标系统天生就不是精确的——由于指标本身就是稀疏采样的,事实上所有的图表和警报都是”估算”,我们也就不必 太纠结于图表和警报的对应性,能够帮助我们发现问题解决问题就是一个好监控系统。当然,有时候我们也得证明这个警报确实没问题,那可以看一眼 ALERTS 指标。ALERTS 是 Prometheus 在警报计算过程中维护的内建指标,它记录每个警报从 Pending 到 Firing 的整个历史过程,拉出来一看也就清楚了。
但有时候 ALERTS 的说服力可能还不够,因为它本身并没有记录每次计算出来的值到底是啥,而在我们回头去考证警报时,又无法选取出和警报计算过程中一模一样的计算时间点, 因此也就无法还原警报计算时看到的计算值究竟是啥。这时候终极解决方案就是把警报所要计算的指标定义成一条 Recording Rule,计算出一个新指标来记录计算值,然后针对这个 新指标做阈值报警。kube-prometheus 的警报规则中就大量采用了这种技术。
Prometheus 警报不仅包含 Prometheus 本身,还包含用于警报治理的 Alertmanager,我们可以看一看上面那张指标计算示意图的全图:
在警报产生后,还要经过 Alertmanager 的分组、抑制处理、静默处理、去重处理和降噪处理最后再发送给接收者。而这个过程也有大量的因素可能会导致警报产生了却最终没有进行通知。
Alertmanager 整体的设计意图就是奔着治理警报(通知)去的,首先它用 Routing Tree 来帮助用户定义警报的归类与发送逻辑,然后再用 Notification Pipeline 来做抑制、静默、去重以提升警报质量。这些功能虽然不能解决”警报”这件事中所有令人头疼的问题,但确实为我们着手去解决”警报质量”相关问题提供了趁手的工具。
本文转载自:「Aylei's Blog」,原文:1.https://url.cn/5Gp0VLq、2.https://url.cn/5MEMY8K,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com 。