Prometheus rate & irate

背景

实例状态、服务状态、Spring MVC 接口等相关监控打点看板基本都由架构、运维实现

但业务中会存在和业务数据更密切的监控需求,一般也使用 Prometheus 或类似的数据库来实现

对于 Prometheus 的函数选择存在一些疑问,所以写了这边文档,着重关注 rateirate 的实现原理和实践方法,可以解释以下问题

  • 为什么一个 QPS 看板缩小时间窗口后,某个点的 QPS 会上升
  • rateirate 的区别和实现方式是什么;以及 increasedelta
  • 时间窗口内数据 range-vector 和步长 step Interval 之间的关系
  • 如何选择合适的函数
  • 聚合函数和时间向量函数的区别

实现

rate

rate 方法用于计算一组向量之间的速率

  • 筛选时间窗口内样本,选择首尾两个样本计算速率
  • 窗口外推
  • 兼容计数器重置
1
2
3
4
5
6
7
8
9
10
// ... 兼容计数器重制
// ... 外推计算

// 外推值
factor := extrapolateToInterval / sampledInterval

if isRate {
// rate 计算
factor /= ms.Range.Seconds()
}

计数器重置

通过将之前的值加到累积的结果中来保持连续性

1
2
3
4
5
6
7
8
// Handle counter resets:
prevValue := samples.Floats[0].F
for _, currPoint := range samples.Floats[1:] {
if currPoint.F < prevValue {
resultFloat += prevValue
}
prevValue = currPoint.F
}

对于普通浮点数值的计数器,当检测到当前值小于前一个值时(表示计数器重置),会将前一个值加到最终结果中

外推

外推的本意是避免某个窗口内样本过少造成的数据波动

如果样本足够接近范围的(下限或上限)边界,会将速率一直外推至该边界,足够接近的定义是不超过范围内样本间平均持续时间的 10%

如果小于阈值,则只外推一半

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Duration between first/last samples and boundary of range.
durationToStart := float64(firstT-rangeStart) / 1000
durationToEnd := float64(rangeEnd-lastT) / 1000

sampledInterval := float64(lastT-firstT) / 1000
averageDurationBetweenSamples := sampledInterval / float64(numSamplesMinusOne)

// 阈值
extrapolationThreshold := averageDurationBetweenSamples * 1.1
extrapolateToInterval := sampledInterval

if durationToStart >= extrapolationThreshold {
durationToStart = averageDurationBetweenSamples / 2
}

// 外推开始时间
extrapolateToInterval += durationToStart

if durationToEnd >= extrapolationThreshold {
durationToEnd = averageDurationBetweenSamples / 2
}

// 外推结束时间
extrapolateToInterval += durationToEnd

这里省略了外推计算时间窗口时对于 Counter 值为 0 的兼容

窗口内的所有点

last - first

rate 方法计算窗口内所有点的平均变化率

1
sampledInterval := float64(lastT-firstT) / 1000

体现了其特点

  • 观察较长时间的趋势,提供更平滑的曲线
  • 有助于减少短期波动引起的噪音

irate

iraterate 的区别是其只取最后的 2 个点

  • 筛选时间窗口内样本,选择最后的 2 个点
  • 处理计数器重置

最后两个点

index 1 - index 0

1
2
resultSample.F = ss[1].F - ss[0].F
resultSample.F /= float64(sampledInterval) / 1000

体现了其特点

  • 提供瞬时变化率,对短期变化更敏感
  • 计数器重置处理更简单,忽略变化结果,因为不重视整体趋势
  • 需要较小的时间窗口和步长配合,否则可能会导致数据失真

计数器重置

直接选择最后一个点的值

1
2
3
4
5
6
7
8
9
10
switch {
case ss[1].H == nil && ss[0].H == nil:
if !isRate || !(ss[1].F < ss[0].F) {
// Gauge, or counter without reset, or counter with NaN value.
resultSample.F = ss[1].F - ss[0].F
}

// In case of a counter reset, we leave resultSample at
// its current value, which is already ss[1].
// ... 这里只考虑 counter 浮点数计算

如果没有重置,则将 resultSample.F 值置为两个点差值

如果重置,则忽略,此时值已经是 ss[1]

图示

借用 irate() vs rate() – What’re they telling you? – Tech Annotation

提供的示意图

假设一组数据,每 10s 进行一次打点

这是 irate 方法的计算,每 20s 作为窗口计算其速率,步长为 10s

这是 rate 方法的计算,每 40s 作为窗口计算其速率,步长为 10s

比较其曲线

总结

rate 函数注重整体趋势,适用于

  • 平滑稳定的指标观察
    • 服务稳定的 QPS
    • 网络流量的整体变化趋势
    • 错误率的整体评估
  • 长期趋势的监控
    • 每日 / 每周 / 每月的业务增长情况
    • 服务容量规划和扩展需求
    • 长期可用性、性能等指标计算
  • 报警
    • 需要避免短期波动导致的误报
  • 容量规划
    • 资源使用趋势分析
    • 预测未来负载增长

irate 函数注重短时间内的变化,适用于

  • 实时监控
    • 快速检测突发问题
    • 实时监控系统状态变化
    • 实时资源使用可视化
  • 高变化率指标的观察
    • CPU 使用率的瞬时波动
    • 服务响应时间的实时变化
  • 系统负载变化实时响应
    • 自动扩缩容触发条件监控
    • 流量突变的即时检测

步长和时间窗口

除了打点数据和其时间窗口,Dashboard 的绘制还需要通过步长 Interval 来确定窗口移动的速度

这个值应该和时间窗口符合一定的匹配关系

例如在 Grafana 中

  • Min interval 用于为查询间隔设置一个最小值;建议将其设置为数据写入频率,例如如果数据每分钟写入一次,则设置为 1m
  • Interval 为计算后步长间隔,Max data points / time range 此值会作为步长用于实际查询
  • Max data points 限制了点的最大数量,同时影响步长的计算;其默认最大值受 Grafana 图标的像素宽度影响

步长和时间窗口之间应该合理搭配,假设一个步长大于时间窗口,则会造成数据缺失

有了步长后,就可以按照步长推进时间窗口

假设如上一组数据,通过图示可以看出

  • 5s 一次采样
  • 一共采样了 15 个点
  • 时间窗口为 30s
  • 步长为 10s

开始的两组数据平稳增长,rateirate 结果一致;而后到了第三组有一个 105 → 150 的突变,此时体现到 irate 的变化更大;对于最后一组数据,rate 对边界进行了外推,补充了缺失的窗口数据,得出了 3.19 的结果

其他函数

向量函数

对于时间向量相关的函数,还有诸如 deltaincrease

实际在 Prometheus 的源码中,它们都与 rate 复用了大部分代码

底层方法和其业务函数

  • extrapolatedRate
    • rate
    • delta
    • increase
  • instantValue
    • irate
    • idelta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// === delta(Matrix parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcDelta(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
return extrapolatedRate(vals, args, enh, false, false)
}

// === rate(node parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcRate(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
return extrapolatedRate(vals, args, enh, true, true)
}

// === increase(node parser.ValueTypeMatrix) (Vector, Annotations) ===
func funcIncrease(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) {
return extrapolatedRate(vals, args, enh, true, false)
}

其中不同的参数是第 4 和 5,它们分别为

第 4 个 isCounter ;如果为 true,则允许处理计数器重置

第 5 个 isRate;如果为 true,则返回为每秒值,否则返回的是总值

由此可见

  • delta 强调双向变化(Gauge),负值或数值降低有其业务含义
  • increase 用于计算总值,所以不需要处以时间

聚合函数

Prometheus 支持以下内置聚合运算符,这些运算符可用于聚合单个瞬时向量的元素,从而生成一个元素更少且具有聚合值的新向量

  • sum (calculate sum over dimensions)
  • avg (calculate the arithmetic average over dimensions)
  • min (select minimum over dimensions)
  • max (select maximum over dimensions)
  • bottomk (smallest k elements by sample value)
  • topk (largest k elements by sample value)

这些运算符既可以用于对所有标签维度进行聚合,也可以通过包含 withoutby 子句来保留不同的维度

可以用在表达式之前或之后

1
2
3
<aggr-op> [without|by (<label list>)] ([parameter,] <vector expression>)

<aggr-op>([parameter,] <vector expression>) [without|by (<label list>)]

对于时间窗口的优化

参考了公司的一篇文章,讲解了现在的 VictoriaMetrics 对于 Delta 类型数据计算 QPS 不友好之处

看起来和 Promethues 的实现类似;其本质上是现有计算逻辑对于稀疏打点的结果无法准确表达;虽然 Counter 在 Prometheus 中是以 Cumulative 方式实现的,这里作为参考

  • increase 的效果是计算区间内的最新一个点和最老一个点的差值
  • rate 的效果是计算区间内的最新一个点和最老一个点的差值,再除以两者之间的时间间隔

存在的问题

如下场景可能出现对应问题

  • 时间窗口小于采样间隔;导致折线图无法连接(采样率太低)
  • 上报的指标数据的间隔不固定,计算出来的数据和真实值的差距会非常大(稀疏)
  • 绘图的时间窗口无法被查询的时间范围整除,或者最后一段的数据还没完全到达(被平滑或外推)
  • 计算时间窗口和数据采样之间的 offset 误差

rate_over_delta 的提出

rate_over_delta 函数的核心思想是 利用时间窗口内的数据除以数据的真实增长时间间隔,而非除以时间窗口

新的概念

  • Counter 类型的数据进行一次 rollup 收集数据,as_count 基于收集的数据做求和运算;as_rate 是基于收集的数据计算 QPS,解决现有的问题
  • 效果类似于 as_rate(rollup(container.cpu.time, default, 1m))

可能产生的问题

  • as_rateas_count 不再具有转化关系,在查询使用的时间范围很短并且数据上报不均匀的时候,as_rateas_count / window 的差值会比较大
  • as_rateas_count 不适合放在聚合函数之后;因为真实的计算逻辑是先计算出 QPS 或者增量,再使用聚合函数进行计算
1
2
3
4
5
6
# Prometheus rate
sum(rate(infra_rpc_server_request_duration_seconds_count{region=~"$region", se
rvice="$service"}[2m])) by(key)

# as_rate
as_rate(sum(tutor_exp_tts_starter_request_count{$serverName, $bizName, $facadeName}) by (bizName))

相关 PR

ADD: add rate_over_delta support for Counters reported by delta types by changshun-shi · Pull Request #6699 · VictoriaMetrics/VictoriaMetrics

ADD: add rate support for delta counters by changshun-shi · Pull Request #32 · VictoriaMetrics/metricsql

参考

prometheus/prometheus: The Prometheus monitoring system and time series database.

Query functions | Prometheus

Operators | Prometheus

irate() vs rate() – What’re they telling you? – Tech Annotation