当前位置:主页 > 查看内容

一文搞懂 bosun 查询

发布时间:2021-05-17 00:00| 位朋友查看

简介:背景 bosun 是一个由 Stack Exchange 开源的监控和告警系统,可以对标的工具有 prometheus 的 alertmanager . bosun 的设计目的是用于配合各种 tsdb 配置监控告警系统,但是 bosun 同时又提供了一套 dsl 用于查询监控、评估指标,使得 bosun 本身也是一种 ts……

背景

bosun 是一个由 Stack Exchange 开源的监控和告警系统,可以对标的工具有 prometheus 的 alertmanager. bosun 的设计目的是用于配合各种 tsdb 配置监控告警系统,但是 bosun 同时又提供了一套 dsl 用于查询监控、评估指标,使得 bosun 本身也是一种 tsdb 无关(目前支持如 opentsdb, prometheus, influxdb, es 等多种 tsdb 后端)的指标查询语言。要理解 bosun 是如何生成告警,或者仅仅是利用他的指标查询能力,配合如 grafana 这样的监控前端来展示指标,那么就必须要了解这门语言。

bosun 并不是一个非常火热的项目,目前有 3.1k star, 市面上介绍他的文档也比较少,大部分是对官方文档的直译。本文的目的是从概念角度介绍 bosun 查询的方式(主要针对后端为 opentsdb),以及一些查询的技巧。

概念

首先要了解 bosun 查询中的一些类型概念:

  1. Scalar 就是一个数字
  2. NumberSet 和 Scalar 基本是一回事,但是多了一组 tag,空 {} 也算是 tag
  3. SeriesSet 是 表征原始指标最常见的格式,和 NumberSet 不同,它对应的值不是一个数字,而是一组关联的时间戳的值,比如 时间 100 下的值 3.14, 时间 200 下的值 3.28
  4. Results 不是一个文档中介绍的概念,却是实际查询中最常见的类型,它代表一次查询的最常见的结果:即一组 tag 不同的 SeriesSet 或者 NumberSet 等的集合。文档里面把不同的 tags 组合又叫 group。

查询

针对 opentsdb 中的查询,bosun 提供了几种查询方式

q

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 前。

# 例子 
q("sum:rate{counter}:${service}.rpc.calledby.success.throughput", "5m", "1m")

group	result	computations
{ }	
{
  "1620196950": 2.016666666666667,
  "1620196980": 2.3666666666666667,
  "1620197010": 1.0999999999999999,
  "1620197040": 1.8333333333333335,
  "1620197070": 2.7333333333333343,
  "1620197100": 2.5,
  "1620197130": 1.7000000000000002,
  "1620197160": 0.9666666666666666
}

bandQuery/overQuery

bandQuery(query string, duration string, period string, eduration string, num scalar) seriesSet band(query string, duration string, period string, num scalar) seriesSet

  • bandQuery 的意思是 用 query 语句进行多次 (num 次) 查询,每次查询的时间范围由 duration/period 决定,
  • band 是 bandQuery 的特殊形式,相当于设置了 eduration = period,比如 band("avg:os.cpu", "1h", "1d", 3) 相当于查询了以下三个语句 q("avg:os.cpu", "25h", "1d"), q("avg:os.cpu", "49h", "2d"), q("avg:os.cpu", "73h", "3d"), 因为设置了 eduration=period,所以最近的那个周期是 (period+duration,period)

overQuery(query string, duration string, period string, eduration string, num scalar) seriesSet over(query string, duration string, period string, num scalar) seriesSet shiftBand(query string, duration string, period string, num scalar) seriesSet

  • overQuery 是 over 和 shiftBand 的通用形式,和 bandQuery 的不同之处在于,查询之后会对查询结果加上查询偏移相关的标签 "shifted"
  • over 和 shiftBand 只是overQuery 的特殊形式,只是相当于分别给 overQuery 的 eduration 设置为 period 和 当前时间(即不填写)
# 例子,由于 剩下几种只是 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 的计算方式可能是最让人困扰的一部分,要理解这个,首先要结合第一节讲的概念理解几个核心:

  1. 查询大部分的返回结果是一组 SeriesSet 或者 NumberSet 即 Results,比如我们在查询的时候使用 这样的 query: avg:rate:net.bytes{host=*}, 就会自动产生多个 group 的 SeriesSet ( 如果不希望产生,只是筛选可以这么写 avg:rate:net.bytes{}{host=1.2.3.4})
  2. bosun 文档中的大部分函数是针对单个 group 的 SeriesSet,即对查询结果应用函数的时候,是对每个 group 按个应用函数,比如 avg(q("avg:rate:net.bytes{host=*}", "60m", "")) 查询返回的结果有 {host=a}, {host=b} 等等,那么对多个 group 分别应用 avg 函数
  3. 不同的 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))

更多例子

$aa=series("tagA=a", 0, 2, 60, 2)
$ab=series("tagA=a,tagB=b", 0, 2, 60, 1)
$ac=series("tagA=a,tagC=c", 0, 2, 60, 3)
$bb=series("tagB=b", 0, 2, 60, 2)
$cc=series("tagC=c", 0, 2, 60, 2)

# {tagA=a} {tagB=b} {tagC=c}
$abc=merge($aa,$bb,$cc)
# {tagA=a} {tagA=a,tagB=b}
$aab = merge($aa, $ab)  
# {tagA=a} {tagA=a,tagC=c}
$aac = merge($aa, $ac)

# 这里可以参与计算的组合有 ({tagA=a}, {tagA=a})  ({tagA=a},{tagA=a,tagC=c}) ({tagA=a,tagB=b},{tagA=a}) 
# $aab+$aac


#  {tagA=a} {tagB=b}
$aabb = merge($aa, $bb)  
#  {tagA=a} {tagC=c}
$aacc = merge($aa, $cc)


# $aacc+$aabb

#  {tagA=a} {tagC=c} + {tagA=a} {tagA=a,tagC=c}
# $aacc+$aac


#  {tagA=a} {tagC=c} +  {tagA=a} {tagB=b} {tagC=c}
# $aacc+$abc

技巧

避免 unjoined group

一种常见的做法是使用 group 相关的一些操作函数,比如查询的时候就干脆不生成 group, 使用 filter 语句查询,比如 avg(q("sum:rate:metrics.notexist{}{status=500)}", "1m", "0m")), 或者查询之后使用 addtags, remove 这样的函数来处理 tags,来避免 group 之间的不兼容。这里介绍另一种巧妙的做法,可以忽略掉 unjoined group. 即使用 bandQuery 来查询,

比如一个计算请求错误率的例子:

$key_err = "sum:rate{counter}:${service}.rpc.calledby.error.throughput{method=*}"
$key_succ = "sum:rate{counter}:${service}.rpc.calledby.success.throughput{method=*}"

$err_now = avg(q($key_err, "5m", "1m"))
$succ_now = avg(q($key_succ, "5m", "1m"))

$rate_now = $err_now / ($err_now +$succ_now)
$rate_now

使用上面的查询方式会产生大量 unjoined group, 原因是 rpc.calledby.error.throughput 这个指标的 tags 数量比 success 的数量要少很多,但是我又希望返回结果能带上 method 这个分组标签。使用 band 的查询方式如下:

$key_err = "sum:rate{counter}:${service}.rpc.calledby.error.throughput{method=*}"
$key_succ = "sum:rate{counter}:${service}.rpc.calledby.success.throughput{method=*}"

$err_now = avg(band($key_err, "4m", "1m", 1))
$succ_now = avg(band($key_succ, "4m", "1m", 1))

$rate_now = $err_now / ($err_now +$succ_now)
$rate_now

使用 band 查询不会产生 unjoined group,unjoined group 的结果会被忽略,即 results 之间的计算中,生成 unjoined group 的步骤会被忽略。

grafana bosun 插件

grafana bosun 插件中会有两个内置的变量

  • $ds: 建议的 downsampling interval, 这个变量很有用,在查询的使用比如 q("avg:$ds-avg:os.disk.fs.space_free{disk=*,host=backup}", "$start", ""), 会在用户选择较大时间范围的时候保持查询效率.
  • $start: 用户选择的起始时间

t 函数的使用

group 的操作函数有几个, 这里介绍一个 t 函数,他可以将多个 group 的 seriesSet join 成一个 group 的,来配合一些计算函数的使用。举个例子,计算 api 的 60 min 加权 latency:

$latency=avg(q("avg:${service}.calledby.success.latency.us.pct99{handle_method=*}", "60m", ""))
$count=sum(q("sum:rate{counter,,,diff}:${service}.calledby.success.throughput{handle_method=*}", "60m", ""))
$total=sum(q("sum:rate{counter,,,diff}:${service}.calledby.success.throughput{}", "60m", ""))


sum(t($latency*($count/$total), ""))

其他参考


本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐