背景
bosun 是一个由 Stack Exchange 开源的监控和告警系统,可以对标的工具有 . bosun 的设计目的是用于配合各种 tsdb 配置监控告警系统,但是 bosun 同时又提供了一套 dsl 用于查询监控、评估指标,使得 bosun 本身也是一种 tsdb 无关(目前支持如 opentsdb, prometheus, influxdb, es 等多种 tsdb 后端)的指标查询语言。要理解 bosun 是如何生成告警,或者仅仅是利用他的指标查询能力,配合如 grafana 这样的监控前端来展示指标起步网校,那么就必须要了解这门语言。
并不是一个非常火热的项目,目前有 3.1k star, 市面上介绍他的文档也比较少物业经理人,大部分是对官方文档的直译。本文的目的是从概念角度介绍 bosun 查询的方式(主要针对后端为 opentsdb),以及一些查询的技巧。
概念
首先要了解 bosun 查询中的一些类型概念:
Scalar 就是一个数字NumberSet 和 Scalar 基本是一回事,但是多了一组 tag,空 {} 也算是 tagSeriesSet 是 表征原始指标最常见的格式,和 NumberSet 不同钓鱼网,它对应的值不是一个数字,而是一组关联的时间戳的值,比如 时间 100 下的值 3.14, 时间 200 下的值 3.28Results 不是一个文档中介绍的概念,却是实际查询中最常见的类型贝语网校,它代表一次查询的最常见的结果:即一组 tag 不同的 SeriesSet 或者 NumberSet 等的集合。文档里面把不同的 tags 组合又叫 group。

查询
针对 opentsdb 中的查询,bosun 提供了几种查询方式
q(query string, startDuration string, endDuration string) seriesSet
这是最常用的查询方式,大部分的告警也都是用这个语句查询出来的,这个语句非常简单,其中 query 是 opentsdb 的查询语句,startDuration 和 endDuration 为启止查询时间,比如 q(sum:rate{counter}:sys.cpu.user, 5m, 1m), 表示查询 sys.cpu.user 指标 5m前到 1m前这段时间的 sum:rate。 由于指标一般收集有延迟,endDuration 一般推荐为至少 1m 前。
bandQuery/overQuery
bandQuery(query string, duration string, period string, eduration string, num scalar) seriesSetband(query string, duration string, period string, num scalar) seriesSet
overQuery(query string, duration string, period string, eduration string, num scalar) seriesSetover(query string, duration string, period string, num scalar) seriesSetshiftBand(query string, duration string, period string, num scalar) seriesSet
# 例子,由于 剩下几种只是 bandQuery 和 overQuery 的特殊形式,这里只给出这两种查询的例子
> bandQuery("sum:rate{counter}:${service}.rpc.calledby.success.throughput", "5m", "60m", "1m", 2)
group result computations
{ }
{
"1620195120": 69.96666666666665,
"1620195150": 5.816666666666666,
"1620195180": 5.766666666666667,
"1620195210": 4.3,
"1620195240": 5.7666666666666675,
"1620195270": 3.7666666666666675,
"1620195300": 4.4,
"1620195330": 4.933333333333334,
"1620195360": 4.033333333333334,
"1620195390": 1.7000000000000002,
"1620198720": 69.93333333333334,
"1620198750": 11.7,
"1620198780": 1.2999999999999998,
"1620198810": 1.8500000000000008,
"1620198840": 2.766666666666667,
"1620198870": 4.633333333333333,
"1620198900": 4.833333333333334,
"1620198930": 2.366666666666667,
"1620198960": 2.366666666666667,
"1620198990": 2.2666666666666666
}
> overQuery("sum:rate{counter}:${service}.rpc.calledby.success.throughput", "5m", "60m", "1m", 2)
group result computations
{ shift=1m0s }
{
"1620198780": 69.93333333333334,
"1620198810": 11.7,
"1620198840": 1.2999999999999998,
"1620198870": 1.8500000000000008,
"1620198900": 2.766666666666667,
"1620198930": 4.633333333333333,
"1620198960": 4.833333333333334,
"1620198990": 2.366666666666667,
"1620199020": 2.366666666666667,
"1620199050": 2.2666666666666666
}
{ shift=1h1m0s }
{
"1620198780": 69.96666666666665,
"1620198810": 5.816666666666666,
"1620198840": 5.766666666666667,
"1620198870": 4.3,
"1620198900": 5.7666666666666675,
"1620198930": 3.7666666666666675,
"1620198960": 4.4,
"1620198990": 4.933333333333334,
"1620199020": 4.033333333333334,
"1620199050": 1.7000000000000002
}
bandQuery 和 overQuery 对于查询一个周期相同时间段(比如每天的这个时间)的指标很有用,而且很有意思的是 bandQuery 并不会产生 unjoined group, 这点在下面的小技巧里面进一步说明。
window
window(query string, duration string, period string, num scalar, funcName string) seriesSet
比起 bandQuery 和 overQuery,window 对于展示用途的查询更有用, window 会对每次查询的结果进行 funcName 的 reduction 计算,返回的值和时间戳生成一个新的时间序列。举个例子,你想查询过去 6个小时内每小时的请求数量,就可以用下面的计算方式:
> window("sum:rate{counter}:${service}.rpc.calledby.success.throughput", "60m", "60m", 6, "sum")
group result computations
{ }
{
"1620175620": 356260.0166666666,
"1620179220": 370473.99999999965,
"1620182820": 391460.0166666665,
"1620186420": 405893.36666666664,
"1620190020": 364280.9166666666,
"1620193620": 380179.3833333336
}

配合 grafana 就可以画出这样的一个曲线或者柱状图
count/change
count 表示查询返回的 Results 长度,而 change 表示变化一流范文网, change("avg:rate:net.bytes", "60m", "") = avg(q("avg:rate:net.bytes", "60m", "")) * 60 * 60
计算
bosun 的计算方式可能是最让人困扰的一部分,要理解这个,首先要结合第一节讲的概念理解几个核心:
查询大部分的返回结果是一组 SeriesSet 或者 NumberSet 即 Results,比如我们在查询的时候使用 这样的 query: avg:rate:net.bytes{host=*}, 就会自动产生多个 group 的 SeriesSet ( 如果不希望产生,只是筛选可以这么写 avg:rate:net.bytes{}{host=1.2.3.4})bosun 文档中的大部分函数是针对单个 group 的 SeriesSet,即对查询结果应用函数的时候,是对每个 group 按个应用函数,比如 avg(q("avg:rate:net.bytes{host=*}", "60m", "")) 查询返回的结果有 {host=a}, {host=b} 等等,那么对多个 group 分别应用 avg 函数不同的 Results 相互计算,举个例子 +, 是对所有的 group 组合分别应用 + 进行计算,但是并不是所有的 group 组合都能互相计算,只有互相为子集或者相等的 group 才能计算,所以就会产生 unjoined group, 没有参与计算的 group 就会产生一个 unjoined group, 这个计算有点抽象,可以看下面的例子帮助理解。在看结果之前可以猜测一下结果,确认自己的理解对不对。
# 两个 results 之间的运算方式
for g1 in Result1:
for g2 in Result2:
if g1 == g2 || g1 is subset of g2 || g2 is subset of g1:
计算
for g1 in Result1:
if g1 没有参与计算:
生成一个 unjoined group
for g2 in Result2:
if g2 没有参与计算:
生成一个 unjoined group
例子1
$a = series("X=a1,Y=b1", 100, 1, 200, 2)
$b = series("X=a2,Y=b2", 100, 2, 200, 3)
$x = series("X=a1", 100, 2, 200, 1)
$y = series("X=a1,Y=b2", 100, 3, 200, 5)
$z = series("X=a2,Y=b2", 100, 3, 200, 2)
# {X=a1,Y=b1} {X=a2,Y=b2}
$ab = merge($a, $b)
# {X=a1} {X=a1,Y=b2} {X=a2,Y=b2}
$xyz = merge($x, $y, $z)
# 这里可以参与计算的组合有 ({X=a1,Y=b1}, {X=a1}), ({X=a2,Y=b2}, {X=a2,Y=b2}), 由于 {X=a1,Y=b2} 没有参与计算,所以会生成一个 unjoined group
$ab+$xyz
-----------------------------------
group result computations
{ X=a1, Y=b1 }
{
"100": 3,
"200": 3
}
{ X=a2, Y=b2 }
{
"100": 5,
"200": 5
}
{ X=a1, Y=b2 }
{
"100": "NaN",
"200": "NaN"
}
merge(series("X=a1,Y=b1", 100, 1, 200, 2), series("X=a2,Y=b2", 100, 2, 200, 3)) + merge(series("X=a1", 100, 2, 200, 1), series("X=a1,Y=b2", 100, 3, 200, 5), series("X=a2,Y=b2", 100, 3, 200, 2)) unjoined group (NaN)

例子2
$a = series("Y=b2", 100, 1, 200, 1)
$b = series("X=a1,Y=b1", 100, 3, 200, 5)
$c = series("X=a2,Y=b2", 100, 3, 200, 2)
$x = series("X=a2", 100, 2, 200, 1)
$y = series("X=a1,Y=b2", 100, 3, 200, 5)
$z = series("X=a2,Y=b2", 100, 3, 200, 2)
# {X=a1,Y=b1} {X=a2,Y=b2} {Y=b2}
$abc = merge($b, $c, $a)
# {X=a2,Y=b2} {X=a1,Y=b2} {X=a2}, 这里把 {X=a2} 放在最后是因为第一个组合不能计算会报错
$xyz = merge($z, $y, $x)
$abc + $xyz
-----------------------------------
group result computations
{ X=a2, Y=b2 }
{
"100": 6,
"200": 4
}
{ X=a2, Y=b2 }
{
"100": 4,
"200": 3
}
{ X=a1, Y=b2 }
{
"100": 4,
"200": 6
}
{ X=a2, Y=b2 }
{
"100": 5,
"200": 3
}
{ X=a1, Y=b1 }
merge(series("X=a2,Y=b2", 100, 3, 200, 2), series("X=a1,Y=b2", 100, 3, 200, 5), series("X=a2", 100, 2, 200, 1)) + merge(series("X=a1,Y=b1", 100, 3, 200, 5), series("X=a2,Y=b2", 100, 3, 200, 2), series("Y=b2", 100, 1, 200, 1))

|