深入剖析Alertmanager:解锁告警管理的核心逻辑

开发 前端
Alertmanager作为监控体系中的关键组件,在告警处理方面展现出了强大的功能和高效的实现逻辑。通过对告警去重、分组、路由和抑制等核心功能的深入分析,以及对其源码中关键数据结构和处理流程的解读,我们清晰地认识到它如何在复杂的监控环境中,将海量的告警信息进行有序管理和精准分发。

一、引言

在当今复杂的IT系统架构中,监控体系对于保障系统的稳定运行至关重要。而Alertmanager作为监控体系里关键的一环,在处理告警信息、确保相关人员及时响应等方面发挥着无可替代的作用。它就像是一个信息枢纽,接收来自各个监控源的告警信息,经过一系列智能处理后,精准地将关键信息传递给相关人员。

接下来,让我们深入探索Alertmanager的实现逻辑,通过源码分析,一窥其背后的运行机制。这不仅能帮助我们更好地理解和运用Alertmanager,还能为优化监控体系提供有力的支持。

二、Alertmanager初相识

(一)功能概览

Alertmanager具备一系列强大的功能,旨在高效处理和管理告警信息。

告警去重:在复杂的监控环境中,同一问题可能会产生多个重复的告警,这不仅会干扰运维人员的判断,还可能导致重要信息被淹没。Alertmanager通过独特的算法,比较告警的标签、内容等关键信息,精准识别并去除重复的告警。例如,当某个服务器的CPU使用率持续过高,多个监控指标可能会同时触发告警,但Alertmanager能够将这些重复告警合并为一个,确保运维人员只收到一次通知 ,有效减少了告警噪音,让运维人员能够专注于真正的问题。

告警分组:将相似的告警进行分组,是Alertmanager的又一核心功能。通过合理的分组策略,能够将大量分散的告警信息整理成有序的集合,提高告警的可读性和管理效率。比如,在一个大型电商系统中,可能会有多个与订单处理相关的服务出现故障,如订单创建失败、订单支付异常等告警。Alertmanager可以根据预先设定的规则,将这些与订单处理相关的告警归为一组,以单一通知的形式发送给负责订单业务的运维团队。这样,运维人员可以一次性了解到订单业务相关的所有问题,快速定位故障范围,提高故障处理的效率。

告警路由:Alertmanager支持根据告警的标签、内容等属性,将告警精准地路由到不同的接收器,如电子邮件、Slack、PagerDuty等。这一功能使得告警能够及时送达至最合适的人员或团队手中,确保问题得到及时处理。例如,对于与网络相关的告警,可以配置Alertmanager将其发送给网络运维团队的Slack群组;而对于与数据库相关的告警,则发送到数据库管理员的邮箱。通过灵活的路由配置,实现了告警通知的个性化和精准化,大大提高了告警响应的及时性和准确性。

告警抑制:在某些情况下,一个告警的产生可能会引发一系列其他相关告警。为了避免在这种情况下运维人员收到过多冗余的告警通知,Alertmanager提供了告警抑制功能。通过设置抑制规则,当某个特定告警被触发后,其他与之相关的告警可以被临时抑制。例如,当整个数据中心的网络出现故障时,可能会导致大量服务器和服务的连接异常告警。此时,可以配置Alertmanager,当数据中心网络故障的告警被触发后,抑制所有服务器和服务的连接异常告警,只保留网络故障的告警通知,这样可以有效避免告警风暴,让运维人员能够快速定位到问题的根源 。

(二)工作流程总览

Alertmanager的工作流程从接收告警开始,历经多个关键环节,最终将处理后的告警信息发送给相应的接收者。

首先,Alertmanager通过其HTTP API接收来自Prometheus或其他监控系统发送的告警信息。这些告警信息包含了丰富的元数据,如告警名称、描述、标签、发生时间等。例如,Prometheus在监测到某个服务器的CPU使用率超过80%且持续5分钟后,会向Alertmanager发送一条告警信息,其中包括告警名称“High CPU Usage”、详细描述“Server X's CPU usage has exceeded 80% for 5 minutes”,以及相关标签如“server=X”“service=backend”等。

接收告警后,Alertmanager会进行去重处理。它会根据告警的标签和内容,判断是否存在重复的告警事件。如果发现重复告警,会将其合并,确保在一定时间内,同一告警只会被通知一次。

接着,进入告警分组环节。根据预先设定的分组规则,Alertmanager会将具有相同或相似标签的告警归为一组。例如,根据“service”标签进行分组,所有与“backend”服务相关的告警会被合并成一个组。分组后的告警信息会以更清晰、有条理的方式呈现给运维人员,便于他们进行统一处理。

之后,告警路由开始发挥作用。Alertmanager会根据告警的属性和配置的路由规则,将告警分发到相应的接收器。例如,对于“severity=critical”且“service=backend”的告警,会被路由到后端运维团队的Slack群组;而对于“severity=warning”且“service=frontend”的告警,则会发送到前端运维团队的电子邮件地址。

在整个过程中,Alertmanager还会根据配置的抑制规则进行抑制判断。如果满足抑制条件,某些相关告警的通知将被临时抑制,避免过多冗余告警的干扰。

最后,通过配置好的接收器,如电子邮件、Slack等,将处理后的告警信息发送给相应的人员或团队。这样,运维人员能够及时获取到关键的告警信息,采取相应的措施进行处理,保障系统的稳定运行。

三、核心功能的实现逻辑

(一)告警去重机制

1. 哈希算法原理

Alertmanager采用哈希算法实现告警去重。它通过对告警的标签进行特定计算,生成一个哈希值。以代码中的hashAlert函数为例,具体步骤如下:

func hashAlert(a *types.Alert) uint64 {  
    const sep = '\xff'  
  
    hb := hashBuffers.Get().(*hashBuffer)  
    defer hashBuffers.Put(hb)  
    b := hb.buf[:0]  
  
    names := make(model.LabelNames, 0, len(a.Labels))  
  
    for ln := range a.Labels {  
       names = append(names, ln)  
    }  
    sort.Sort(names)  
  
    for _, ln := range names {  
       b = append(b, string(ln)...)  
       b = append(b, sep)  
       b = append(b, string(a.Labels[ln])...)  
       b = append(b, sep)  
    }  
  
    hash := xxhash.Sum64(b)  
  
    return hash  
}

首先,它将告警a的所有标签名提取出来,存入names切片中。接着,对names进行排序,这一步至关重要,确保了相同标签集合无论以何种顺序输入,都能得到一致的处理结果。然后,遍历排序后的names,将标签名、分隔符sep、标签值以及分隔符依次追加到字节切片b中。最后,使用xxhash.Sum64函数对字节切片b进行哈希计算,得到最终的哈希值。这个哈希值就像告警的“指纹”,用于唯一标识该告警。如果两个告警的哈希值相同,那么在去重机制中,它们就被视为重复告警。

2. 去重流程

在Alertmanager的去重过程中,DedupStage阶段起着关键作用。其核心逻辑如下:

func (n *DedupStage) Exec(ctx context.Context, _ *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {  
    gkey, ok := GroupKey(ctx)  
    if !ok {  
       return ctx, nil, errors.New("group key missing")  
    }  
  
    repeatInterval, ok := RepeatInterval(ctx)  
    if !ok {  
       return ctx, nil, errors.New("repeat interval missing")  
    }  
  
    firingSet := map[uint64]struct{}{}  
    resolvedSet := map[uint64]struct{}{}  
    firing := []uint64{}  
    resolved := []uint64{}  
  
    var hash uint64  
    for _, a := range alerts {  
       hash = n.hash(a)  
       if a.Resolved() {  
          resolved = append(resolved, hash)  
          resolvedSet[hash] = struct{}{}  
       } else {  
          firing = append(firing, hash)  
          firingSet[hash] = struct{}{}  
       }  
    }  
  
    ctx = WithFiringAlerts(ctx, firing)  
    ctx = WithResolvedAlerts(ctx, resolved)  
  
    entries, err := n.nflog.Query(nflog.QGroupKey(gkey), nflog.QReceiver(n.recv))  
    if err != nil && !errors.Is(err, nflog.ErrNotFound) {  
       return ctx, nil, err  
    }  
  
    var entry *nflogpb.Entry  
    switch len(entries) {  
    case 0:  
    case 1:  
       entry = entries[0]  
    default:  
       return ctx, nil, fmt.Errorf("unexpected entry result size %d", len(entries))  
    }  
  
    if n.needsUpdate(entry, firingSet, resolvedSet, repeatInterval) {  
       return ctx, alerts, nil  
    }  
    return ctx, nil, nil  
}

在这个函数中,首先初始化了两个用于存储哈希值的集合firingSet和resolvedSet,分别用于记录触发状态和已解决状态的告警哈希值,同时初始化了两个用于存储哈希值的切片firing和resolved。然后,遍历传入的所有告警alerts,通过n.hash(a)计算每个告警的哈希值。根据告警的状态(a.Resolved()判断是否已解决),将哈希值分别存入对应的集合和切片中。接着,通过WithFiringAlerts和WithResolvedAlerts函数将计算得到的触发和已解决的告警哈希值切片存入上下文ctx中,以便后续阶段使用。之后,从日志nflog中查询与当前告警组和接收器相关的记录entries。根据查询结果,判断是否需要更新告警信息。如果n.needsUpdate返回true,则说明当前实例需要继续发送这些告警,函数返回当前上下文ctx、原始告警列表alerts以及nil错误,表示处理成功;否则,返回当前上下文ctx、空的告警列表nil以及nil错误,意味着这些告警已被其他实例发送,本实例不再重复发送。通过这一系列步骤,Alertmanager有效地实现了告警去重,避免了重复告警对运维人员的干扰。

(二)告警分组策略

1. 分组依据

Alertmanager主要依据告警的标签来进行分组。通过配置group_by参数,可以指定按照哪些标签进行分组。例如,当配置group_by: ['alertname', 'cluster']时,具有相同alertname和cluster标签值的告警会被归为一组。假设在一个分布式系统中,有多个服务实例运行在不同的集群上。当某个服务出现故障时,会产生多个告警,每个告警都带有alertname(如ServiceDown)和cluster(如cluster1、cluster2等)标签。根据上述配置,所有alertname为ServiceDown且cluster相同的告警会被分到一组。这样,运维人员可以清晰地看到每个集群中该服务的故障情况,而不是面对大量零散的告警信息,大大提高了故障排查的效率。如果在一个电商系统中,订单服务出现问题,可能会触发多个与订单相关的告警,如订单创建失败、订单支付失败等。通过合理设置group_by参数,将这些告警按照订单服务相关的标签进行分组,运维人员可以快速了解订单服务整体的故障状况,而不是被众多单独的告警所困扰。

2. 分组时间控制

分组时间控制涉及到group_wait和group_interval两个重要参数。

group_wait表示在发送一个告警组的通知之前,Alertmanager等待新告警加入同一组的时间。例如,当设置group_wait: 30s时,如果在这30秒内,有新的符合分组条件的告警产生,它们会被添加到当前组中,然后一起发送通知。这就好比在收集货物准备发货,在30秒的等待时间内,不断有新的货物(新告警)到达,都被装进同一个包裹(告警组),30秒后,这个包裹被寄出(发送告警组通知)。这样可以避免短时间内频繁发送小的告警组通知,减少通知的数量,提高运维人员的处理效率。

group_interval则定义了在一个告警组已经发送通知后,再次发送该组更新通知之前需要等待的时间。例如,设置group_interval: 5m,当一个告警组在某一时刻发送了通知后,在接下来的5分钟内,即使该组有新的告警加入或状态发生变化,Alertmanager也不会立即发送更新通知。只有在5分钟之后,才会重新评估该组是否需要发送新的通知。这有助于防止在短时间内对同一问题进行过度通知,避免运维人员被频繁的告警更新所打扰。比如,某个服务的短暂波动可能会在短时间内产生多个告警,但通过合理设置group_interval,可以将这些告警的更新合并在一个适当的时间点发送,让运维人员能够更有效地处理告警信息。

(三)告警路由规则

1. 路由树结构

Alertmanager的路由规则以路由树的形式组织。路由树的每个节点都包含了一系列的配置信息,用于决定如何处理接收到的告警。节点的核心构成包括匹配条件和Continue字段。

匹配条件用于判断告警是否与该节点匹配。例如,通过设置match: {severity: 'critical'},表示当告警的severity标签值为critical时,该告警与这个节点匹配。还可以使用正则表达式进行更灵活的匹配,如match_re: {service: '^backend.*'},这会匹配所有service标签以backend开头的告警。

Continue字段则决定了告警在匹配当前节点后的行为。当Continue为false(默认值)时,一旦告警匹配到当前节点,就会停止在路由树中的进一步匹配,直接按照该节点的配置进行处理,如将告警发送到指定的接收器。例如,在一个简单的路由配置中,顶级节点配置了receiver: 'default_receiver',一个子节点配置为match: {severity: 'critical'}, receiver: 'critical_receiver', Continue: false。当一个severity为critical的告警到达时,它会匹配到这个子节点,由于Continue为false,告警将直接被发送到critical_receiver,不再继续检查其他子节点。而当Continue为true时,告警在匹配当前节点后,会继续在路由树中向下匹配其他节点,直到找到最合适的处理方式。假设存在另一个子节点配置为match: {service: 'database'}, receiver: 'database_receiver', Continue: true,且告警的service标签为database,即使它先匹配到了前面的critical节点,由于该节点Continue为true,告警仍会继续匹配到这个database节点,并最终可能被发送到database_receiver,具体取决于后续的匹配情况。通过这种灵活的路由树结构和配置,Alertmanager能够根据告警的各种属性,将其准确地路由到相应的接收器,实现高效的告警分发。

2. 路由匹配过程

当Alertmanager接收到一个告警时,会从路由树的顶级节点开始进行匹配。以如下代码所示的路由配置为例:

route:
  group_by: ['alertname']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'default_receiver'
  routes:
  - match:
      severity: 'critical'
      service: 'backend'
    receiver: 'backend_critical_receiver'
    continue: false
  - match:
      severity: 'warning'
      service: 'backend'
    receiver: 'backend_warning_receiver'
    continue: true
  - match:
      service: 'frontend'
    receiver: 'frontend_receiver'

当一个告警到达后,首先会检查其是否满足顶级节点的条件(这里顶级节点主要是设置了一些默认参数和分组相关配置)。然后,依次检查子节点的匹配条件。假设一个告警的标签为severity: 'critical', service: 'backend',它会首先匹配到第一个子节点,由于该子节点的Continue为false,告警将直接被发送到backend_critical_receiver,不再继续检查其他子节点。若一个告警的标签为severity: 'warning', service: 'backend',它会匹配到第二个子节点,因为Continue为true,告警会继续检查后续子节点。如果后续没有其他更匹配的节点,它将根据该节点的配置被发送到backend_warning_receiver。再假设一个告警的标签为service: 'frontend',它不会匹配到前两个子节点,但会匹配到第三个子节点,最终被发送到frontend_receiver。通过这样逐步的匹配过程,Alertmanager能够根据告警的具体属性,将其精准地路由到最合适的接收器,确保告警能够被及时、准确地处理。

(四)告警抑制功能

1. 抑制规则定义

在 Alertmanager 的配置文件中,通过 inhibit_rules 部分定义抑制规则。每个抑制规则包含以下三个主要部分:

  • source_match 和 **source_match_re**:定义触发抑制的告警的匹配条件。这些告警通常是高等级的告警。source_match 用于精确匹配标签值,而 source_match_re 用于正则表达式匹配。
  • target_match 和 **target_match_re**:定义将被抑制的告警的匹配条件。这些告警通常是低等级的告警。同样,target_match 用于精确匹配,target_match_re 用于正则表达式匹配。
  • **equal**:定义触发抑制的告警和被抑制的告警之间必须匹配的标签。只有当这些标签的值相同时,抑制规则才会生效。

2、抑制逻辑的处理流程

  • 告警接收与存储:Alertmanager 通过 API 接收来自 Prometheus 的告警信息,并将其存储在内存中的 Alert Provider 中。
  • 告警匹配与分组:Dispatcher 组件从 Alert Provider 订阅告警信息,并根据配置的路由规则(route)对告警进行匹配和分组。
  • 抑制规则的匹配:在告警分组后,进入 Notification Pipeline 组件的 InhibitStage 阶段。此阶段会检查当前告警是否满足抑制规则:

a.首先,检查当前告警是否匹配 target_match 和 target_match_re 定义的条件。

b.然后,检查是否存在已触发的告警(即 source 告警),且该 source 告警满足 source_match 和 source_match_re 定义的条件。

c.最后,检查 source 告警和当前告警的 equal 标签值是否相同。如果所有条件都满足,则当前告警被标记为抑制状态,不会发送通知。

d.后续处理:被抑制的告警不会进入后续的通知发送阶段,从而避免了不必要的告警通知。

2. 告警抑制的实现源码

  • 抑制规则的定义与加载 在 Alertmanager 配置文件加载时,会解析 inhibit_rules 配置,并将其转换为内部的抑制规则结构。相关代码如下:
// inhibit/inhibitor.go
func NewInhibitor(ap provider.Alerts, rs []config.InhibitRule, mk types.AlertMarker, logger *slog.Logger) *Inhibitor {  
    ih := &Inhibitor{  
       alerts: ap,  
       marker: mk,  
       logger: logger,  
    }  
    for _, cr := range rs {  
       r := NewInhibitRule(cr)  
       ih.rules = append(ih.rules, r)  
    }  
    return ih  
}

这里,config.InhibitRule 是从配置文件中解析出的抑制规则结构,Inhibitor 结构体用于管理这些规则。

  • 抑制规则的匹配 在 InhibitStage 阶段,会调用 Inhibitor 的 Mutes 方法来检查告警是否满足抑制规则。相关代码如下:
// inhibit/inhibit.go
func (ih *Inhibitor) Mutes(lset model.LabelSet) bool {  
    fp := lset.Fingerprint()  
  
    for _, r := range ih.rules {  
       if !r.TargetMatchers.Matches(lset) {  
          // If target side of rule doesn't match, we don't need to look any further.  
          continue  
       }  
       // If we are here, the target side matches. If the source side matches, too, we  
       // need to exclude inhibiting alerts for which the same is true.       
       if inhibitedByFP, eq := r.hasEqual(lset, r.SourceMatchers.Matches(lset)); eq {  
          ih.marker.SetInhibited(fp, inhibitedByFP.String())  
          return true  
       }  
    }  
    ih.marker.SetInhibited(fp)  
  
    return false  
}

Mutes 方法会遍历所有定义的抑制规则,调用每个规则的 Mutes 方法来判断当前告警是否满足该规则。

告警抑制的方法在notify/notify.go中的抑制阶段MuteStage结构体中进行调用,如下:

// MuteStage filters alerts through a Muter.type MuteStage struct {  
    muter   types.Muter  
    metrics *Metrics  
}  
  
// NewMuteStage return a new MuteStage.  
func NewMuteStage(m types.Muter, metrics *Metrics) *MuteStage {  
    return &MuteStage{muter: m, metrics: metrics}  
}  
  
// Exec implements the Stage interface.func (n *MuteStage) Exec(ctx context.Context, logger *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {  
    var (  
       filtered []*types.Alert  
       muted    []*types.Alert  
    )  
    for _, a := range alerts {  
       // TODO(fabxc): increment total alerts counter.  
       // Do not send the alert if muted.  
       if n.muter.Mutes(a.Labels) {  
          muted = append(muted, a)  
       } else {  
          filtered = append(filtered, a)  
       }  
       // TODO(fabxc): increment muted alerts counter if muted.  
    }  
    if len(muted) > 0 {  
  
       var reason string  
       switch n.muter.(type) {  
       case *silence.Silencer:  
          reason = SuppressedReasonSilence  
       case *inhibit.Inhibitor:  
          reason = SuppressedReasonInhibition  
       default:  
       }  
       n.metrics.numNotificationSuppressedTotal.WithLabelValues(reason).Add(float64(len(muted)))  
       logger.Debug("Notifications will not be sent for muted alerts", "alerts", fmt.Sprintf("%v", muted), "reason", reason)  
    }  
  
    return ctx, filtered, nil  
}

通过n.muter.Mutes(a.Labels)来判断告警是否被抑制,如果未被抑制,则存入filtered结构体进行后续处理,反之存入muted结构体,这些告警不发送通知。

通过以上源码分析可以看出,Alertmanager 的告警抑制功能是通过配置文件定义规则,然后在告警处理流程中逐条检查告警是否满足抑制规则来实现的。

四、源码深度剖析

(一)关键数据结构

1. Alert结构

在Alertmanager的源码中,Alert结构体是表示告警信息的核心数据结构。其定义如下:

type Alert struct {
 GeneratorURL strfmt.URI `json:"generatorURL,omitempty"`
 Labels LabelSet `json:"labels"`
}

Labels字段是一个LabelSet类型的键值对集合,用于唯一标识告警,并在告警分组、路由和去重等操作中发挥关键作用。例如,在告警分组时,通过比较不同告警的Labels中指定的标签,将具有相同标签值的告警归为一组。假设在一个分布式系统中,有多个服务实例,每个服务实例的告警都带有service、instance等标签。当配置group_by: ['service']时,Labels中service标签值相同的告警会被分到同一组。

2. 其他重要结构

除了Alert结构,还有一些与告警处理紧密相关的重要结构。

Group结构在告警分组中起着关键作用。它将多个相关的Alert组合在一起,方便进行统一处理和通知。一个Group通常包含一组具有相同或相似特征的告警,这些特征由配置的group_by标签决定。例如,在一个包含多个微服务的系统中,根据service标签进行分组,所有与user-service相关的告警会被归到同一个Group中。在代码实现中,Group结构可能包含Labels(用于标识该组告警的公共标签)、Alerts(该组内的告警列表)等字段,如下所示:

type AlertGroup struct {  
    Alerts []*GettableAlert `json:"alerts"`
    Labels LabelSet `json:"labels"`
    Receiver *Receiver `json:"receiver"`
}

在告警分组过程中,通过计算告警的标签指纹,将具有相同指纹(即相同group_by标签组合)的告警添加到同一个Group中。这使得运维人员在收到通知时,可以一次性了解到与某个特定服务或场景相关的所有告警情况,而不是面对大量零散的告警信息,大大提高了告警处理的效率。

Route结构则用于定义告警的路由规则。它以树状结构组织,每个节点都包含了一系列的配置信息,用于决定如何处理接收到的告警。其核心构成包括Matchers(匹配条件)、Receiver(接收器)和Continue字段。

type Route struct {  
    parent *Route  
  
    // The configuration parameters for matches of this route.  
    RouteOpts RouteOpts  
  
    // Matchers an alert has to fulfill to match    // this route.    
    Matchers labels.Matchers  
  
    // If true, an alert matches further routes on the same level.  
    Continue bool  
  
    // Children routes of this route.  
    Routes []*Route  
}

Matchers用于判断告警是否与该节点匹配。例如,通过设置Matchers: labels.Matchers{{Name: "severity", Value: "critical"}},表示当告警的severity标签值为critical时,该告警与这个节点匹配。Receiver指定了匹配该节点的告警要发送到的接收器,如电子邮件地址、Slack频道等。Continue字段决定了告警在匹配当前节点后的行为。当Continue为false(默认值)时,一旦告警匹配到当前节点,就会停止在路由树中的进一步匹配,直接按照该节点的配置进行处理,如将告警发送到指定的接收器。而当Continue为true时,告警在匹配当前节点后,会继续在路由树中向下匹配其他节点,以寻找更合适的处理方式。通过这种灵活的路由树结构和配置,Alertmanager能够根据告警的各种属性,将其准确地路由到相应的接收器,实现高效的告警分发。

(二)核心处理流程

1. API接收告警

在Alertmanager中,API接收告警的功能主要由api/v2/api.go文件中的代码实现。当Alertmanager接收到来自Prometheus或其他监控系统发送的告警时,会调用postAlertsHandler函数,其代码如下:

func (api *API) postAlertsHandler(params alert_ops.PostAlertsParams) middleware.Responder {  
    logger := api.requestLogger(params.HTTPRequest)  
  
    alerts := OpenAPIAlertsToAlerts(params.Alerts)  
    now := time.Now()  
  
    api.mtx.RLock()  
    resolveTimeout := time.Duration(api.alertmanagerConfig.Global.ResolveTimeout)  
    api.mtx.RUnlock()  
  
    for _, alert := range alerts {  
       alert.UpdatedAt = now  
  
       // Ensure StartsAt is set.  
       if alert.StartsAt.IsZero() {  
          if alert.EndsAt.IsZero() {  
             alert.StartsAt = now  
          } else {  
             alert.StartsAt = alert.EndsAt  
          }  
       }  
       // If no end time is defined, set a timeout after which an alert  
       // is marked resolved if it is not updated.       if alert.EndsAt.IsZero() {  
          alert.Timeout = true  
          alert.EndsAt = now.Add(resolveTimeout)  
       }  
       if alert.EndsAt.After(time.Now()) {  
          api.m.Firing().Inc()  
       } else {  
          api.m.Resolved().Inc()  
       }  
    }  
  
    // Make a best effort to insert all alerts that are valid.  
    var (  
       validAlerts    = make([]*types.Alert, 0, len(alerts))  
       validationErrs = &types.MultiError{}  
    )  
    for _, a := range alerts {  
       removeEmptyLabels(a.Labels)  
  
       if err := a.Validate(); err != nil {  
          validationErrs.Add(err)  
          api.m.Invalid().Inc()  
          continue  
       }  
       validAlerts = append(validAlerts, a)  
    }  
    if err := api.alerts.Put(validAlerts...); err != nil {  
       logger.Error("Failed to create alerts", "err", err)  
       return alert_ops.NewPostAlertsInternalServerError().WithPayload(err.Error())  
    }  
  
    if validationErrs.Len() > 0 {  
       logger.Error("Failed to validate alerts", "err", validationErrs.Error())  
       return alert_ops.NewPostAlertsBadRequest().WithPayload(validationErrs.Error())  
    }  
  
    return alert_ops.NewPostAlertsOK()  
}

首先,函数将接收到的告警数据params.Alerts通过OpenAPIAlertsToAlerts函数进行转换,得到alerts列表。接着,对每个告警a进行处理。调用removeEmptyLabels函数清理告警标签中的空值,确保标签数据的有效性。然后,通过a.Validate()方法对告警进行全面校验,包括检查标签的格式是否正确、是否包含必要的标签等。如果校验失败,将错误信息添加到validationErrs中,并增加无效告警的统计计数api.m.Invalid().Inc(),同时跳过该无效告警,继续处理下一个。

经过校验后,将所有有效的告警validAlerts通过api.alerts.Put方法存入alerts存储结构中。这里的api.alerts是一个实现了Alerts接口的实例,通常是基于内存的mem.Alerts实现。在mem.Alerts的Put方法中,会为每个告警生成唯一的指纹(基于标签计算),并将告警存储到内部的store.Alerts结构(本质是一个map[model.Fingerprint]*types.Alert)中。如果存储过程中发生错误,记录错误日志并返回500 Internal Server Error响应给发送方,告知告警接收失败。若所有操作成功,则返回200 OK响应,表明告警已成功接收并存储。通过这一系列严谨的步骤,Alertmanager实现了对告警的准确接收、校验和存储,为后续的告警处理流程提供了可靠的数据基础。

2. Dispatcher分发告警

Dispatcher在Alertmanager中承担着分发告警的重要职责,其核心逻辑在dispatch/dispatch.go文件中。Dispatcher通过订阅alerts存储结构(通常是mem.Alerts)的Subscribe方法,获取到新的告警信息。其Run函数如下:

func (d *Dispatcher) Run() {  
    d.done = make(chan struct{})  
  
    d.mtx.Lock()  
    d.aggrGroupsPerRoute = map[*Route]map[model.Fingerprint]*aggrGroup{}  
    d.aggrGroupsNum = 0  
    d.metrics.aggrGroups.Set(0)  
    d.ctx, d.cancel = context.WithCancel(context.Background())  
    d.mtx.Unlock()  
  
    d.run(d.alerts.Subscribe())  
    close(d.done)  
}

在run函数中,通过不断从it.Next()获取新的告警alert,并针对每个接收到的告警,查找匹配的路由规则:

func (d *Dispatcher) run(it provider.AlertIterator) {  
    maintenance := time.NewTicker(30 * time.Second)  
    defer maintenance.Stop()  
  
    defer it.Close()  
  
    for {  
       select {  
       case alert, ok := <-it.Next():  
          if !ok {  
             // Iterator exhausted for some reason.  
             if err := it.Err(); err != nil {  
                d.logger.Error("Error on alert update", "err", err)  
             }  
             return  
          }  
  
          d.logger.Debug("Received alert", "alert", alert)  
  
          // Log errors but keep trying.  
          if err := it.Err(); err != nil {  
             d.logger.Error("Error on alert update", "err", err)  
             continue  
          }  
  
          now := time.Now()  
          for _, r := range d.route.Match(alert.Labels) {  
             d.processAlert(alert, r)  
          }  
          d.metrics.processingDuration.Observe(time.Since(now).Seconds())  
  
       case <-maintenance.C:  
          d.doMaintenance()  
       case <-d.ctx.Done():  
          return  
       }  
    }  
}

d.route.Match(alert.Labels)函数会遍历路由树,查找与告警标签匹配的路由节点r。一旦找到匹配的路由节点,就调用processAlert函数对告警进行处理。

processAlert函数的主要工作是将告警分配到对应的聚合组aggrGroup中,代码如下:

func (d *Dispatcher) processAlert(alert *types.Alert, route *Route) {  
    groupLabels := getGroupLabels(alert, route)  
  
    fp := groupLabels.Fingerprint()  
  
    d.mtx.Lock()  
    defer d.mtx.Unlock()  
  
    routeGroups, ok := d.aggrGroupsPerRoute[route]  
    if !ok {  
       routeGroups = map[model.Fingerprint]*aggrGroup{}  
       d.aggrGroupsPerRoute[route] = routeGroups  
    }  
  
    ag, ok := routeGroups[fp]  
    if ok {  
       ag.insert(alert)  
       return  
    }  
  
    // If the group does not exist, create it. But check the limit first.  
    if limit := d.limits.MaxNumberOfAggregationGroups(); limit > 0 && d.aggrGroupsNum >= limit {  
       d.metrics.aggrGroupLimitReached.Inc()  
       d.logger.Error("Too many aggregation groups, cannot create new group for alert", "groups", d.aggrGroupsNum, "limit", limit, "alert", alert.Name())  
       return  
    }  
  
    ag = newAggrGroup(d.ctx, groupLabels, route, d.timeout, d.logger)  
    routeGroups[fp] = ag  
    d.aggrGroupsNum++  
    d.metrics.aggrGroups.Inc()  
  
    // Insert the 1st alert in the group before starting the group's run()  
    // function, to make sure that when the run() will be executed the 1st    
    // alert is already there.    
    ag.insert(alert)  
  
    go ag.run(func(ctx context.Context, alerts ...*types.Alert) bool {  
       _, _, err := d.stage.Exec(ctx, d.logger, alerts...)  
       if err != nil {  
          logger := d.logger.With("num_alerts", len(alerts), "err", err)  
          if errors.Is(ctx.Err(), context.Canceled) {  
             // It is expected for the context to be canceled on  
             // configuration reload or shutdown. In this case, the             
             // message should only be logged at the debug level.             
             logger.Debug("Notify for alerts failed")  
          } else {  
             logger.Error("Notify for alerts failed")  
          }  
       }  
       return err == nil  
    })  
}

首先,根据告警alert和匹配的路由route,通过getGroupLabels函数获取用于分组的标签groupLabels,并计算其指纹fp。然后,从d.aggrGroupsPerRoute中查找与该路由route对应的聚合组集合routeGroups。如果该集合不存在,则创建一个新的空集合。接着,在routeGroups中查找是否已存在指纹为fp的聚合组ag。若存在,直接将告警插入该聚合组ag.insert(alert)。若不存在,则创建一个新的聚合组ag,使用newAggrGroup函数,传入上下文d.ctx、分组标签groupLabels、路由route、超时设置d.timeout和日志记录器d.logger。创建完成后,将新的聚合组添加到routeGroups中,并增加聚合组的统计计数。最后,将告警插入新创建的聚合组,并启动一个新的协程go ag.run(...),该协程会调用d.stage.Exec方法,将聚合组中的告警传递给后续的处理阶段(如Notify模块)进行处理。通过这样的流程,Dispatcher实现了将告警准确地分发到相应的聚合组,为后续的告警处理和通知发送做好准备。

3. Notify模块发送通知

Notify模块负责将处理后的告警信息发送给相应的接收者,其核心处理流程在notify/notify.go文件中定义。Notify模块通过构建一个pipeline来处理告警,pipeline由多个阶段(Stage)组成,每个阶段执行特定的操作。

PipelineBuilder用于构建pipeline,其New函数如下:

func (pb *PipelineBuilder) New(  
    receivers map[string][]Integration,  
    wait func() time.Duration,  
    inhibitor *inhibit.Inhibitor,  
    silencer *silence.Silencer,  
    intervener *timeinterval.Intervener,  
    marker types.GroupMarker,  
    notificationLog NotificationLog,  
    peer Peer,  
) RoutingStage {  
    rs := make(RoutingStage, len(receivers))  
  
    ms := NewGossipSettleStage(peer)  
    is := NewMuteStage(inhibitor, pb.metrics)  
    tas := NewTimeActiveStage(intervener, marker, pb.metrics)  
    tms := NewTimeMuteStage(intervener, marker, pb.metrics)  
    ss := NewMuteStage(silencer, pb.metrics)  
  
    for name := range receivers {  
       st := createReceiverStage(name, receivers[name], wait, notificationLog, pb.metrics)  
       rs[name] = MultiStage{ms, is, tas, tms, ss, st}  
    }  
  
    pb.metrics.InitializeFor(receivers)  
  
    return rs  
}

在构建pipeline时,首先创建了一些通用的阶段,如NewGossipSettleStage用于处理集群节点间的状态同步(确保各个节点对告警处理的一致性),NewMuteStage(分别基于inhibitor和silencer)用于实现告警抑制和静默功能,NewTimeMuteStage和NewTimeActiveStage用于根据时间区间对告警进行静音或激活处理。然后,针对每个接收器name,通过createReceiverStage函数创建一个包含特定接收器处理流程的阶段st。这个阶段通常包含Wait(等待阶段,用于控制发送频率)、Dedup(去重阶段,避免重复发送相同告警)、Retry(重试阶段,确保告警能成功发送)等子阶段。最后,将这些阶段组合成一个MultiStage结构,并添加到rs中,形成完整的pipeline。

以RetryStage为例,其执行逻辑如下:

func (r RetryStage) exec(ctx context.Context, l *slog.Logger, alerts ...*types.Alert) (context.Context, []*types.Alert, error) {  
    var sent []*types.Alert  
  
    // If we shouldn't send notifications for resolved alerts, but there are only  
    // resolved alerts, report them all as successfully notified (we still want the    // notification log to log them for the next run of DedupStage).    
    if !r.integration.SendResolved() {  
       firing, ok := FiringAlerts(ctx)  
       if !ok {  
          return ctx, nil, errors.New("firing alerts missing")  
       }  
       if len(firing) == 0 {  
          return ctx, alerts, nil  
       }  
       for _, a := range alerts {  
          if a.Status() != model.AlertResolved {  
             sent = append(sent, a)  
          }  
       }  
    } else {  
       sent = alerts  
    }  
  
    b := backoff.NewExponentialBackOff()  
    b.MaxElapsedTime = 0 // Always retry.  
  
    tick := backoff.NewTicker(b)  
    defer tick.Stop()  
  
    var (  
       i    = 0  
       iErr error  
    )  
  
    l = l.With("receiver", r.groupName, "integration", r.integration.String())  
    if groupKey, ok := GroupKey(ctx); ok {  
       l = l.With("aggrGroup", groupKey)  
    }  
  
    for {  
       i++  
       // Always check the context first to not notify again.  
       select {  
       case <-ctx.Done():  
          if iErr == nil {  
             iErr = ctx.Err()  
             if errors.Is(iErr, context.Canceled) {  
                iErr = NewErrorWithReason(ContextCanceledReason, iErr)  
             } else if errors.Is(iErr, context.DeadlineExceeded) {  
                iErr = NewErrorWithReason(ContextDeadlineExceededReason, iErr)  
             }  
          }  
  
          if iErr != nil {  
             return ctx, nil, fmt.Errorf("%s/%s: notify retry canceled after %d attempts: %w", r.groupName, r.integration.String(), i, iErr)  
          }  
          return ctx, nil, nil  
       default:  
       }  
  
       select {  
       case <-tick.C:  
          now := time.Now()  
          retry, err := r.integration.Notify(ctx, sent...)  
          dur := time.Since(now)  
          r.metrics.notificationLatencySeconds.WithLabelValues(r.labelValues...).Observe(dur.Seconds())  
          r.metrics.numNotificationRequestsTotal.WithLabelValues(r.labelValues...).Inc()  
          if err != nil {  
             r.metrics.numNotificationRequestsFailedTotal.WithLabelValues(r.labelValues...).Inc()  
             if !retry {  
                return ctx, alerts, fmt.Errorf("%s/%s: notify retry canceled due to unrecoverable error after %d attempts: %w", r.groupName, r.integration.String(), i, err)  
             }  
             if ctx.Err() == nil {  
                if iErr == nil || err.Error() != iErr.Error() {  
                   // Log the error if the context isn't done and the error isn't the same as before.  
                   l.Warn("Notify attempt failed, will retry later", "attempts", i, "err", err)  
                }  
                // Save this error to be able to return the last seen error by an  
                // integration upon context timeout.                
                iErr = err  
             }  
          } else {  
             l := l.With("attempts", i, "duration", dur)  
             if i <= 1 {  
                l = l.With("alerts", fmt.Sprintf("%v", alerts))  
                l.Debug("Notify success")  
             } else {  
                l.Info("Notify success")  
             }  
  
             return ctx, alerts, nil  
          }  
       case <-ctx.Done():  
          if iErr == nil {  
             iErr = ctx.Err()  
             if errors.Is(iErr, context.Canceled) {  
                iErr = NewErrorWithReason(ContextCanceledReason, iErr)  
             } else if errors.Is(iErr, context.DeadlineExceeded) {  
                iErr = NewErrorWithReason(ContextDeadlineExceededReason, iErr)  
             }  
          }  
          if iErr != nil {  
             return ctx, nil, fmt.Errorf("%s/%s: notify retry canceled after %d attempts: %w", r.groupName, r.integration.String(), i, iErr)  
          }  
          return ctx, nil, nil  
       }  
    }  
}

在RetryStage中,使用backoff.NewExponentialBackOff创建一个指数退避策略的重试机制。通过backoff.NewTicker启动一个定时器tick,定时尝试发送告警。每次尝试发送时,调用r.integration.Notify(ctx, alerts...)方法,通过具体的接收器(如电子邮件、Slack等集成方式)发送告警。在整个过程中,如果上下文ctx被取消,且存在未处理的错误iErr,则返回错误信息。通过这样的pipeline处理流程,Notify模块实现了对告警的去重、按策略发送和重试等功能,确保告警能够准确、可靠地发送到相应的接收者手中。

五、最后

Alertmanager作为监控体系中的关键组件,在告警处理方面展现出了强大的功能和高效的实现逻辑。通过对告警去重、分组、路由和抑制等核心功能的深入分析,以及对其源码中关键数据结构和处理流程的解读,我们清晰地认识到它如何在复杂的监控环境中,将海量的告警信息进行有序管理和精准分发。它不仅有效减少了告警噪音,提高了运维人员的工作效率,还确保了关键告警能够及时送达相关人员手中,为保障系统的稳定运行发挥了重要作用。

责任编辑:武晓燕 来源: 运维开发故事
相关推荐

2009-09-25 09:36:55

Hibernate核心

2024-01-16 16:39:33

PythonPyPy

2021-02-18 15:36:13

PrometheusAlertmanageGrafana

2024-09-11 08:37:39

2021-11-29 12:11:09

npm包管理器工具

2010-12-15 15:46:43

SharePoint

2011-12-25 15:33:13

ibmdwKVM

2017-11-24 11:38:05

2009-07-06 10:44:45

JSP charset

2009-09-27 17:13:36

Hibernate V

2011-06-03 13:48:18

JavaScript重构

2010-06-03 13:08:51

2023-01-13 08:35:29

告警降噪系统

2009-09-14 15:12:40

LINQ to XML

2010-05-25 12:59:00

Subversion

2021-03-06 22:41:06

内核源码CAS

2010-02-04 13:50:56

ibmdw云计算

2010-10-08 15:17:47

JavaScriptJava

2010-08-04 08:42:28

Flex和Flash

2010-07-07 10:21:35

IGRP路由协议
点赞
收藏

51CTO技术栈公众号