📉 max_over_time 与 deriv:抹抖动与算斜率

Gauge 抓取点天生带毛刺:max_over_time 取窗口最大值负责「看得清」,deriv 拟合斜率负责「判得准」。覆盖 内存盘 m100 / m101 / m102 / m108 / m115 等面板 · 核心告警 deriv(RSS[1h]) > 5MB/h 的原理课。

max_over_time 抹抖动 · deriv 算斜率 类比:一分钟内的最高体温 vs 体温的上升速度(每小时涨几度) 原始抓取点(Gauge) Prometheus 每 15/30s 抓一个点 单点之间有抖动(毛刺) max_over_time(x[1m]) 取窗口内最大值 → 抹平毛刺 deriv(x[1h]) 线性拟合斜率 → 每小时涨多少字节 曲线更干净 m101 RSS · m102 GC · m115 池在用 斜率 = 累积速度 内存告警核心判据(m100 口径卡) ✅ 健康路:锯齿 + deriv ≈ 0 deriv ≈ 0:每小时净涨 ≈ 0,用完就还 🚨 危险路:线性爬升 + deriv 高位 线性爬升,永不回落 💥 事故现场:内存告警触发 deriv(RSS[1h]) 大于 5MB/h 且持续 每小时涨超 5MB = 真累积 先查在途面板,再查泄漏哨兵 为什么 deriv 重要:rag 曾因 LibreOffice 大文件被 OOM 杀——5MB/h 告警让你在顶到限额之前就动手 观察面板:m100 口径卡 · m101 RSS 形态 · m108 泄漏哨兵

💡 一句话理解

护士每 15 秒给你量一次体温,单次读数总有毛刺(刚喝过热水、刚打了个冷颤)——max_over_time(x[1m]) 就是「取这一分钟里的最高体温」,把抓取瞬间的抖动抹平,曲线立刻干净好读。而医生真正关心的往往不是「现在几度」,而是「还在往上烧吗、烧得多快」——deriv(rss[1h]) 就是「过去 1 小时体温的上升速度」。

一个负责看得清(抹抖动定形态),一个负责判得准(算斜率定量)。四两内存盘的核心告警 deriv(RSS[1h]) > 5MB/h 就是靠后者抓「真累积」:每小时涨超 5MB 且持续,说明内存在被真吃掉,而不是正常的锯齿波动。

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

想象住院量体温。护士每 15 秒来读一次体温计(Prometheus 每 15/30 秒抓一个点),把读数记在本子上。你把本子上的数画成曲线——毛毛躁躁上下抖,因为你刚喝了口热水读数就高一点,手凉了一下读数就低一点。这不代表病情在抖,只是测量本身有噪声。怎么办?看「一分钟里的最高读数」:max_over_time(x[1m]) 在每个时刻往回看 1 分钟的窗口,取窗口里的最大值——毛刺被抹平,趋势一眼可见。内存盘的 RSS 面板(m101)、GC 待回收面板(m102)、连接池使用率面板(m115)都是这么干的。

第二步是定量。医生查房时不问「现在几度」,他问「和两小时前比,烧退了还是更高了?」——也就是斜率。deriv(x[1h]) 对过去 1 小时窗口内的所有点做一次「直线拟合」,算出这条直线的坡度:正数在涨、负数在降、绝对值越大涨得越猛。对内存来说,斜率就是「每小时涨多少字节」——这是唯一能把「真泄漏」和「正常波动」区分开的数字。

两条路一对比就懂:健康路是锯齿形——处理请求时内存涨上去,干完活 GC 收回来,一小时的净涨幅约等于零,deriv ≈ 0,不告警;危险路是线性爬升——每小时稳定涨超 5MB,一天就是 120MB,迟早顶到容器限额被 OOM 枪毙。所以告警不看「现在内存多少」(高位平台期可能完全健康),只看「是不是在持续爬」。

类比里的东西系统里对应的东西
护士每 15 秒量一次体温Prometheus 每 15/30s 抓一个 Gauge 数据点(核心盘 15s、内存盘 30s)
一分钟内的最高体温max_over_time(x[1m]):窗口内取最大,抹掉抓取毛刺(m101/m102/m115 的标准写法)
体温上升速度(度/小时)deriv(rss[1h]):对窗口内点做线性拟合,得出累积斜率
持续低烧且越烧越高deriv > 5MB/h 持续 = 真累积,触发内存告警(m100 口径卡的 5 条核心告警之一)
烧到 39 度但稳定不升平台期:deriv ≈ 0,allocator 留着内存复用,不是泄漏(详见泄漏判定课)

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

  1. 看原始曲线——查询 siliang_process_resident_memory_bytes{job="siliang_backend_worker"},把时间窗缩到 15m:看到每条 worker 线毛毛躁躁在抖。
  2. 套上抹抖动——改成 max_over_time(siliang_process_resident_memory_bytes[1m]):曲线立刻变干净,这就是 m101 面板的真实写法。
  3. 换算斜率——再改成 deriv(siliang_process_resident_memory_bytes[1h]):数值变得很小,健康 worker 的线在 0 附近上下抖(正负交替)。
  4. 找一台在爬升的 worker——如果某条线的 deriv 持续为正且数字不小,对上 m100 口径卡的核心告警——那就是「真累积」,去在途面板和哨兵面板找嫌疑人。

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

max_over_time 只喂 Gauge
它取「窗口内最大值」,只对可上可下的温度计(Gauge)有意义。内存、GC 待回收、池借出数、协程数全是 Gauge——所以四两内存盘几乎每张曲线都这么写。
# m101 进程内存面板的口径:1 分钟窗口取最大
max_over_time(siliang_process_resident_memory_bytes[1m])
窗口大小的取舍
[1m] 刚好抹掉抓取抖动又不迟钝,是曲线显示的默认口径;哨兵类面板用 [5m]/[10m] 更稳(如 m108 沙箱哨兵用 5m、m124 MiniMax 登记簿用 10m)。窗口越大越平滑,但也越「钝」。
# 哨兵面板口径:5m 窗口,非零即违规
max_over_time(siliang_sandbox_tracked_threads[5m])
deriv 是斜率不是当前值
deriv 对窗口内的点做线性拟合,输出的是「变化快慢」(拟合坡度);内存告警按「每小时涨超 5MB」判读。它回答的是「往哪走、走多快」,不是「现在多少」。
# 斜率 = 累积速度;告警口径:持续 > 5MB/h(以 m100 口径卡为准)
deriv(siliang_process_resident_memory_bytes[1h])
deriv 喂 Gauge,rate 喂 Counter
rate 专门处理「只增不减 + 重启归零」的里程表,会自动补齐 Counter 重置的断崖;deriv 处理的是温度计,直接拟合。两者喂错了对象,曲线都无意义。
# 错配演示:Gauge 求 rate 毫无意义
rate(siliang_process_resident_memory_bytes[5m])  # ✗
deriv(siliang_process_resident_memory_bytes[1h]) # ✓ 斜率
为什么告警窗口用 1h
短窗口的 deriv 会被一次大对象冲高骗到(比如处理一个 200MB 视频让 RSS 冲一下又回落);1h 窗口拟合出来的是「这一小时整体在不在爬」,专抓持续性,不抓单次冲高。
# m109 字节分布面板解释了冲高:处理 200MB 视频内存必先涨 200MB+
# 1h 窗口的 deriv 冲高后回落 = 正常;持续为正 = 真累积
负斜率也是信息
deriv < 0 = 内存在回落:可能是大活干完了、GC 收割了,或刚发布重启。发布后确认 deriv 转负,是「新版本没有泄漏」的快速旁证。
# 发布后看一眼:斜率转负 = 新版本把内存还回来了
deriv(siliang_process_resident_memory_bytes[1h]) # 负数 = 回落
同族函数一起记
max_over_time / min_over_time / avg_over_time 是一家人。四两面板选 max,因为抓取瞬间可能恰好取到一个偏低的点,取最大值更接近真实水位。
# 一家三口:取最大 / 取最小 / 取平均
max_over_time(x[1m])  # 面板主流口径
avg_over_time(x[1m])  # 想看均值时用
别忘了 job 过滤
共享端口的 server job 会随机命中 worker 造成双计,四两所有面板查询都带 job="siliang_backend_worker" 排除——你手写查询时也要带上,否则曲线数量翻倍。
# 标准姿势:先按 job 过滤,再按 instance 看 worker
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])

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

① 进程内存 RSS / VMS(m101)——内存盘的体温计,曲线就是 max_over_time(...[1m]) 抹过抖动的;判断涨没涨、什么形态全靠它:📊 进程内存 RSS / VMS ↗

② GC 各代待回收对象数(m102)——gen2 待回收持续爬升是比 RSS 更早的泄漏预警器,同样用 max_over_time[1m] 让爬升看得清:🧹 GC 各代待回收对象数 ↗

③ 数据库连接池使用率(m115)——分子分母都套 max_over_time[1m] 再相除,抹掉抓取瞬间的抖动,避免毛刺造成假告警:🔌 数据库连接池使用率 ↗

④ 泄漏哨兵(m108)——「应恒 0」的防回退警报器用 max_over_time[5m]:窗口内出现过非零就揪出来,抖动逃不掉:🚨 沙箱跟踪与清理哨兵 ↗

⑤ 监控口径与告警规则卡(m100)——内存盘 5 条核心告警里最著名的一条就是 deriv(RSS[1h]) > 5MB/h,本课讲的正是它:📖 监控口径与告警规则(先读我)↗

🏭 生产实战 real world

场景 1 · 曲线毛刺太多,看不出到底涨没涨

原始抓取点抖来抖去,肉眼判断不了趋势。两步走:先 max_over_time 抹平看形态,再 deriv 定量看斜率。

# 第一步:抹毛刺(m101 口径),看形态:锯齿 / 爬升 / 平台期
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])

# 第二步:算斜率,定量回答「每小时涨多少字节」
deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h])

# 判读:deriv 在 0 附近正负交替 = 锯齿健康;
#       持续为正且大 = 真累积,进入排查流程

判读收尾:曲线平滑了、斜率数字说话了,才算「看懂了这张图」。

场景 2 · 给「真累积」写一条告警规则

内存高位本身不是告警理由,「持续爬升」才是。用 deriv 抓持续性,用 for 子句要求它持续足够久才响。

# 内存真累积告警(Prometheus rule,数值口径以内存盘「先读我」卡为准)
groups:
- name: siliang-memory
  rules:
  - alert: BackendRssClimbing
    expr: |
      deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h])
        > 5 * 1024 * 1024   # 5MB/h:每小时涨超 5MB 且持续
    for: 30m               # 持续半小时才响,避免单次冲高误报(时长以口径卡为准)
    labels:
      severity: warning
    annotations:
      summary: "worker {{ $labels.instance }} RSS 持续爬升,疑似真累积"

看到告警先别慌:对照 m101 看形态是爬升还是平台期,再进在途/哨兵面板找凶手。

场景 3 · 区分「平台期」和「真泄漏」这两个双胞胎

两条都停在高位、长得一模一样的曲线,一个健康一个有病。deriv 一眼定案。

# 平台期:冲高后停住(allocator 留着复用)→ 斜率 ≈ 0
deriv(siliang_process_resident_memory_bytes{instance="253-backend-0"}[1h])
# 预期:在 0 附近抖 → 不是泄漏,别动手

# 真泄漏:爬完没停 → 斜率持续为正
deriv(siliang_process_resident_memory_bytes{instance="253-backend-1"}[1h])
# 预期:稳定 > 5MB/h → 按内存盘排查链下钻(在途 → 哨兵 → GC)

场景 4 · 发布后确认新版本内存会回落

发布滚动完成后最怕「新版比旧版更能吃」。盯 30 分钟 deriv,转负就是好信号。

# 发布完成后跑一次:按 instance 看 4 个 worker 各自的斜率
deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h]) by (instance)

# 预期:旧 worker 内存随流量迁走回落(deriv 负),新 worker 平稳;
# 若新 worker deriv 持续为正且大 → 新版本引入泄漏,考虑回滚

场景 5 · 连接池使用率毛刺造成假告警

抓取瞬间恰好赶上高峰,使用率闪到 95% 又立刻回落——直接画会天天狼来了。m115 的口径是分子分母都抹抖动再除。

# m115 数据库连接池使用率口径:分子分母都取 1m 最大值再相除
max_over_time(siliang_db_pool_connections{state="checked_out",job="siliang_backend_worker"}[1m])
  / on(instance, pool)
max_over_time(siliang_db_pool_capacity{job="siliang_backend_worker"}[1m])

# 判读:80% 黄 / 95% 红;贴 100% = 池打满,请求开始排队

⚠️ 常见坑 pitfalls

坑 1 · 对 Gauge 求 rate — 症状:内存曲线套 rate 后出现莫名其妙的锯齿甚至负值。原因:rate 是给「只增不减的里程表」用的,内存是温度计。正解:温度计看斜率用 deriv,里程表看速度用 rate。
# 错: rate(siliang_process_resident_memory_bytes[5m])   # → Gauge 求 rate,曲线无意义
# 对: deriv(siliang_process_resident_memory_bytes[1h])   # → 每小时涨多少字节
坑 2 · deriv 窗口太小满屏噪声 — 症状:deriv(x[1m]) 的输出抖得像股票分时图,没法判读。原因:1m 窗口内点太少,拟合斜率被单点噪声主导。正解:告警和判读用 [1h] 窗口,抓持续性;面板显示才用 [1m] 抹抖。
# 错: deriv(siliang_process_resident_memory_bytes[1m])  # → 噪声放大器
# 对: deriv(siliang_process_resident_memory_bytes[1h])  # → 稳定的累积速度
坑 3 · 被一次冲高骗成「泄漏」 — 症状:刚处理完一个大视频就收到内存爬升告警,工程师白跑一趟。原因:短观察窗把 m109 里的「200MB 文件过手、内存先涨 200MB+」当成了持续累积。正解:看 deriv 是否持续为正(for 30m),冲高会自己回落,真累积不会。
# 错: 看到单点 deriv 大就报警           # → 冲高也响,天天狼来了
# 对: deriv(...[1h]) 持续 > 5MB/h 才响  # → 冲高回落不响,真爬升必响
坑 4 · 给 max_over_time 喂 Counter — 症状:max_over_time(请求数_total[1m]) 画出来永远缓慢单调上扬,毫无信息量。原因:Counter 一直涨,窗口最大值永远是最新值,等于什么都没算。正解:Counter 配 rate/increase,max_over_time 留给 Gauge。
# 错: max_over_time(siliang_http_requests_total[1m])  # → 永远在涨,无信息
# 对: rate(siliang_http_requests_total[5m])           # → 才是 QPS
坑 5 · 相除忘了 on(instance,pool) 对齐 — 症状:使用率算出来忽大忽小甚至超 100%。原因:借出数和容量两组线的 label 集合没对齐,Prometheus 默认配对规则错配。正解:照抄 m115 口径,显式 on(instance, pool)。
# 错: max_over_time(checked_out[1m]) / max_over_time(capacity[1m])        # → 错配
# 对: ... / on(instance, pool) max_over_time(siliang_db_pool_capacity[1m]) # → 按 worker×池 对齐
坑 6 · 把 deriv>0 当告警理由 — 症状:健康时段也频繁收到「内存在涨」的提示,麻木后真告警被忽略。原因:锯齿的上行沿 deriv 就是正的,这是呼吸不是病变。正解:只有「持续为正且大」(超 5MB/h 且持续半小时级)才算信号。
# 错: deriv(rss[15m]) > 0                         # → 锯齿上行沿天天误报
# 对: deriv(rss[1h]) > 5*1024*1024  for: 30m      # → 只抓真累积
坑 7 · 手写查询忘带 job 过滤 — 症状:自己写的曲线比面板多出一倍、数值翻倍。原因:共享端口的 server job 会随机命中 worker 双计,面板全部排除了它,你的临时查询没排。正解:照抄面板的 job="siliang_backend_worker" 过滤。
# 错: deriv(siliang_process_resident_memory_bytes[1h])                  # → 双计
# 对: deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h])

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

Q1 · 用一句话向外行解释 max_over_time 和 deriv 各自干什么?

参考答案:max_over_time 是「取这一分钟里的最高体温」——把测量本身的毛刺抹平,让曲线看得清;deriv 是「过去一小时体温的上升速度」——不管现在几度,只回答还在不在往上烧、每小时涨多少。前者管看得清,后者管判得准。

Q2 · 内存告警为什么用 deriv(RSS[1h]) 而不是「RSS 超过某个值就报」?

参考答案:因为绝对值高不等于有问题——大流量过后 allocator 留着内存复用,RSS 停在高位走平台期是完全健康的常态,按绝对值报警会天天误报。deriv 抓的是「持续爬升」这个真正的病灶:每小时涨超 5MB 且持续,才说明内存被真吃掉了,才需要去查在途面板和泄漏哨兵。

Q3 · deriv > 0 就该半夜爬起来处理吗?

参考答案:不该。健康锯齿的上行沿 deriv 就是正的,处理请求时内存本来就要涨。要同时满足「涨得多(超 5MB/h)」和「持续(1h 窗口、for 30m 不回落)」才算真信号;单点为正、很快回落的多半只是刚搬完一个大文件。

Q4 · max_over_time 应该喂 Counter 还是 Gauge?为什么?

参考答案:只喂 Gauge。Counter 只增不减,窗口内的最大值永远是最新的那个数,等于什么都没算、画出来是一条永远上扬的无信息曲线;Gauge 可上可下,取窗口最大值才有「抹平抓取抖动」的价值。Counter 要看速度请配 rate。

← 上一课:直方图水桶与 topk 📚 课程目录 下一课:QPS·并发·延迟铁三角 →