内存排查第一步永远是量体温定罪:RSS 曲线的形态三分法(锯齿 / 爬升 / 平台期),加上 GC gen2 这只比体温更早异常的预警器。覆盖内存盘 📊 全局水位组 3 张面板(m100–m102)· 仪表盘 siliang-memory。
去医院看病,护士不问病史先给你夹体温计——内存排查也一样: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 条核心告警 |
# 错:Gauge 求 rate;对:直接读 / 用 deriv 看斜率 max_over_time(siliang_process_resident_memory_bytes[1m])
# m101 双线:RSS 主看,VMS 参考 …_resident_memory_bytes # RSS:真实物理占用 …_virtual_memory_bytes # VMS:几乎永远虚高
# m101 口径:1 分钟窗口内最大值 max_over_time(siliang_process_resident_memory_bytes[1m])
# 拉长窗口看形态,而不是盯最新一个点 锯齿 ✅ 平台期 😐 线性爬升 🚨 # 只有爬升是病
# 核心告警口径:真累积的量化标准 deriv(siliang_process_resident_memory_bytes[1h]) > 5e6 # 5MB = 5 * 1024 * 1024 ≈ 5.24e6,口径卡记 5MB/h
# m102 口径:按代拆开,盯 generation=2 max_over_time(siliang_python_gc_pending_objects{generation="2"}[1m])
# 唯一可信口径(m100 规则) {job="siliang_backend_worker"} # 不带过滤 = 混入双计
# 使用率 = 借出 ÷ 藏书(80% 黄 / 95% 红) …connections{state="checked_out"} / …capacity
它是什么:整张内存盘的使用说明书。它写清了三件事:instance 命名规则(<机器>-<服务>-<worker 序号>,共享端口的 server job 会双计、已全部排除);池面板"两张卡"的用法(使用率卡看趋势告警 + 绝对值卡看上限);以及 5 条核心告警规则。最重要的是那句判定心法:线性爬升=真累积(查泄漏);平台期=allocator 不归还(常态)。
回答什么问题:这张盘的数怎么读?哪些告警是核心?不回答它,后面每张图都可能被误读。
✅ 说明卡没有健康数值——把两条心法读进去,它就完成使命了
🚨 不读口径卡的"危险":把平台期当泄漏乱重启(重启只是把证据清零),或拿 VMS 当 RSS 报警。动作:先记住心法再往下看图
# 口径卡里最著名的一条核心告警(m100 → 头号告警) deriv(siliang_process_resident_memory_bytes[1h]) > 5MB/h
联动:主图谱 m100 ↗ · 相关课程:泄漏判定心法 · job / instance / label
它是什么:每个 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
它是什么:垃圾回收站三个区各堆了多少"待处理垃圾",按 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 互证)
内存盘最核心的告警: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 看形态,确认是"斜线"而不是把偶发冲高误算成了斜率。
值班时看到内存高,两步定罪:先看形状,再算斜率。
# 第 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 → 真累积,进入下钻
有人看 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
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 尚平稳 = 最早的干预窗口
发布后流量回放、缓存重建,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 下钻
# 错: alert on …_virtual_memory_bytes # 对: deriv(…_resident_memory_bytes[1h]) > 5MB/h
# 错: RSS 高 → 立刻重启 # 对: deriv(RSS[1h]) ≈ 0 → 平台期,观察即可
# 错: 曲线涨了两格就升级事故 # 对: 拉 6h:锯齿 / 平台期 / 爬升,三选一再定罪
# 错: rate(siliang_process_resident_memory_bytes[5m]) # 对: deriv(siliang_process_resident_memory_bytes[1h])
# 错: max_over_time(siliang_process_resident_memory_bytes[1m]) # 对: max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])
# 错: 只看当前值 vs 阈值 # 对: max_over_time(…[1m]) 拉 6h 看形状
# 错: pending_objects 只要涨就报 # 对: max_over_time(…{generation="2"}[1m]) 持续爬升才报
# 错: deriv(RSS[5m]) > 5MB/h # → 抖动误报 # 对: deriv(RSS[1h]) > 5MB/h + for: 30m
参考答案:只有线性爬升(只涨不跌的斜线)是真泄漏。锯齿是健康——用了就还,新陈代谢;平台期是 allocator 不归还——Python 的内存分配器把用完的内存留着复用、不退给系统,重启才真正归还,看起来高但不是失控。判定心法:线性爬升=真累积(查泄漏),平台期=常态(别慌)。
参考答案:gen2 待回收持续爬升说明有一批大对象赖在老年代不走(还有引用拽着,GC 合法地不敢收)——这发生在"对象还活着"的阶段,比 RSS 上涨出现得更早,所以能在体温升起来之前动手。等 RSS 明显线性爬升时,泄漏往往已经积累了一段时间,更像事后验尸。两条证据合起来(gen2 爬升 + gen2 回收速率停滞)才是实锤。
参考答案:横着走 = deriv≈0 = 平台期,是 allocator 把用完的内存留着复用的常态,不是泄漏。重启唯一的效果是把复用池和现场证据一起清零,下次流量来还会再冲高一遍。正确动作是 deriv(RSS[1h]) 确认斜率接近 0,然后观察;只有持续 >5MB/h 的斜线才进入排查。
参考答案:deriv(RSS[1h]) > 5MB/h——对每 worker 的 RSS 求 1 小时斜率,每小时净涨超 5MB 且持续(for 一段时间)就报,它专门抓"真累积",锯齿和平台期天然不触发。口径卡还列出另外两条:WS 状态字节 >50MB、沙箱跟踪 >0(防回退哨兵)。触发后按本课后半程下钻:进程资源 → 大对象在途 → 泄漏哨兵。