cover_image

流利说告警排班系统的实践

LLS YC 流利说技术团队
2021年12月30日 11:00

1. 为什么要告警

        根据 Google SRE 这本书里对监控通知的定义,一个监控系统应该只有三类输出,其中一类便是 alert 警报。

        紧急警报(alert):意味着收到警报的用户需要立即执行某种操作,目标是解决某种已经发生的问题,或者是避免即将发生的问题。


2. 为什么需要排班

        对于一个警报而言,它可能是程序相关的警报, 也可能是程序运行环境的警报,对于程序来说,它的告警就应该发送给这个程序(app)相关的负责人,而对于运行环境来说,它的告警就应该发送给相关运维团队(team)。

        每一个 app 或者 team 会对应有多个人员,假设这么多人都要一起同时接收告警,就会显得很不合理,而我们想要的是一段时间内某一个人接收相关 app 的告警,这样的排班机制会减少运维成本、沟通成本以提升效率。


3. 告警排班系统的核心

  • 准确性:告警应该发往能解决告警的人

  • 触达性:能确保告警接收人收到告警

  • 收拢:多个相关的告警可以合并成一条告警

  • 及时响应:确保告警可以及时被处理


4. 架构演变:

图片

        这里我们将 pagerduty 替换成了 goalert(排班系统) + notice-center(通知系统),主要是为了

  • goalert 人员对齐公司内部人员系统,pagerduty 需要额外去注册生效,而 goalert 只需要通过企业聊天系统扫码登录,降低了操作成本。

  • notice-center 支持企业聊天系统告警通知等多种通知渠道,满足业务需求。

  • 根据 mercury 告警转换服务可以使 goalert 对接云监控等多种告警源。

  • 根据不同业务生成对应的排班策略表。


5. 排班系统(goalert)

5.1 目的

        当前时间段内某个 app 或 team 的相关告警应该发送给谁。

5.2 优点

  • 永不错过的 alert:goalert 根据定制的排班表逐层通知,如果某一层暂时无法响应,请自动升级到其他人。

  • 简化 oncall 的管理:通过对告警调度表的管理,控制 oncall 顺序以快速解决相关信息。

  • 可定制的集成:goalert 提供与现有监控和遥测系统的外部集成的便捷选项(支持与多种告警源交互)。

  • 对移动端友好:您可以通过移动端的 Web UI 来确认和关闭警报。

5.3 告警流程

1. Prometheus 根据告警规则触发告警发往 AlertManager。

2. AlertManager 先根据分组、抑制、静默策略对告警进行分组过滤,然后再根据告警的 app 或 team label,将告警通过服务集成密钥(integration keys)发往对应的 goalert service。

3. goalert service 根据绑定的排班策略表(escalation policy)计算出第一层告警人,然后将告警详情以及告警接收人发送给通知系统(notice-center)。

4. 通知系统先根据接收到的告警详情中的告警级别字段指定告警通知渠道,然后再查询告警接收人的联系方式,将告警详情渲染后通过指定的告警通知渠道将告警发送给接收人

5.4 登录方式

        目的:对接公司内部人员系统,直接扫码即可登录注册。

        方案:由于 goalert 只支持 OIDC 登录,所以在这里我们通过 dex 做了一层代理,将 LLS OAUTH 登录转换成了 OIDC 登录,最终对接到 goalert 。


图片


5.5 goalert 相关资源定义

  • rotation(轮班表,多个人按天或按周轮换)

  • 图片

  • schedule(调度表,可以在指定时间段内绑定多个轮班表或者 user ,而且也可以在指定时间内替换某个人排班,比轮班表更加精确)

  • 图片

  • escalation policy(升级策略,通过告警升级策略,配置每一层应该发给某个调度表或者某个人员)

  • 图片

  • service(服务,可以绑定升级策略以及设定服务集成密钥)

  • 图片

  • integration keys(服务集成密钥,提供给 alertmanager、mercury或其他告警源发送告警)

  • 图片

5.6 二次开发

 场景需求:

        当接收到一个当前无法解决的告警,我们需要将告警静默一段时间再继续通知。虽然 alertmanager 自带的 silence 功能就可以支持,但是如果让用户在两个服务之间来回切换会增加成本,而且可能会因为操作失误的原因影响到其他告警,所以我们需要在goalert 端静默掉指定时间段内的告警,让告警在 alertmanager 中存在但不通知到用户。

原 ack 功能:

        在 ack 某条告警之后,该告警便会一直静默。但是如果我们忘记处理该告警,那么该告警便一直不会发送给用户,如果告警一直没有被处理,那么便会影响业务。

改进:

        增加一个 snooze 表,对每条告警维护一个静默时间,当 ack 一个告警,默认让它静默 10 minutes,时间到后然后将告警的状态重置为 active(可以继续通知用户),如果想要静默更长时间,可以在页面上继续为它配置更长静默时间。

func (db *DB) _createOrUpdate(ctx context.Context, tx *sql.Tx, sz *AlertSnooze) (*AlertSnooze, error) {    findRow := tx.StmtContext(ctx, db.find).QueryRowContext(ctx, sz.AlertID)    err := findRow.Scan(&sz.ID)    if err == sql.ErrNoRows {        // 没有找到该条 snooze 记录则创建一条 snooze 记录(配置默认静默时间)        row := tx.StmtContext(ctx, db.create).QueryRowContext(ctx, sz.AlertID, sz.ServiceID, sz.LastAckTime, sz.DelayMinutes)        err = row.Scan(&sz.ID)        if err != nil {            return nil, err        }        return sz, nil    } else if err != nil {        return nil, err    }     // 如果找到该条 snooze 记录,则更新它的 DelayMinutes 字段(配置更长的静默时间)    row := tx.StmtContext(ctx, db.update).QueryRowContext(ctx, sz.AlertID, sz.DelayMinutes)    err = row.Scan(&sz.ID)    if err != nil {        return nil, err    }     return sz, nil}

效果如下

图片图片


6. 通知系统(notice-center)

6.1 目的

        将告警以指定的渠道通知到用户

6.2 主要功能

        通知系统按照功能划分为三个微服务

    • 信息查询服务:将企业聊天系统中人员的联系信息封装后提供给调度服务

    • 调度服务:

      • 根据接收到的告警信息,确定通知渠道

      • 查询告警接收人联系方式

      • 渲染通知模版

      • 合并不同接收人的相同消息内容

      • 按照指定的通知渠道将联系方式以及模版发送给通知服务

    • 通知服务:根据调度服务指定的通知渠道,调用云服务 API (目前支持email、phone、企业聊天系统)发送指定模版到指定通知人

6.3 通知流程

        1. 调度服务接收到一条告警通知,首先去解析通知方式、通知模版内容。

        2. 调度服务在信息查询服务查询告警接收人的联系信息。

        3. 调度服务将通知方式、通知模版内容以及告警接收人联系信息发往通知服务。

        4. 通知服务按照指定的通知方式将具体的通知模版发送给告警接收人。

func (d *Dispatch) Send(ctx context.Context, request *service_v1.DispatcherSendRequest) (*service_v1.DispatcherSendResponse, error) {    log.BgLogger().Info("svc.Send",        zap.String("msg", "sending message..."),        zap.Any("request type", request.Type),        zap.Any("request emailPrefix", request.EmailPrefix))     // 查询人员信息    person, err := d.search(request.EmailPrefix)    if err != nil {         log.BgLogger().Warn("svc.Send.query",            zap.String("msg", "query failed"),            zap.Error(err))         return &service_v1.DispatcherSendResponse{Status: false, Message: err.Error()},            status.Error(codes.NotFound, "pokemon center search error")    }     log.BgLogger().Debug("svc.Send.query",        zap.String("msg", "query ok"),        zap.Any("person", person))     // type:通知方式,payload:渲染后的通知模版,    // 按照指定的通知方式、通知模版发送给指定接收人    if err := d.send(context.Background(), person, request.Type, request.Payload); err != nil {         log.BgLogger().Info("svc.Send.email", zap.String("debug:", err.Error()))         return &service_v1.DispatcherSendResponse{Status: false, Message: err.Error()},            status.Error(codes.Aborted, "pikachu send error")    }     return &service_v1.DispatcherSendResponse{Status: true}, nil}


7. mercury 服务(Devops)

7.1 目的

        提供告警转换功能、实现自动化以减少运维成本

7.2 主要功能

  • 通过为第三方告警源提供 webhook 的告警接口,然后将接收到的告警转换成 goalert 告警接收格式后发往 goalert ,完成了告警转换功能。

  • 按照 catalog(维护公司内部 team, app, member 之间的关系) 人员关系通过 mercury 资源管理服务自动的在 goalert 上生成相关的排班策略,完成了资源生成功能。

  • 当在 goalert 中创建一个 service 以及相关的 integration key 时,会自动触发 mercury 在 alertmanager config 中生成对应的 config,减少运维成本。

  • 自动为新注册用户生成 Contact Methods(发往通知中心的 webhook 配置),用户只需首次注册 goalert 即可,减少不必要的人工成本。

  • 自动让用户关注当天所 oncall 的 service(goalert 默认只为用户显示用户关注的 service 的告警),提高准确性。

7.3 默认告警排班表模版

  • 第一层:app owner + app member 按周轮班(15 min 升级)

  • 第二层:同时通知所有的 app owner(15 min 升级)

  • 第三层:app所属 group / team member 按周轮换(15 min 升级)(重复三次)

7.4 具体实现

  • mercury 相关接口

图片

  • goalert 资源生成

var appInfo catalog.AppInfo// appOwners = catalogAppOwner // appUsers = catalogAppOwner + catalogAppMember// 获取 app 相关信息(member、owner、group、team)err = g.CatalogClient.GetAppInfo(&appInfo, "app", serviceName)if err != nil {    log.Errorf("Get app member from catalog failed, error : %v", err)    return nil, err}// 获取 app 对应的 team/group 相关信息(member)err = g.CatalogClient.GetAppInfo(&appInfo, appInfo.AscriptionType, serviceName)if err != nil {    log.Errorf("Get app's %s member from catalog failed, error : %v", appInfo.AscriptionType, err)    return nil, err} // 根据获取的信息,生成排班策略模版service = g.createServiceInputTemplate(createServiceTemplate{    serviceName:     serviceName,    serviceType:     serviceType,    ascriptionName:  appInfo.AscriptionName,    ascriptionType:  appInfo.AscriptionType,    appUsers:        appInfo.AppMembers,    appOwner:        appInfo.AppOwnerMembers,    ascriptionUsers: appInfo.AscriptionMembers,})
  • 告警转换

// 根据 message stateValue 判断告警状态if alarmDetail.NewStateValue == "ALARM" {    alarm.EventType = config.GoalertTriggerStatus} else if alarmDetail.NewStateValue == "OK" {    alarm.EventType = config.GoalertCloseStatus} else {    return fmt.Errorf("unknown alert status %s", alarmDetail.NewStateValue)} // 解析云监控告警到指定结构alarm.Description = strings.Join([]string{alarmDetail.NewStateValue, alarmDetail.AlarmName, alarmDetail.Region}, " ")alarm.Source = config.AwsAlarmSourcealarm.Details = alarmDetailalarm.Client = "Aws Console"alarm.ClientURL = awsClientUrl + alarmDetail.AlarmNamealarm.Details.Trigger.Unit = nil // 将解析好的云监控告警发送给 goalertif err = c.postToGoAlert(&awsAlarmReq{    Summary: alarm.Description,    Details: alarm,});

  • 自动生成 alertmanager config 

图片


8. 总结:

8.1 现有系统如何满足告警系统的核心:

  • 收拢:主要是根据 alertmanager 原生的分组(Grouping)、抑制(Inhibition)特性来将相同类型的告警归并到一个告警中,或者将由某个告警引起的其他告警屏蔽掉。

  • 准确性:我们在 goalert 中为每一个 app 都创建了一个 service,并且都有自定义的排班策略表,alertmanager 通过 app label 准确的将告警打给 goalert 中对应的 service ,然后由 goalert 选择出对应的告警接收人将告警发出。

  • 触达性:通知中心提供多个告警渠道以及发送失败重试机制来确保告警能够发送到告警接收人。

  • 响应:当排班策略表中某一层的告警接收人没有对告警及时响应时,goalert 将会自动通知给下一层中的告警接收人,确保告警可以被响应。

8.2 展望:

        未来我们会将通知中心接入 infra team 的各种各样的消息(ci、cd、trace等),不仅仅是作为告警排版系统的通知,而是逐渐改变成一个消息中心,来管理所有需要通知给用户的消息。

继续滑动看下一个
流利说技术团队
向上滑动看下一个