📊 内存全局水位:先量体温(内存盘 3 图)

内存排查第一步永远是量体温定罪:RSS 曲线的形态三分法(锯齿 / 爬升 / 平台期),加上 GC gen2 这只比体温更早异常的预警器。覆盖内存盘 📊 全局水位组 3 张面板(m100–m102)· 仪表盘 siliang-memory。

每 30s 抓取 查询 触发告警 预警更早 内存全局水位 —— 先量体温定罪:RSS 形态三分法 + GC gen2 预警器 backend worker ×4 分店 · 每店一套 独立内存 · 独立 GC instance 253-backend-3 Prometheus 每 30s 抓取 /metrics instance 规则见 m100 server job 双计已排除 面板 m101 · RSS / VMS max_over_time(rss[1m]) 每 worker 一条线 VMS 虚高,仅参考 ① 锯齿 = 健康(用完就还) 涨跌交替,围绕中线抖 ② 冲高后平台期 = 常态 allocator 留着复用,重启才还 ③ 线性爬升 = 真泄漏 deriv(RSS[1h]) > 5MB/h 面板 m100 · 监控口径与告警规则(先读我) · instance = <机器>-<服务>-<worker 序号>,如 253-backend-3 —— 顶部 HOST 变量按机器前缀下钻 · 共享端口的 server job 随机命中 worker 会双计数 —— 所有面板已排除 · 池面板「两张卡」用法:使用率卡看趋势告警 + 绝对值卡看离上限多远 · 5 条核心告警(其中三条):deriv(RSS[1h])>5MB/h · WS 状态字节>50MB · 沙箱跟踪>0 · 判定心法:线性爬升 = 真累积(查泄漏);平台期 = allocator 不归还(常态) 面板 m102 · GC 各代待回收对象数 —— 泄漏预警器 · gen0 / gen1 周转快,上下波动 = 垃圾站日常量,正常 · gen2(老物件区)存量应基本稳定 —— 持续爬升 = 大对象赖着不走 · 预警器:比 RSS 涨得更早(对象还活着时 RSS 才开始涨) · 实锤互证:gen2 爬升 + gen2 回收速率停滞(面板 m126) 事故现场 · 内存盘头号告警 deriv(siliang_process_resident_memory_bytes[1h]) > 5MB/h 且持续 = 真累积,马上下钻找凶手 OOM 杀进程不打招呼不留遗言,日志常是空白 —— 事后日志靠不住,曲线不会说谎 读图顺序:m100 定口径 → m101 看形态定罪 → m102 提前预警 → 头号告警触发后按本课后半程下钻进程资源 / 在途 / 哨兵三组面板

💡 一句话理解

去医院看病,护士不问病史先给你夹体温计——内存排查也一样:m101 这张 RSS 曲线就是每个 worker 的体温计,先看涨没涨、什么形态,再谈别的。形态只有三种:锯齿(涨跌交替)是健康;冲高后走平台期是 allocator 不归还的常态;只有线性爬升永不回落才是真泄漏。

而 m102 的 gen2 待回收对象数是"炎症指标":它比体温(RSS)升得更早——对象还赖在内存里活着的时候,RSS 才刚开始涨,垃圾站里已经先堆出苗头。m100 则是这本体检手册的使用说明书:instance 规则、server job 双计已排除、两张卡用法和 5 条核心告警,全在这一张卡里。

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

想象你是急诊科医生。病人被抬进来(内存告警响了),你的第一动作不是开颅,是量体温。m101 就是体温计:backend 的 4 个 worker 每人一条 RSS 线,RSS = 这个进程此刻真实占着的物理内存(类比钱包里实际花的钱,不是信用卡额度——那是 VMS,虚高,只做参考)。

体温曲线有三种长相,只有一种是病。锯齿:升升降降来回抖——程序用了内存、用完还回去,这是新陈代谢,健康。平台期:冲高之后横着走不回落——这不是病,是 Python 内存分配器的"吝啬":用完的内存它自己留着复用,不退给系统,就像生意高峰雇的服务员高峰过后不当天辞退,留着排班。线性爬升:一条斜线只涨不跌——每个周期都漏一点、永不归还,这就是真泄漏,要去下面几组面板抓凶手。

最后是预警器。化验单上的炎症指标往往在发烧之前就异常了——m102 的 gen2 待回收对象就是这只指标。Python 的垃圾回收按年龄分三区,gen2 是"老干部区"平时很少扫;它的存量本该稳定,一旦持续爬升,说明有一批大对象赖在老年代不走(还有引用拽着,GC 合法地不敢收)。这比 RSS 上涨出现得更早——所以它是预警器,不是验尸报告。

类比里的东西系统里对应的东西
体温计读数m101 RSS:进程真实占用的物理内存(Gauge 温度计,直接读数);VMS 是"信用卡额度",虚高仅参考
体温曲线三种长相形态三分法:锯齿=健康 / 平台期=常态 / 线性爬升=真泄漏——第二问永远是"斜着涨还是横着走"
高峰过后不辞退服务员allocator 不归还:内存留着复用、重启才真正归还,看起来高但不是失控
每小时体温升幅deriv(RSS[1h]) 算斜率;内存盘核心告警口径:>5MB/h = 真累积
先于发烧的炎症指标m102 gen2 待回收持续爬升 = 泄漏早期信号,比 RSS 更早,是预警器
挂号须知m100 口径卡:instance 命名规则、server job 双计已排除、池面板两张卡用法、5 条核心告警

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

  1. 先读 m100 口径卡——记住 instance = <机器>-<服务>-<worker 序号>,以及"server job 双计已排除",后面所有图都不被骗。
  2. 打开 m101,把时间窗拉到 6h——对照右图判断形态:锯齿 / 平台期 / 爬升,先定性再谈数值。
  3. 拖回最近一次发布的时间点——发布后 RSS 冲高然后横着走 = 平台期常态;冲完继续斜着涨才是问题。
  4. 看 m102 的 gen2 线——平稳=没事;斜着涨=预警器响了,对照 m126 的 gen2 回收速率是否停滞互证。

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

RSS 是温度计
RSS 可上可下,是 Gauge 不是 Counter——直接读数,配 rate 毫无意义。
# 错:Gauge 求 rate;对:直接读 / 用 deriv 看斜率
max_over_time(siliang_process_resident_memory_bytes[1m])
VMS 虚高仅参考
VMS 是"登记要 100 间房",RSS 才是"真住 3 间"。判断内存问题一律看 RSS,别拿 VMS 报警。
# m101 双线:RSS 主看,VMS 参考
…_resident_memory_bytes  # RSS:真实物理占用
…_virtual_memory_bytes   # VMS:几乎永远虚高
max_over_time 抹抖动
抓取是每 30s 瞄一眼的采样,瞬时值会抖;[1m] 窗口内取最大值,曲线更干净——这是 m101 的主口径。
# m101 口径:1 分钟窗口内最大值
max_over_time(siliang_process_resident_memory_bytes[1m])
三形态判读
看内存图不看单点数值,看形状:锯齿=用完就还;平台期=allocator 留着复用;线性爬升=真泄漏。
# 拉长窗口看形态,而不是盯最新一个点
锯齿 ✅   平台期 😐   线性爬升 🚨 # 只有爬升是病
deriv 算斜率
deriv 对 Gauge 求导:每小时涨多少字节。内存盘头号告警就是它。
# 核心告警口径:真累积的量化标准
deriv(siliang_process_resident_memory_bytes[1h]) > 5e6
  # 5MB = 5 * 1024 * 1024 ≈ 5.24e6,口径卡记 5MB/h
gen2 预警器
gen0/1 周转快波动正常;gen2 存量应稳定,持续爬升 = 大对象赖在老年代不走,比 RSS 更早报警。
# m102 口径:按代拆开,盯 generation=2
max_over_time(siliang_python_gc_pending_objects{generation="2"}[1m])
instance 身份证
每条线 = 一个 worker:<机器>-<服务>-<序号>(如 253-backend-3);server job 双计已排除,自建查询要带 job 过滤。
# 唯一可信口径(m100 规则)
{job="siliang_backend_worker"}   # 不带过滤 = 混入双计
两张卡用法
内存盘的成对面板:使用率卡看趋势与告警,绝对值卡看离天花板多远——连接池面板(m115/m116 等)按这个用法读。
# 使用率 = 借出 ÷ 藏书(80% 黄 / 95% 红)
…connections{state="checked_out"} / …capacity

📋 逐面板精讲 3 张图一张不落

📋 m100 · 监控口径与告警规则(先读我)

它是什么:整张内存盘的使用说明书。它写清了三件事:instance 命名规则(<机器>-<服务>-<worker 序号>,共享端口的 server job 会双计、已全部排除);池面板"两张卡"的用法(使用率卡看趋势告警 + 绝对值卡看上限);以及 5 条核心告警规则。最重要的是那句判定心法:线性爬升=真累积(查泄漏);平台期=allocator 不归还(常态)。

回答什么问题:这张盘的数怎么读?哪些告警是核心?不回答它,后面每张图都可能被误读。

✅ 说明卡没有健康数值——把两条心法读进去,它就完成使命了

🚨 不读口径卡的"危险":把平台期当泄漏乱重启(重启只是把证据清零),或拿 VMS 当 RSS 报警。动作:先记住心法再往下看图

# 口径卡里最著名的一条核心告警(m100 → 头号告警)
deriv(siliang_process_resident_memory_bytes[1h]) > 5MB/h

联动:主图谱 m100 ↗ · 相关课程:泄漏判定心法 · job / instance / label

📊 m101 · 进程内存 RSS / VMS(每 worker)

它是什么:每个 worker 的体温计,本盘的主面板。RSS = 物理内存真实占用(主看这条);VMS = 虚拟地址空间,登记了不等于真给了,几乎永远虚高、仅做参考。图上每个 worker 一条线,用 max_over_time(…[1m]) 抹掉抓取瞬间的抖动,按机器 repeat 成对展开。

回答什么问题:内存到底涨没涨?是病(线性爬升)还是常态(平台期)?——先量体温,再定罪。

✅ 锯齿形(涨跌交替)健康;冲高后平台期 = allocator 不归还,常态、重启才还

🚨 线性爬升永不回落 = 真泄漏。动作:马上下钻进程资源 / 在途 / 哨兵三组面板找凶手,并用 deriv(RSS[1h]) 量化爬升速度

# 面板 PromQL(照抄主图谱口径)
max_over_time(siliang_process_resident_memory_bytes[1m])
  # VMS 参考线:…_virtual_memory_bytes,别拿它报警

联动:主图谱 m101 ↗ · 相关课程:RSS / VMS / working_set · max_over_time 与 deriv

📈 m102 · GC 各代待回收对象数

它是什么:垃圾回收站三个区各堆了多少"待处理垃圾",按 generation=0/1/2 拆线。gen0 新生区、gen1 中年区周转快,上下波动是日常量;gen2 是老干部区,平时很少扫,存量本该基本稳定。

回答什么问题:泄漏有没有比 RSS 更早的信号?——有:gen2 待回收持续爬升,说明大对象赖在老年代不走。

✅ gen0/1 波动属正常周转;gen2 平稳低位

🚨 gen2 持续爬升 = 泄漏早期信号,比 RSS 涨得更早(是预警器)。动作:对照 m126 的 gen2 回收速率——"爬升 + 停滞"两条证据合在一起就是实锤

# 面板 PromQL(照抄主图谱口径,三区各一条线)
max_over_time(siliang_python_gc_pending_objects{generation="0/1/2"}[1m])

联动:主图谱 m102 ↗ · 相关课程:Python GC 分代回收 · 进程资源四件套(m126 互证)

🏭 生产实战 real world

场景 1 · 把头号告警落成规则(YAML 可直接抄)

内存盘最核心的告警:RSS 每小时涨超 5MB 且持续——它抓的是"真累积",锯齿和平台期都不会触发。

# 告警规则:RSS 线性爬升(口径来自内存盘 m100 口径卡)
groups:
- name: siliang-memory-waterline
  rules:
  - alert: BackendRssSustainedGrowth
    expr: deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h]) * 3600
          > 5 * 1024 * 1024   # deriv 单位是 字节/秒,×3600 换算成 字节/小时,阈值 5MB/h
    for: 30m         # 持续 30 分钟才报:锯齿和平台期天然被排除
    labels:
      severity: warning

判读收尾:触发后第一步是回 m101 看形态,确认是"斜线"而不是把偶发冲高误算成了斜率。

场景 2 · 三形态判读 SOP(先定性,再定量)

值班时看到内存高,两步定罪:先看形状,再算斜率。

# 第 1 步:m101 主口径看形态(拉 6h 窗口)
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])
# 第 2 步:量化斜率(内存盘核心告警口径)
deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h])
# 判读:
#   涨跌交替、deriv 在 ±5MB/h 内来回穿 0  → 锯齿,健康
#   冲高后横走、deriv ≈ 0                → 平台期,常态别动
#   deriv 稳定为正且 >5MB/h              → 真累积,进入下钻

场景 3 · 排除 VMS 误报

有人看 VMS 那条线"好几个 G 好吓人"就报警——VMS 是登记额度,几乎永远虚高,报警只会狼来了。

# 对比两条线,体会虚高(m101 双线口径)
max_over_time(siliang_process_resident_memory_bytes[1m])   # RSS:报警只认它
max_over_time(siliang_process_virtual_memory_bytes[1m])    # VMS:参考,不告警
# 正解:所有内存告警表达式一律基于 resident_memory_bytes

场景 4 · gen2 预警提前量:抢在 RSS 之前动手

gen2 爬升比 RSS 上涨出现得更早——用它把"发现泄漏"的时点往前挪。

# 预警:gen2 待回收持续爬升(m102 口径,盯老年代)
max_over_time(siliang_python_gc_pending_objects{generation="2", job="siliang_backend_worker"}[1m])
# 互证 1:gen2 回收速率应同步停滞(m126 口径)
sum by (generation) (rate(siliang_python_gc_collections_total[5m]))
# 互证 2:RSS 还没动?说明抓得早——先去"大对象在途"(m106/m107)找嫌疑人
max_over_time(siliang_process_resident_memory_bytes[1m])
# 判读:gen2 爬升 + gen2 速率 ≈ 0 + RSS 尚平稳 = 最早的干预窗口

场景 5 · 发布窗口内的 RSS 冲高:对照平台期心法

发布后流量回放、缓存重建,RSS 常冲高一截——先判断它停不停,再决定要不要半夜爬起来。

# 发布后观察 RSS(m101 口径),等 1~2 个小时窗
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])
# 判读:冲高 → 横走(deriv≈0)= allocator 留着复用,常态,重启才还,别动
deriv(siliang_process_resident_memory_bytes[1h])
# 冲高 → 继续 +5MB/h 以上斜着涨 = 真累积,按场景 2 SOP 下钻

⚠️ 常见坑 pitfalls

坑 1 · 拿 VMS 报警 — 症状:VMS 曲线几个 G,看着吓人就告警。原因:VMS 是虚拟地址空间"登记额度",几乎永远虚高。正解:判断内存问题一律看 RSS,VMS 仅参考。
# 错: alert on …_virtual_memory_bytes
# 对: deriv(…_resident_memory_bytes[1h]) > 5MB/h
坑 2 · 把平台期当泄漏,一高了就重启 — 症状:RSS 冲高横走,值班直接重启"释放内存"。原因:那是 allocator 留着复用的常态,重启反而把证据和复用池一起清零。正解:平台期不是病,只有线性爬升才查。
# 错: RSS 高 → 立刻重启
# 对: deriv(RSS[1h]) ≈ 0 → 平台期,观察即可
坑 3 · 把锯齿当泄漏 — 症状:看到曲线在涨就喊泄漏。原因:健康代谢本来就是涨涨跌跌。正解:拉长窗口看整体形态,锯齿=用完就还。
# 错: 曲线涨了两格就升级事故
# 对: 拉 6h:锯齿 / 平台期 / 爬升,三选一再定罪
坑 4 · 对 RSS 求 rate — 症状:想看"内存增长速度"就写了 rate()。原因:RSS 是 Gauge,rate 只配 Counter。正解:Gauge 求斜率用 deriv。
# 错: rate(siliang_process_resident_memory_bytes[5m])
# 对: deriv(siliang_process_resident_memory_bytes[1h])
坑 5 · 自建图不带 job 过滤 — 症状:自己写的面板数值比官方面板大一截。原因:共享端口的 server job 随机命中 worker 会双计数(m100 已写明官方面板全部排除)。正解:查询一律带 {job="siliang_backend_worker"}。
# 错: max_over_time(siliang_process_resident_memory_bytes[1m])
# 对: max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])
坑 6 · 只看单点数值不看形态 — 症状:"现在 3G 正常啊"而错过正在爬升的斜线。原因:单点是照片,形态才是录像。正解:第二问永远是"斜着涨还是横着走"。
# 错: 只看当前值 vs 阈值
# 对: max_over_time(…[1m]) 拉 6h 看形状
坑 7 · gen0/1 波动当泄漏 — 症状:m102 上 gen0 线大起大落就报警。原因:新生区周转快,波动是垃圾站日常量。正解:只盯 gen2 的持续爬升。
# 错: pending_objects 只要涨就报
# 对: max_over_time(…{generation="2"}[1m]) 持续爬升才报
坑 8 · deriv 窗口用 [5m] — 症状:deriv(RSS[5m]) 曲线抖成毛线团、频繁误报。原因:窗口太短,抓取抖动没被抹平。正解:告警口径用 [1h],与 m100 一致。
# 错: deriv(RSS[5m]) > 5MB/h   # → 抖动误报
# 对: deriv(RSS[1h]) > 5MB/h + for: 30m

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

Q1 · RSS 曲线的三种形态,哪种是病?另外两种为什么不是?

参考答案:只有线性爬升(只涨不跌的斜线)是真泄漏。锯齿是健康——用了就还,新陈代谢;平台期是 allocator 不归还——Python 的内存分配器把用完的内存留着复用、不退给系统,重启才真正归还,看起来高但不是失控。判定心法:线性爬升=真累积(查泄漏),平台期=常态(别慌)。

Q2 · 为什么说 m102 的 gen2 是"预警器",而 m101 的 RSS 是"验尸报告"?

参考答案:gen2 待回收持续爬升说明有一批大对象赖在老年代不走(还有引用拽着,GC 合法地不敢收)——这发生在"对象还活着"的阶段,比 RSS 上涨出现得更早,所以能在体温升起来之前动手。等 RSS 明显线性爬升时,泄漏往往已经积累了一段时间,更像事后验尸。两条证据合起来(gen2 爬升 + gen2 回收速率停滞)才是实锤。

Q3 · 同事看到 RSS 高位横着走就要重启,你用什么理由拦住他?

参考答案:横着走 = deriv≈0 = 平台期,是 allocator 把用完的内存留着复用的常态,不是泄漏。重启唯一的效果是把复用池和现场证据一起清零,下次流量来还会再冲高一遍。正确动作是 deriv(RSS[1h]) 确认斜率接近 0,然后观察;只有持续 >5MB/h 的斜线才进入排查。

Q4 · 内存盘 5 条核心告警里最著名的一条是什么?怎么读它?

参考答案:deriv(RSS[1h]) > 5MB/h——对每 worker 的 RSS 求 1 小时斜率,每小时净涨超 5MB 且持续(for 一段时间)就报,它专门抓"真累积",锯齿和平台期天然不触发。口径卡还列出另外两条:WS 状态字节 >50MB、沙箱跟踪 >0(防回退哨兵)。触发后按本课后半程下钻:进程资源 → 大对象在途 → 泄漏哨兵。

← 上一课:🔴 数据库锁冲突:抢座位吵架 📚 课程目录 下一课:🧵 进程资源四件套 →