Prometheus 动态指标可视化的深度优化:Counter 与 Gauge 的差异化处理

本文是《告别 Grafana 手搓 Dashboard:基于指标分组的 Prometheus 可视化新方案》的深度实践篇

一、一个让人困惑的监控图

16:05 发生了什么?

下午 4 点,订单团队的小王收到告警:订单创建失败。他立刻打开监控平台,点击"订单监控"菜单,看到这样一张图:

scss 复制代码
alarm_log_error_total (订单创建失败)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 1 |                    ╭─╮
   |                    │ │
   |                    │ │
 0 |────────────────────╯ ╰─────────
   └──────────────────────────────────
   16:00  16:05  16:10  16:15  16:20

小王懵了:"16:05 明明发生了错误,为什么图上显示的是 0?"

查看 Prometheus 原始数据

他去 Prometheus 查询 alarm_log_error_total,看到:

makefile 复制代码
时间      原始值
16:00     (不存在)
16:01     (不存在)
16:05     1        ← 第一次出现,错误已发生
16:06     1        ← 保持不变
16:07     1
16:10     2        ← 又增加了1次
16:11     2

原始数据清楚地记录了:16:05 发生了第一次错误,16:10 发生了第二次错误。

可为什么图上看不出来?


二、问题根源:Prometheus 的 increase() 不适合这个场景

传统做法:使用 increase()

在 Grafana 或其他可视化工具中,监控 Counter 指标的标准做法是:

promql 复制代码
increase(alarm_log_error_total[1m])

这个查询的结果是:

makefile 复制代码
时间      原始值    increase(1m)
16:05     1         0           ← 因为没有"前值",无法计算增量
16:06     1         0           ← 从 1 到 1,增量为 0
16:07     1         0
16:08     1         0
16:09     1         0
16:10     2         1           ← 从 1 到 2,增量为 1
16:11     2         0

结果:除了 16:10 这个点,其他全是 0。

小王看到的图就是基于这个结果画的------一条几乎全是 0 的线,只在 16:10 有一个小凸起。

为什么 increase() 会这样?

Prometheus 的 increase() 函数计算的是时间窗口内的变化量

  • 如果 Counter 从 0 变成 1,increase() = 1 ✓
  • 如果 Counter 保持为 1,increase() = 0 ✗

但在告警场景中:

  • 16:05 指标第一次被采集时,值就已经是 1(错误已经发生了)
  • 后续几分钟没有新错误,值保持为 1
  • increase() 认为"没有变化",所以返回 0

Prometheus 的设计理念:关注的是"速率"和"累计趋势",不是"瞬时事件"。

但业务团队要的是:"我就想知道每个时刻发生了几次错误"


三、我们想要什么样的图?

理想效果

小王期望看到的图应该是这样:

scss 复制代码
alarm_log_error_total (订单创建失败次数)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 1 |     ╭─╮         ╭─╮
   |     │ │         │ │
   |     │ │         │ │
 0 |─────╯ ╰─────────╯ ╰──────────
   └──────────────────────────────────
   16:00  16:05  16:10  16:15  16:20
          ↑            ↑
       第一次错误    第二次错误

清晰地看到

  • 16:05 有一个波峰,值为 1
  • 16:10 有一个波峰,值为 1
  • 其他时刻值为 0(没有新增错误)

对比两种图

时刻 Prometheus 原始值 传统 increase() 我们想要的
16:05 1 0 ✗ 1 ✓
16:06 1 0 ✓ 0 ✓
16:10 2 1 ✓ 1 ✓
16:11 2 0 ✓ 0 ✓

关键差异 :16:05 这个时刻,increase() 返回 0,但我们想要的是 1。


四、解决方案:计算相邻点的差值

核心思路

不使用 Prometheus 的 increase(),而是:

  1. 查询原始 Counter 值
  2. 在后端计算相邻两个点的差值
  3. 把差值返回给前端
ini 复制代码
原始值:   [0, 0, 0, 1, 1, 1, 2, 2, 2]
时间:    [t1,t2,t3,t4,t5,t6,t7,t8,t9]

计算差值:
t2: 0 - 0 = 0
t3: 0 - 0 = 0
t4: 1 - 0 = 1  ← 第一次错误
t5: 1 - 1 = 0
t6: 1 - 1 = 0
t7: 2 - 1 = 1  ← 第二次错误
t8: 2 - 2 = 0
t9: 2 - 2 = 0

返回前端: [0, 0, 1, 0, 0, 1, 0, 0]
时间:    [t2,t3,t4,t5,t6,t7,t8,t9]
         ↑ 注意:第一个点 t1 被丢弃

为什么丢弃第一个点?

因为第一个点没有"前值"可比较,无法计算差值。丢弃它之后,每个返回的点都表示"相对于前一时刻的变化"。

技术架构

graph LR A[Prometheus
存储原始Counter] -->|QueryRange| B[后端服务] B -->|1.补齐时间点| C[完整时序] C -->|2.计算差值| D[相邻点相减] D -->|3.移除首点| E[返回增量值] E -->|JSON| F[前端ECharts] F --> G[波峰图] style D fill:#ff9999 style G fill:#99ff99

五、后端核心实现(3 步搞定)

步骤 1:查询原始 Counter 值

go 复制代码
// 直接查询指标,不使用 increase() 或 rate()
query := "alarm_log_error_total"

result, _, err := promAPI.QueryRange(ctx, query, v1.Range{
    Start: startTime,
    End:   endTime,
    Step:  30 * time.Second,  // 30秒采样一次
})

// 返回: [[timestamp, value], ...]
// 例如: [[1736049600, 0], [1736049630, 0], [1736049660, 1], ...]

步骤 2:补齐缺失的时间点

Prometheus 不保证每个时间点都有数据(指标可能还未创建),需要补 0:

go 复制代码
func fillMissingPoints(points [][]float64, start, end time.Time, step time.Duration) [][]float64 {
    pointMap := make(map[int64]float64)
    for _, p := range points {
        pointMap[int64(p[0])] = p[1]
    }

    var result [][]float64
    for t := start.Unix(); t <= end.Unix(); t += int64(step.Seconds()) {
        if val, exists := pointMap[t]; exists {
            result = append(result, []float64{float64(t), val})
        } else {
            result = append(result, []float64{float64(t), 0})  // 补0
        }
    }
    return result
}

步骤 3:计算相邻差值(Counter 专用)

go 复制代码
func processCounter(points [][]float64) [][]float64 {
    if len(points) <= 1 {
        return [][]float64{}
    }
    
    result := make([][]float64, 0, len(points)-1)
    
    for i := 1; i < len(points); i++ {
        prevValue := points[i-1][1]
        currValue := points[i][1]
        timestamp := points[i][0]
        
        diff := currValue - prevValue
        
        // 处理 Counter 重置(服务重启)
        if diff < 0 {
            diff = currValue
        }
        
        result = append(result, []float64{timestamp, diff})
    }
    
    return result  // 注意:长度比原始数据少1
}

完整数据流

sequenceDiagram participant P as Prometheus participant B as 后端 participant F as 前端 F->>B: 查询"订单监控",时间范围1小时 B->>P: QueryRange(alarm_log_error_total) P-->>B: [0,0,0,1,1,1,2,2,2] Note over B: 补齐缺失时间点 Note over B: 计算相邻差值 Note over B: 移除第一个点 B-->>F: [[t2,0], [t3,0], [t4,1], [t5,0], [t6,0], [t7,1], [t8,0], [t9,0]] Note over F: 渲染图表 F->>F: 16:05和16:10出现波峰

六、Gauge 指标怎么办?

场景对比

假设我们还有一个 SQL 查询规则,统计"待处理订单数":

sql 复制代码
SELECT COUNT(*) FROM orders WHERE status = 'pending'

这个指标是 Gauge 类型(可增可减的瞬时值):

makefile 复制代码
时间      查询结果
16:00     100
16:05     150
16:10     120
16:15     180

业务团队想看的是:"当前有多少待处理订单",而不是"相对于上一时刻增加了多少"。

Gauge 不需要计算差值

go 复制代码
func processGauge(points [][]float64) [][]float64 {
    // 直接返回原始值,不做任何处理
    return points
}

返回前端:[[16:00, 100], [16:05, 150], [16:10, 120], [16:15, 180]]

前端渲染出一条平滑的曲线,清晰展示数值变化趋势。

Counter vs Gauge 对比

维度 Counter Gauge
语义 累计值(只增不减) 当前值(可增可减)
业务关心 增量:这一分钟新增了几次 绝对值:现在是多少
处理方式 计算相邻差值 保持原值
示例 错误次数、请求总数 连接数、队列长度、SQL查询结果

七、前端:零负担渲染

前端只做 3 件事

javascript 复制代码
// 1. 接收后端数据
const response = await getMetricData(groupId, timeRange)

// 2. 转换时间格式(秒 → 毫秒)
const chartData = response.series.map(s => ({
  name: formatLegend(s.metric),
  type: 'line',
  data: s.points.map(p => [p[0] * 1000, p[1]])  // 秒转毫秒
}))

// 3. 渲染图表
chart.setOption({
  xAxis: { type: 'time' },
  yAxis: { type: 'value' },
  series: chartData
})

不做任何差值计算、不做任何业务逻辑------所有复杂处理都在后端完成。

后端返回的数据格式

json 复制代码
{
  "name": "alarm_log_error_total",
  "type": "counter",
  "series": [{
    "metric": {
      "service": "order-service",
      "level": "error"
    },
    "points": [
      [1736049660, 0],
      [1736049720, 0],
      [1736049780, 1],  // 16:05 波峰
      [1736049840, 0],
      [1736049900, 0],
      [1736049960, 1]   // 16:10 波峰
    ]
  }]
}

八、效果对比

改造前(使用 increase())

makefile 复制代码
16:05 发生错误 → 图上显示 0
16:10 又发生错误 → 图上显示 1

业务团队:看不懂

改造后(计算相邻差值)

makefile 复制代码
16:05 发生错误 → 图上显示 1(波峰)
16:10 又发生错误 → 图上显示 1(波峰)

业务团队:一目了然


九、关键设计点

1. 采样粒度自适应

根据查询时间长度,自动调整采样间隔:

go 复制代码
func calculateStep(timeRange string) time.Duration {
    switch timeRange {
    case "15m":  return 15 * time.Second  // 15分钟 / 60点
    case "1h":   return 30 * time.Second  // 1小时 / 120点
    case "3h":   return 60 * time.Second  // 3小时 / 180点
    case "6h":   return 2 * time.Minute   // 6小时 / 180点
    case "12h":  return 4 * time.Minute   // 12小时 / 180点
    case "24h":  return 8 * time.Minute   // 24小时 / 180点
    }
}

设计原则:每个图表 60~180 个数据点,平衡精度与性能。

2. 指标类型识别

从规则类型推断指标类型:

go 复制代码
func getMetricType(rule Rule) string {
    if rule.Type == "keyword" {
        return "counter"  // 日志关键词规则 → Counter
    }
    if rule.Type == "query" {
        return "gauge"    // SQL 查询规则 → Gauge
    }
}

3. 前后端职责分离

层次 职责 为什么
后端 指标类型识别、差值计算、数据补齐 理解业务语义,处理复杂逻辑
前端 时间格式转换、图表渲染 专注展示,保持简洁

十、处理边界情况

Counter 重置

服务重启时,Counter 从大值重置为 0:

makefile 复制代码
时间      原始值    天真计算     修正后
16:00     100       -            -
16:01     150       50 ✓        50 ✓
16:02     0         -150 ✗      0 ✓
16:03     5         5 ✓         5 ✓

修正逻辑:

go 复制代码
diff := currValue - prevValue
if diff < 0 {
    diff = currValue  // 负数说明重置,用当前值
}

指标首次出现

指标第一次被采集时,前面的时间点都补 0:

makefile 复制代码
时间      原始值    补齐后     差值
15:55     (无)      0          -
15:56     (无)      0          0
15:57     (无)      0          0
15:58     1         1          1  ← 第一次出现的增量

十一、与 Grafana 对比

维度 Grafana + increase() 本方案
Counter 首次出现 显示 0(看不出来) 显示 1(清晰波峰)
学习成本 需要懂 PromQL 点击菜单即可
Dashboard 配置 手动配置 Panel 自动生成
适用场景 通用监控平台 业务告警可视化

Grafana 的优势:灵活、强大、通用

本方案的优势:简单、直观、符合业务直觉


十二、实践建议

1. 何时使用这个方案?

适合:

  • 告警系统的指标可视化
  • 业务团队日常巡检
  • 非技术人员查看监控

不适合:

  • 需要复杂 PromQL 查询(rate、histogram_quantile)
  • 高度自定义图表布局
  • 专业运维团队的深度分析

2. 性能优化建议

go 复制代码
// 1. 限制数据点数量
if pointCount > 180 {
    step = calculateAdaptiveStep(timeRange, 180)
}

// 2. 缓存查询结果
cache.Set(cacheKey, result, 30*time.Second)

// 3. 并发查询多个指标
var wg sync.WaitGroup
for _, metric := range metrics {
    wg.Add(1)
    go queryMetric(metric)
}

3. 前端渲染优化

javascript 复制代码
// 虚拟滚动(图表数量 > 20)
<virtual-list :data="metrics" :item-height="400">
  <template v-slot="{ item }">
    <Chart :data="item" />
  </template>
</virtual-list>

// 懒加载(点击展开才渲染)
<el-collapse>
  <el-collapse-item v-for="metric in metrics">
    <Chart v-if="expanded" :data="metric" />
  </el-collapse-item>
</el-collapse>

十三、总结

核心解决的问题

Prometheus 的 increase() 无法展示 Counter 首次出现时的值 → 通过计算相邻差值解决

技术亮点

  1. 后端做重活:指标类型识别、差值计算、数据补齐
  2. 前端很轻松:接收数据、转时间、渲染图表
  3. 配置即生效:新增指标关联到指标组,立即可视化

与上一篇的关系

  • 上一篇:解决了"指标分组 + 动态菜单"问题
  • 本篇:解决了"Counter 可视化"问题

两者结合 = 完整的"零配置、业务化"监控方案

适用场景

  • 告警系统指标可视化 ✓
  • 业务团队自助查看 ✓
  • 非技术人员使用 ✓
  • 复杂 PromQL 查询 ✗
  • 高度自定义图表 ✗

效果:业务团队不再问"为什么图上看不到",而是问"能不能多加几个指标"。

相关推荐
颜酱7 小时前
用填充表格法-吃透01背包及其变形
前端·后端·算法
落羽凉笙7 小时前
【Python基础】第2章学习笔记:从Python历史到IPO编程模式,含习题详解
开发语言·笔记·后端·python·学习
timeweaver7 小时前
前端也必看的Docker 核心命令与实战指南
前端·后端·架构
nbsaas-boot8 小时前
Spring Cloud 2025 全面分析与生态边缘化趋势
后端·spring·spring cloud
珠海西格电力8 小时前
零碳园区如何优化能源结构?
运维·人工智能·物联网·架构·能源
北京盟通科技官方账号8 小时前
EC-Master 适配 Xenomai 4:构建 Linux 环境下的硬实时 EtherCAT 主站架构
linux·运维·网络·人工智能·架构·机器人
乾元8 小时前
高可用传输网络的 AI 级联恢复策略——跨域自动化在服务提供商网络中的工程化实现
运维·网络·人工智能·架构·自动化
咖丨喱8 小时前
【解决Miracast出现组形成失败问题】
后端·asp.net
Mintopia8 小时前
🤖 Sentry × AI:让系统监控拥有“大脑”的新时代
运维·人工智能·监控
韩立学长8 小时前
基于Springboot就业岗位推荐系统a6nq8o76(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端