读懂 Bosun 语句:一条告警是怎么从指标变成人的

前两篇讲了监控报警体系和规则设计。这一篇落到 Bosun:一条告警规则在 Bosun 里怎么写,表达式怎么从时间序列变成布尔判断,模板和通知又怎么把机器信号变成人能处理的信息。

Bosun 是一个围绕时间序列告警设计的系统。它可以采集和转发数据,查询 OpenTSDB、Graphite、Logstash/Elasticsearch、Prometheus 等后端,用自己的表达式语言计算告警条件,并用 Go template 生成通知内容。它的配置是文本文件,适合版本管理。

一、Bosun 的心智模型

读 Bosun 语句,要先抓住三层:

  1. 数据层:查询函数拿到时间序列,例如 q() 返回一组 series。
  2. 表达式层:聚合、运算、比较,把 seriesSet 变成 numberSet 或 scalar。
  3. 告警层warn/crit 判断是否异常,template 渲染上下文,notification 负责发送。

Bosun 官方文档里有一句非常关键:表达式最终会被归约成一个数字,0 表示不触发,非 0 表示触发;同时表达式可以产生一个或多个 group,这些 group 决定告警实例的维度。

二、RuleConf:alert、template、notification

Bosun 的规则文件由多个 section 组成,每个 section 有类型和名称。最常见的是 alerttemplatenotificationlookupmacro

notification oncall_email {
    email = oncall@example.com
}

template service_basic {
    subject = `{{.Alert.Name}} {{.Group}} {{.CurrentStatus}}`
    body = `
    <p>状态:{{.CurrentStatus}}</p>
    <p>告警:{{.Alert.Name}}</p>
    <p>分组:{{.Group}}</p>
    <p>规则:{{.Alert.Text}}</p>
    <p>最近一次异常:{{.LastAbnormalTime}}</p>
    `
}

alert api_high_error_rate {
    template = service_basic
    $total = sum(q("sum:rate{counter,,1}:api.request.count{service=pay,cluster=prod,status=*}", "5m", ""))
    $error = sum(q("sum:rate{counter,,1}:api.request.count{service=pay,cluster=prod,status=5xx}", "5m", ""))
    $rate = ($error / $total) * 100
    warn = $rate > 1
    crit = $rate > 5
    warnNotification = oncall_email
    critNotification = oncall_email
}

这个例子里,alert 定义计算逻辑,template 定义通知长什么样,notification 定义发到哪里。生产环境里最好把通用模板和通知渠道抽出来复用,业务规则只写变量和判断。

三、数据类型和 group:为什么一条规则会变成很多条告警

Bosun 表达式里最常见的类型有三种:

  • Scalar:一个数字,没有业务维度。比如 160
  • NumberSet:一组带标签的数字。比如每台机器一个 CPU 使用率。
  • SeriesSet:一组带标签的时间序列。比如每台机器最近 5 分钟的 CPU 曲线。

告警实例由 alert name + group 唯一确定。比如 api_high_error_rate{service=pay,cluster=prod} 是一个实例,api_high_error_rate{service=order,cluster=prod} 是另一个实例。

这就是为什么查询里的 tag 很重要。你写 host=*,就可能每台机器一条告警;你先按 service 聚合,就可能每个服务一条告警。告警维度不是通知平台决定的,而是表达式里的 group 决定的。

四、q():从 OpenTSDB 拿时间序列

q(query, startDuration, endDuration) 是 Bosun 里最常见的 OpenTSDB 查询函数。第一个参数是 OpenTSDB 查询字符串,第二个参数表示从多久以前开始,第三个参数表示到多久以前结束;第三个参数为空字符串时表示到现在。

# 最近 5 分钟 CPU,每个 host 一条时间序列
$q = q("avg:rate{counter,,1}:os.cpu{host=*}", "5m", "")

# 最近 1 小时到 30 分钟前,用于和当前窗口对比
$previous = q("sum:api.request.count{service=pay}", "1h", "30m")

Bosun 查询出来的是 seriesSet,通常不能直接拿来做告警判断,要先通过 reduction function 归约成 numberSet。

五、归约函数:把曲线变成数字

常用归约函数包括 avg()max()min()sum()last()percentile() 等。它们会把每条时间序列压缩成一个数字,但保留 group。

$cpu_series = q("avg:rate{counter,,1}:os.cpu{host=*}", "5m", "")
$cpu_avg = avg($cpu_series)
$cpu_max = max($cpu_series)

warn = $cpu_avg > 80
crit = $cpu_max > 95

这里 $cpu_avg$cpu_max 都是按 host 分组的 numberSet。如果有 100 台机器,就可能得到 100 个判断结果。

六、warn / crit:0 与非 0

Bosun 的 warncrit 表达式必须返回 scalar 或 numberSet。结果为 0 表示 false,不触发;非 0 表示 true,触发。crit 优先于 warn:如果 crit 为真,就不会再按 warn 处理。

alert api_latency_high {
    template = service_basic
    $latency = percentile(q("avg:api.latency.ms{service=pay,cluster=prod}", "10m", ""), 95)
    warn = $latency > 300
    crit = $latency > 1000
    warnNotification = oncall_email
    critNotification = oncall_email
}

变量以 $ 开头,但 Bosun 的变量本质是文本替换,不是传统编程语言里的强类型变量。复杂表达式建议主动加括号,避免展开后优先级不符合预期。

七、Unknown:缺失数据怎么处理

Bosun 会记住某条规则见过的 tagset。如果之后这个 tagset 不再出现,表达式无法评估,就可能进入 Unknown。Unknown 很有价值,因为它能暴露采集失败、服务停止上报、指标改名等问题。

但不是所有缺失都应该报警。日志类指标没有数据可能代表没有错误;低频任务没有数据可能代表任务还没到调度时间。Bosun 提供了几个关键选项:

  • unknown = 5m:指定多久无法评估后进入 Unknown。
  • ignoreUnknown = true:忽略 Unknown,不让它变成告警。
  • unknownIsNormal = true:把 Unknown 视为恢复正常,适合错误日志这类稀疏信号。
  • nv(numberSet, scalar):在二元运算中把 NaN 替换成指定值,防止缺失组导致错误扩散。
alert api_no_traffic {
    template = service_basic
    $qps = sum(q("sum:rate{counter,,1}:api.request.count{service=pay,cluster=prod}", "10m", ""))
    crit = nv($qps, 0) < 1
    critNotification = oncall_email
}

这个例子把缺失流量当成 0 处理,适合应该持续有请求的入口服务。但如果是低频后台任务,这样写就会误报。

八、lookup:不同服务用不同阈值

统一阈值很容易出问题。支付服务和后台管理服务、核心接口和低频接口,不应该共享同一个错误率阈值。Bosun 的 lookup table 可以根据 tag 返回不同阈值或通知人。

lookup api_threshold {
    entry service=pay,cluster=prod {
        warn = 1
        crit = 5
        owner = oncall_email
    }
    entry service=*,cluster=prod {
        warn = 3
        crit = 10
        owner = oncall_email
    }
}

alert api_error_rate_by_lookup {
    template = service_basic
    $total = sum(q("sum:rate{counter,,1}:api.request.count{service=*,cluster=prod,status=*}", "5m", ""))
    $error = sum(q("sum:rate{counter,,1}:api.request.count{service=*,cluster=prod,status=5xx}", "5m", ""))
    $rate = ($error / $total) * 100
    warn = $rate > lookup("api_threshold", "warn")
    crit = $rate > lookup("api_threshold", "crit")
    warnNotification = lookup("api_threshold", "owner")
    critNotification = lookup("api_threshold", "owner")
}

lookup 的价值不只是动态阈值,也可以做动态路由:不同 service、cluster、host 匹配到不同通知渠道。这样规则可以保持通用,责任关系放到表里维护。

九、depends 与 squelch:控制告警风暴

depends 用来表达规则依赖:当另一个告警已经触发时,本告警可以不再评估。典型场景是 host down 后,host 上的 CPU、磁盘、进程告警都不应该继续刷屏。

alert host_down {
    template = service_basic
    $up = avg(q("avg:host.alive{host=*}", "5m", ""))
    crit = nv($up, 0) == 0
    critNotification = oncall_email
}

alert host_high_cpu {
    template = service_basic
    depends = alert("host_down", "crit")
    $cpu = avg(q("avg:rate{counter,,1}:os.cpu{host=*}", "5m", ""))
    warn = $cpu > 80
    crit = $cpu > 95
    warnNotification = oncall_email
    critNotification = oncall_email
}

squelch 则是按 tag 直接抑制某些范围,比如演练环境、临时集群、已下线机房。它更像配置层面的过滤。

十、模板和通知:让告警可处理

告警内容不是越短越好,而是要让值班人不用再问三件事:影响是什么、为什么触发、下一步去哪看。

template service_basic {
    subject = `{{.Alert.Name}} {{.Group}} {{.CurrentStatus}}`
    body = `
    <h3>{{.Alert.Name}}</h3>
    <p><b>状态:</b>{{.CurrentStatus}}</p>
    <p><b>分组:</b>{{.Group}}</p>
    <p><b>规则:</b>{{.Alert.Text}}</p>
    <p><b>最近异常:</b>{{.LastAbnormalTime}}</p>
    <p><b>处理建议:</b>{{.Alert.Vars.runbook}}</p>
    `
}

alert api_high_error_rate {
    template = service_basic
    $runbook = https://wiki.example.com/runbook/api-error-rate
    # ...表达式略...
}

一个高质量模板至少应该包含:当前值、阈值、分组、服务 owner、最近变更、相关图表、runbook、静默入口和升级方式。Bosun 的模板基于 Go template,并提供了 Graph、Eval、Lookup 等上下文函数,可以把表达式结果和图直接放进通知里。

十一、常见坑

  • 把所有 tag 都带进告警:group 过细会制造大量告警实例。先想清楚处理动作,再决定 group。
  • 低流量下算比例:没有最小流量保护,错误率会被少量请求放大。
  • 忽略 Unknown:采集挂了和服务挂了都可能表现为无数据,不能默认正常。
  • 变量不加括号:Bosun 变量是文本替换,复杂表达式要显式括号。
  • 没有回测:规则上线前应该用历史窗口测试,看看过去 7 天会触发几次。
  • 通知没有上下文:只写「错误率高」没有意义,必须带当前值、阈值、影响范围和排障入口。

结语

Bosun 语句的难点,不在函数多,而在它把「时间序列计算」和「告警实例维度」绑在一起。你写的每个 tag、每个聚合、每个 lookup,都会影响最后谁被叫醒、看到什么、能不能处理。

所以写 Bosun 规则时,不要只问语法能不能跑,还要问三个工程问题:这条规则代表什么故障?它会产生多少告警实例?收到它的人能不能立刻行动?

参考资料