🚗 Counter 计数器 vs Gauge 温度计

监控世界的第一课:每个指标只有两种性格——只增不减的里程表、可上可下的体温计。性格决定看法,看法决定 PromQL 写法。覆盖 核心盘 + 内存盘几乎所有面板的「底层语法」。

Counter 计数器 vs Gauge 温度计:所有指标的两种性格 类比:里程表只滚不退 · 体温计可上可下 · 类型决定 PromQL 写法 Counter 计数器 = 汽车里程表 只增不减 · 名字几乎都带 _total 从 0 开始只往上加,永不回头 累计请求数 · 累计错误数 · 累计锁冲突数…… Gauge 温度计 = 体温计 可上可下 · 直接读当下读数 忙时涨、闲时落,围绕水位呼吸 内存 · 在线人数 · 队列深度 · 连接数…… 看法:配 rate() 看速度 rate(x[5m]) → 平均每秒涨多少 = QPS(下一课) 看法:直接读数 现在多少画多少,套 rate 反而画出无意义的数 ✅ 正确路:Counter 配 rate() 曲线有起伏:高峰低谷一眼可见 c16 HTTP QPS · c12 LLM QPS 都是这个形状 🚨 危险路:Counter 画绝对值 💥 事故现场:siliang_http_requests_total 直接画图 从 0 涨到几百万的斜线,除「在涨」外零信息 正解:套 rate() —— 详见下一课《rate():从里程表算出速度》

💡 一句话理解

监控里的每个数字只有两种性格:Counter(计数器)像汽车里程表,从开机那一刻只往前滚、永不后退,名字几乎都带 _total 后缀;Gauge(温度计)像体温计,可升可降,读数就是「此刻的状态」。请求数、错误数是里程表;内存、在线人数、队列深度是体温计。

性格决定看法:里程表的绝对读数没有意义,要看「每小时跑多快」——所以 Counter 配 rate();体温计直接读数——所以 Gauge 直接画曲线。主图谱 howto 卡把它浓缩成两句话:带 _total 的都是计数器,要配 rate() 看速度;其他多半是温度计,直接读数。类型认错,PromQL 必错,这是 89 张面板通用的第一语法。

🧩 费曼拆解 讲给完全没接触过的小白

想象你租了辆车自驾游。仪表盘上有两个数字:一个是里程表,显示这辆车出厂以来总共跑过 38217 公里;一个是时速表,显示此刻 90 km/h。你从来不会盯着里程表读数安排行程——它只涨不跌,读到天荒地老也只是「越跑越多」;你关心的是时速表:现在快不快、堵车时掉到多少。里程表回答「历史总量」,时速表回答「此刻状态」,两个表谁也替代不了谁。

服务器世界一模一样。siliang_http_requests_total(累计请求数)是里程表:每个 worker 从启动开始只往上加,从不回头;siliang_process_resident_memory_bytes(内存占用)是体温计:忙时涨、闲时落,围绕一个水位上下呼吸。四两 89 张面板里的每个指标,先判断它是里程表还是体温计,才知道该用什么姿势看它。

为什么这是整个课程的第一课?因为看错类型,画出来的图就彻底报废:给里程表直接画绝对值,你得到一条从 0 涨到几百万、永远上扬的斜线——除了证明「服务运行过」之外毫无信息量;反过来给体温计套上 rate(),输出的数字没有任何物理意义。接下来四课讲的 rate、increase、histogram 全都建立在「先认对类型」之上。

类比里的东西系统里对应的东西
汽车里程表(只往前滚)Counter:siliang_http_requests_total、siliang_db_lock_errors_total,名字带 _total,只增不减
体温计(可升可降)Gauge:siliang_process_resident_memory_bytes(内存)、siliang_ws_online_users(在线人数)、队列深度,没有固定后缀
里程表读数没意义,要看时速Counter 配 rate() 算「每秒涨多少」→ QPS、错误率、GC 回收速率
时速表直接读数Gauge 直接画曲线:现在多少就是多少,套加工函数反而画蛇添足
把里程表读数当时速看事故现场:对 _total 画绝对值 → 一条永远上扬的斜线,毫无信息量

🛠 动手验证(4 步,在 Grafana 里亲手做一遍)

  1. 打开主图谱的 c16「HTTP QPS」卡——看它的查询:rate(siliang_http_requests_total[5m]),Counter 配 rate,正是「看时速」的写法。
  2. 再打开 c1「在线用户数」卡——查询是 sum(siliang_ws_online_users{...}):Gauge 直接求和读数,正是「读体温」的写法。
  3. 在 Grafana Explore 手敲 siliang_http_requests_total(不带 rate)——曲线永远上扬:亲眼看到「里程表」本表,顺便理解为什么它没信息量。
  4. 包上 rate() 再跑一次——同一条数据立刻变成有起伏的 QPS 曲线:类型认对 + 看法用对,图才活过来。

🧠 必知必会 看懂本课全部面板的地基

Counter:只增不减
从进程启动开始累计,只往上加。识别标志是 _total 后缀——这是全 Prometheus 世界最可靠的暗号。进程重启会归零重新累计,但「只涨」的方向性不变。
# Counter 示例:开机以来的累计 HTTP 请求总数
siliang_http_requests_total{job="siliang_backend_worker"}
# 读数永远 = 本次启动以来的请求总和,只涨不跌
Gauge:可上可下
一切「此刻状态」都是 Gauge:内存、在线人数、连接数、队列深度、池借出数。没有固定后缀,看语义判断——能「回落到 0 再涨回来」的就是它。
# Gauge 示例:此刻的在线用户数(可上可下)
sum(siliang_ws_online_users{job="siliang_backend_worker"})
# 下班了会掉、上班了会涨——体温计逻辑
后缀速查口诀
_total = Counter(配 rate);_bucket = 直方图水桶(配 histogram_quantile,第 5 课);其余基本都是 Gauge(直接读)。拿到陌生指标先看后缀再动笔。
# 三种后缀对号入座
…_requests_total        # Counter → rate()
…_duration_seconds_bucket # Histogram → histogram_quantile()
siliang_db_pool_connections  # 无后缀 → Gauge 直接读
类型决定函数
Counter 配 rate / increase;Gauge 直接读,或配 max_over_time(抹抖动)、deriv(算斜率)。喂错对象,曲线必废。
# 对号入座:谁配谁
rate(siliang_http_requests_total[5m])              # ✓ Counter 配 rate
max_over_time(siliang_process_resident_memory_bytes[1m]) # ✓ Gauge 抹抖动
Histogram 是第三种
延迟这类「分布值」既不是单个里程表也不是单个体温计,而是一叠累积水桶(每个桶是一个 Counter)。它是第 5 课的主角,这里先记住后缀 _bucket。
# Histogram 的桶:同一指标有一叠 le 不同的序列
siliang_llm_call_duration_seconds_bucket{le="0.5"}
# 意思是:耗时 ≤ 0.5 秒的调用累计有多少个
错误率 = 两个 Counter 相除
错误数和总数都是里程表,各自算出速度再相除就是错误率——这是监控里最常见的「Counter 加工链」。
# 5xx 错误率 = 错误速率 ÷ 总速率
sum(rate(siliang_http_requests_total{status=~"5.."}[5m]))
  / sum(rate(siliang_http_requests_total[5m]))
Gauge 抹抖动用 max_over_time
抓取点天生带毛刺,体温计曲线想更干净,套一层 max_over_time(x[1m]) 取窗口最大值——四两内存盘 RSS、GC、连接池面板的标准写法(第 6 课细讲)。
# m101 进程内存面板口径:1 分钟窗口取最大
max_over_time(siliang_process_resident_memory_bytes[1m])
别忘了 job 过滤
共享端口的 server job 会随机命中 worker 造成双计,四两所有面板都带 job="siliang_backend_worker" 排除它。你手写查询时也要带上,否则数值翻倍。
# 标准姿势:先按 job 过滤,再看 worker
sum(siliang_ws_online_users{job="siliang_backend_worker"})
# 不带过滤 → server job 混入,在线人数虚高一倍

🔗 在四两监控里哪里用到 理论落回你的 89 张图

① HTTP QPS(按 handler)(c16)——Counter 配 rate 的教科书案例:siliang_http_requests_total 这个里程表被算成每秒速度:🟢 HTTP QPS(按 handler)↗

② 在线用户数(c1)——Gauge 直接读数的典型大数字面板:siliang_ws_online_users 可上可下,此刻多少人就是多少:🟣 在线用户数(协作画布去重)↗

③ 进程内存 RSS / VMS(m101)——内存盘的体温计,锯齿/爬升/平台期三种形态全靠直接读曲线判断:📊 进程内存 RSS / VMS(每 worker)↗

④ 数据库锁冲突(近 1h 次数)(c34)——Counter 的另一种看法:配 increase() 算「近一小时总共涨了几次」,锁专项修复的复发哨兵:🔴 数据库锁冲突(近 1h 次数)↗

⑤ GC 各代回收速率(m126)——siliang_python_gc_collections_total 是「垃圾车累计出车次数」里程表,配 rate 变成出车频率:🧹 GC 各代回收速率 ↗

🏭 生产实战 real world

场景 1 · 拿到一个陌生指标,三步定看法

新同事甩给你一个指标名问「这图怎么画」。先看后缀定类型,再定函数,最后再谈配色。

# 第一步:看后缀定类型(以 siliang_ws_connections_total 为例)
siliang_ws_connections_total        # _total 结尾 → Counter 里程表

# 第二步:Counter 配 rate 算速度(c21 连接结果分布的口径)
sum(rate(siliang_ws_connections_total[5m])) by (outcome)
# → 每秒几次敲门 / 几次被拒,曲线有起伏才有信息

# 对照:Gauge 版的兄弟指标直接读(c20 口径)
sum(siliang_ws_connections_active{job="siliang_backend_worker"})
# → 此刻多少条对讲机在线,直接画

判读收尾:同一个「连接」话题,_total 看速率分布、无后缀看当前存量——类型认对,两张图各答各的问题。

场景 2 · 「累计」与「在途」各画一张图

HTTP 流量有两个天然成对的指标:累计完成数(Counter)和此刻在途数(Gauge)。混在一张图里看不懂,分开设两张才清楚。

# 累计完成数:Counter,配 rate 看 QPS(c16 口径)
sum(rate(siliang_http_requests_total[5m])) by (handler)

# 此刻在途数:Gauge,直接读(c18 口径,含 ops 排空参考线)
sum(siliang_http_requests_in_progress) by (method)
  + sum(siliang_ops_http_in_flight)

# 判读:QPS 高但在途平稳 = 处理得快,健康;
#       QPS 平稳但在途持续高位 = 请求在排队,出事了

场景 3 · 错误率:两个里程表相除

「错误多不多」不能看错误绝对数(流量大时错误天然多),要看占比。分子分母都是 Counter,先各自 rate 再除。

# LLM 错误率(c13 口径,clamp_min 防止除零出现 NaN)
rate(siliang_llm_calls_total{outcome="error"}[5m])
  / clamp_min(rate(siliang_llm_calls_total[5m]), 1)

# 判读(c13 卡):<5% 绿 / 5-10% 黄 / >10% 红;
# >10% 多半是 provider 限流(429)或 key 失效——切流量,不是修 bug

场景 4 · 用里程表的「断崖」发现服务重启过

Counter 重启会归零。曲线上一夜之间从几百万砸回 0 的断崖,就是一次重启的铁证——比翻登录记录还快。

# 原始 Counter:断崖 = 重启归零(谁在什么时候重启一目了然)
siliang_http_requests_total{job="siliang_backend_worker"}

# 更正式的问法:启动时间戳 1h 内变了几次手(k5 口径,第 3 课细讲)
count by (service) (changes(container_start_time_seconds[1h]))
# 发布窗口外出现 > 0 = 计划外重启,要查

场景 5 · 把里程表读数翻译成周报语言

老板不问「累计请求数是多少」,问「昨天跑了多少量、这周涨得多快」。里程表读数要加工才能进周报。

# 「昨天总共处理多少请求」→ increase:窗口内总共涨多少(第 3 课)
sum(increase(siliang_http_requests_total[24h]))

# 「最近涨得多快」→ deriv 算斜率(第 6 课,喂 Gauge 用)
deriv(siliang_process_resident_memory_bytes[1h])

# 原则:绝对读数不进周报,加工后的「速度/总量/斜率」才说话

⚠️ 常见坑 pitfalls

坑 1 · 对 Counter 画绝对值 — 症状:请求数曲线从 0 一路涨到几百万,永远上扬,看不出任何节奏。原因:里程表读数本身没有「好坏」,只有差值才有意义。正解:Counter 一律配 rate()/increase() 看速度或增量。
# 错: siliang_http_requests_total                        # → 永远上扬的斜线
# 对: rate(siliang_http_requests_total[5m])             # → 有起伏的 QPS
坑 2 · 对 Gauge 求 rate — 症状:内存曲线套 rate 后出现莫名其妙的锯齿甚至负数。原因:内存是体温计,rate 是给「只增不减」的里程表发明的。正解:Gauge 看趋势用 deriv,看峰值用 max_over_time,直接读数就画原曲线。
# 错: rate(siliang_process_resident_memory_bytes[5m])    # → 无意义的数
# 对: deriv(siliang_process_resident_memory_bytes[1h])   # → 每小时涨多少字节
坑 3 · 把 Counter 归零当成「流量变少」 — 症状:请求数曲线一夜砸到 0,误报「没流量了」。原因:那是进程重启归零,不是没生意。正解:看到断崖先查发布/重启记录,再看重启之后的曲线。
# 错: 看到 _total 掉 0 就喊「服务没人用了」  # → 其实是刚重启
# 对: 断崖后曲线重新从 0 爬升 = 重启;配合 k5 重启面板确认
坑 4 · 把 _bucket 当 Gauge 直接画 — 症状:直方图桶画出来是一叠永远上扬的线,完全看不懂。原因:_bucket 是「累计计数」的一叠桶,单只桶没有直接意义。正解:桶必须交给 histogram_quantile 插值算分位数(第 5 课)。
# 错: siliang_llm_call_duration_seconds_bucket           # → 一叠上扬线
# 对: histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le, model))
坑 5 · 名字不带 _total 就一定是「能直接画」的 Gauge — 症状:把 container_start_time_seconds 画成曲线,看到一条平平的线偶尔跳一下,不知所云。原因:它的值是「本次启动的时间戳」,本质是状态量,画绝对值没意义。正解:用 changes() 数它变了几次手 = 重启次数(k5 口径)。
# 错: container_start_time_seconds                      # → 平线+跳变,看不懂
# 对: changes(container_start_time_seconds[1h])         # → 重启次数
坑 6 · 相除不看 label 对齐 — 症状:池使用率算出来忽大忽小甚至超过 100%。原因:分子分母的 label 集合没对齐,Prometheus 配对错乱。正解:照抄 m115 口径,显式 on(instance, pool) 对齐。
# 错: 借出数 / 容量                        # → 两组线乱配对
# 对: … / on(instance, pool) …              # → 按 worker×池 精确对齐
坑 7 · 手写查询忘带 job 过滤 — 症状:自己写的曲线比面板多出一倍、数值翻倍。原因:共享端口的 server job 随机命中 worker 会双计,面板全排除了它,你的临时查询没排。正解:照抄面板的 job="siliang_backend_worker" 过滤。
# 错: sum(siliang_ws_online_users)                        # → 双计翻倍
# 对: sum(siliang_ws_online_users{job="siliang_backend_worker"})

🎓 费曼自测 合上书,能讲出来才算懂

Q1 · 用租车仪表盘,向产品经理解释 Counter 和 Gauge 的区别?

参考答案:仪表盘上有里程表和时速表。里程表(Counter)只往前滚、从不后退,记录的是「历史总量」,比如累计请求数,名字带 _total;时速表(Gauge)可快可慢,显示「此刻状态」,比如在线人数、内存占用。里程表的绝对读数没人关心,要看的是速度——所以 Counter 要配 rate();时速表看一眼就知道现在几码——所以 Gauge 直接读数。

Q2 · 为什么「对 Counter 画绝对值」毫无信息量?怎么改?

参考答案:Counter 从启动起只增不减,画出来必然是一条从 0 涨到几百万、永远上扬的斜线——它只证明「服务运行过」,看不出高峰低谷、看不出故障时刻。改法是套 rate(x[5m]):算「过去 5 分钟平均每秒涨多少」,曲线立刻有了起伏,QPS、错误率全是从这里来的。

Q3 · siliang_db_pool_connections 是哪种类型?你怎么判断?该怎么看?

参考答案:是 Gauge。判断依据:没有 _total 后缀,且语义上「借出的连接」能借能还——可上可下就是体温计。看法是直接读数或套 max_over_time(x[1m]) 抹抖动:四两 m115 的池使用率面板就是分子(checked_out 借出数)除以分母(capacity 容量),再按 on(instance, pool) 对齐。

Q4 · 一个 _total 指标突然掉到 0,最可能发生了什么?接下来看什么?

参考答案:Counter 只增不减,掉 0 几乎必然是进程重启归零重新累计,不是流量消失。接下来看:① 曲线重启后是否恢复爬升;② k5 的 changes(container_start_time_seconds[1h]) 确认重启时间与次数;③ 对照发布记录——发布窗口内的重启属预期,窗口外的是计划外重启,要查。

📚 课程目录 下一课:rate():从里程表算出速度 →