🧠 RSS / VMS / working_set:三种「内存」

酒店类比:VMS = 登记 100 间(虚高只参考)· RSS = 真住 3 间(进程面板主口径)· working_set = 地板上正在摊的(内核 OOM 判定口径)。覆盖 内存盘 m101 / m100 · biz 盘 b9 · 容器盘 k1 / k2。

三种「内存」:登记的 / 真住的 / 地板上的 酒店类比:登记 100 间(VMS 虚)· 真住 3 间(RSS 主口径)· 地板上正在摊的(working_set,OOM 看它) VMS · 虚拟内存 对前台说:可能要住 100 间 登记不等于给房,一张空头支票 虚高,动辄 RSS 的好几倍 · 只做参考 绝不拿来报警 RSS · 常驻内存 实际住着 3 间房 物理内存的真实占用 进程面板主口径(m101 / b9 / m128) 判断内存问题一律看它 working_set · 工作集 地板上摊开正在用的 最近用过、想扔也扔不掉的 容器视角 · 内核 OOM 判定口径 容器盘 k1 / k2 用它 为什么判 OOM 不用 usage_bytes?—— 它把冰箱贴纸也算进去了 page cache:系统顺手把最近读过的文件留在内存(急缺时第一个扔)。usage_bytes 把它算进去 → 虚高;working_set 减掉它 → 与内核 OOM 判定一致 ✅ 正确姿势:各看各的口径 进程层面:看 RSS 的形态(锯齿 / 爬升 / 平台期) 容器层面:看 working_set 离限额多远 VMS:放旁边当参考,永远不报警 例:working_set 3.2G ÷ 限额 10G = 32%,安全 80% 黄 90% 红(容器盘口径) 刻度与告警阈值以容器盘「先读我」卡(k0)为准 🚨 两种翻车:被虚高骗 / 被真枪毙 翻车一:拿 usage_bytes 判 OOM——缓存冲高就误报 翻车二:无视 working_set 贴线——真到枪毙线了没人盯 💥 事故现场:working_set 顶到限额 → memcg OOM SIGKILL 直接枪毙:不打招呼、不留遗言(详见下一课) 误报让人麻木,漏报让人挨打——口径错了两个都沾 一句话带走:进程看 RSS(m101 / b9)· 容器看 working_set(k1 / k2)· VMS 只参考 · 判 OOM 只认 working_set 单位提醒:所有指标都是 bytes,换算 GiB 要除以 1024 的三次方

💡 一句话理解

你去酒店办入住,说「我们团可能要 100 间房」。前台在本子上记下 100 间——这只是登记(VMS,虚拟内存),一间都没真给你;你们实际住了 3 间——这是真占用(RSS,常驻内存);而酒店消防检查看的既不是登记簿也不是入住总数,是你房间地板上摊开正在用的东西(working_set)——地板满到没地方下脚(顶到限额),才会被赶出去(OOM)。

三种口径各管一摊:看进程内存问题(m101、b9)主看 RSS 的形态;判断容器会不会被 OOM 杀(k1、k2)只认 working_set;VMS 永远虚高好几倍,只做参考、绝不报警。混用口径的代价是:要么被虚高的数吓一跳(误报),要么真出事时盯错了表(漏报)。

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

先说「登记 100 间」的 VMS。操作系统给每个进程一个巨大的虚拟地址空间,程序申请内存、映射文件,都只是先在账本上记账,真正的物理内存要等真的读写那块地址才分配——就像酒店前台先记下「可能要 100 间」,实际一间没给。所以 VMS 几乎永远比真实占用大得多,动辄是 RSS 的好几倍。看图时它是那条高高的、没什么起伏的参考线:看看就好,别拿它说事。

再说「真住 3 间」的 RSS——真正被加载进物理内存、此刻正被进程使用的部分。操作系统说「这个进程占了多少内存」,说的就是它。内存泄漏、内存冲高、涨没涨回没落,看的都是它。内存盘 m101、biz 盘 b9、worker 体检单 m128,主线全是 RSS。看它也不是看单点数值,而是看形态:锯齿(用完就还)健康、线性爬升(只进不出)是病、冲高后平台期是分配器留着复用的常态脾气。

最后是「地板」working_set。Linux 会把最近读过的文件顺手缓存在内存里(page cache),像你把菜谱贴满冰箱门——需要时随时能撕,不算真占。容器视角的 usage_bytes 把这些也算了进去,所以虚高;而 working_set 是减掉「可回收部分」之后的值,和内核 memcg 的 OOM 判定口径完全一致。一句话:「容器会不会被枪毙」这个问题,只有 working_set 有资格回答。

类比里的东西系统里对应的东西
登记要 100 间(空头支票)VMS:虚拟地址空间,申请了不等于真给了,虚高只做参考
真住着 3 间房RSS:物理内存真实占用,进程面板(m101/b9/m128)的主口径
地板上摊开正在用的东西working_set:最近用过、扔不掉的部分,内核 OOM 就看它(k1/k2)
冰箱门上的菜谱贴纸(可随时撕)page cache:可回收的文件缓存,急缺时第一个被扔
柜子加地板全算上usage_bytes:含可回收 cache 所以虚高,别拿来判 OOM
消防按地板判定超载memcg OOM 按 working_set 判定:顶到限额就 SIGKILL(下一课)

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

  1. 体会「登记不等于真住」——打开内存盘 m101:同屏看 RSS 和 VMS 两组线,VMS 高出好几倍、几乎水平——这就是那张「100 间的登记单」。
  2. 看进程主线——打开 biz 盘 b9:每个 worker 一条 RSS 线(1 分钟窗口最大值),判断形态:锯齿 / 爬升 / 平台期。
  3. 看容器口径——打开容器盘 k1:working_set 实线 vs 限额虚线,看离天花板多远。(当前 cAdvisor 未部署、容器盘暂无数据——先看图理解口径,部署后这就是日常巡检位。)
  4. 手算一遍使用率——k2 的口径就是 working_set ÷ limit:拿 k1 的两个数除一下,对照 80% 黄 / 90% 红,练出「离枪毙线多远」的直觉。

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

RSS 是进程主口径
「这个进程占多少内存」的标准答案。四两所有进程内存面板都以它为主线,配 max_over_time[1m] 抹抓取抖动。
# m101 主线:每 worker 的 RSS(1 分钟窗口最大值)
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])
VMS 只参考绝不报警
虚拟地址空间是「登记单」,malloc、mmap 都先记账——动辄 RSS 好几倍,拿它报警天天狼来了。
# m101 副线:只做参考对照,永不设告警
max_over_time(siliang_process_virtual_memory_bytes[1m])  # 别拿它判问题
working_set 是 OOM 判定口径
cAdvisor/内核视角的「真占用」:最近确实用过、想扔也扔不掉的部分。容器杀不杀,内核只认它。
# k1 口径:每个容器实际装了多少 vs 盒子多大
sum by (service) (container_memory_working_set_bytes)
  + sum by (service) (container_spec_memory_limit_bytes)  # 一实一虚两条线
usage_bytes 天生虚高
它把 page cache(可回收文件缓存)也算了进去——读过大文件的容器 usage 会冲得很高,但那些内存急缺时马上能被回收。
# 错:拿 usage_bytes 判 OOM → page cache 冲高就误报
# 对:判 OOM 只认 container_memory_working_set_bytes
使用率 = working_set ÷ limit
容器盘 k2 的口径,除法用 clamp_min 兜底防止限额缺失时除零;80% 黄 / 90% 红是这张盘的刻度。
# k2 口径:容器内存使用率
container_memory_working_set_bytes
  / clamp_min(sum by (service) (container_spec_memory_limit_bytes), 1)
形态判读跨口径通用
不管哪个口径,看内存都先看形态:锯齿健康、平台期常态、线性爬升才是病——deriv 是定量武器。
# 形态定量:每小时涨多少字节(内存盘核心告警口径)
deriv(siliang_process_resident_memory_bytes[1h])  # 持续 > 5MB/h = 真累积
三口径对照速记
进程问 RSS,容器问 working_set,两者别互相硬套:4 个 worker 的 RSS 之和不等于容器 working_set(视角不同,后者还涉及 cgroup 层面的统计)。
# 进程视角(每 worker 一条线)
siliang_process_resident_memory_bytes{job="siliang_backend_worker"}
# 容器视角(整盒统计)——两套口径,别相加互证
container_memory_working_set_bytes  # by (service)
容器盘依赖 cAdvisor
working_set / limit 这些指标由 cAdvisor 采集。当前未部署,容器盘整盘无数据——不是坏了,部署后自动生效。
# k 盘暂无数据的原因(口径卡 k0 写明)
# cAdvisor 未部署 → container_memory_working_set_bytes 无采集源

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

① 进程内存 RSS / VMS(m101)——两种口径同屏:RSS 主线看形态,VMS 副线只做参考——「登记 100 间 vs 真住 3 间」的现场:📊 进程内存 RSS / VMS ↗

② 监控口径与告警规则卡(m100)——内存盘使用说明书:instance 规则、两张卡用法、5 条核心告警(含 deriv(RSS[1h]) > 5MB/h)都在这:📖 监控口径与告警规则(先读我)↗

③ biz 进程内存水位(b9)——biz 版 RSS 主线,按容器拆分、每 worker 一条线,形态判读方法完全一样:🟢 biz 进程内存水位(RSS/VMS)↗

④ 容器内存使用 vs 限制(k1)——working_set 实线 vs 限额虚线,一实一虚:使用线贴上限制线 = memcg OOM 倒计时:🟡 容器内存使用 vs 限制 ↗

⑤ 内存使用率(k2)——working_set ÷ limit 的百分比版,80% 黄 / 90% 红,rag(LibreOffice 前科)是头号盯防对象:🟡 内存使用率(working_set / limit)↗

🏭 生产实战 real world

场景 1 · 判断容器离 OOM 还有多远

巡检容器的健康度不是看绝对值,是看「装了多少 ÷ 盒子多大」。使用率超 80% 该留意,超 90% 持续 5 分钟就该告警——那是 OOM 前的最后窗口。

# k2 口径:按 service 算容器内存使用率
sum by (service) (container_memory_working_set_bytes)
  / clamp_min(sum by (service) (container_spec_memory_limit_bytes), 1)

# 绝对值余量:还差多少字节到限额(k1 口径的减法版)
sum by (service) (container_spec_memory_limit_bytes)
  - sum by (service) (container_memory_working_set_bytes)
# 判读:余量除以限额小于 10% 且 deriv 为正 → 立即处理,别等枪响

场景 2 · OOM 前最后窗口的告警规则

90% 是最后防线:给 5 分钟持续时间,滤掉正常冲高的毛刺,抓「爬上去就不下来」的真威胁。

groups:
- name: siliang-container
  rules:
  - alert: ContainerMemNearLimit
    expr: |
      sum by (service) (container_memory_working_set_bytes)
        / clamp_min(sum by (service) (container_spec_memory_limit_bytes), 1) > 0.9
    for: 5m              # 持续 5 分钟才响(口径以 k0 建议为准)
    labels:
      severity: critical
    annotations:
      summary: "{{ $labels.service }} working_set 超 90%:OOM 倒计时"

场景 3 · 进程排查永远从 RSS 形态开始

接到「backend 是不是泄内存了」的反馈,第一动作不是翻 heap dump,是看 RSS 三形态定调:锯齿别慌、平台期是脾气、爬升才立案。

# 第一步:抹抖动看形态(m101 口径)
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])

# 第二步:deriv 定量(真累积 = 持续大于 5MB/h,口径以 m100 为准)
deriv(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1h])

# 判读:deriv 在 0 附近抖 = 呼吸;持续为正且大 = 立案,进在途/哨兵排查链

场景 4 · usage 和 working_set 差了多少「冰箱贴纸」

两者相减就是可回收的 page cache。大文件搬运后这个差值会瞬间拉大——知道它可回收,就不会被 usage 的冲高吓到。

# 可回收部分 = usage - working_set(读文件缓存为主)
sum by (service) (container_memory_usage_bytes)
  - sum by (service) (container_memory_working_set_bytes)

# 判读:usage 高但差值大 = 冰箱贴纸多,撕了就回来,不用动手;
#       working_set 本身高 = 真占用,才需要按限额距离决策

场景 5 · 别拿 VMS 吓自己:一条对照查询

新人常把 VMS 的高位截图发群里喊泄漏。一条查询并排两条线,自己看一遍就终身免疫。

# RSS 与 VMS 同屏对照(m101 的两条线)
max_over_time(siliang_process_resident_memory_bytes{job="siliang_backend_worker"}[1m])
max_over_time(siliang_process_virtual_memory_bytes{job="siliang_backend_worker"}[1m])

# 预期:VMS 恒为 RSS 的数倍且几乎水平 = 登记单,正常;
#       该报警的是 RSS 的 deriv,不是任何一条 VMS 线

⚠️ 常见坑 pitfalls

坑 1 · 拿 VMS 报警 — 症状:「内存用了 30G!」截图发群,全员虚惊。原因:VMS 是登记单不是真占用,虚高数倍是常态。正解:判断内存问题一律看 RSS,VMS 永远只做参考。
# 错: 告警 expr 用 siliang_process_virtual_memory_bytes > 8GB
# 对: deriv(siliang_process_resident_memory_bytes[1h]) > 5MB/h 持续
坑 2 · 拿 usage_bytes 判 OOM — 症状:大文件搬运后 usage 逼近限额,以为要被杀了,结果啥事没有。原因:usage 含 page cache(可随时回收的冰箱贴纸),虚高。正解:判 OOM 只认 working_set,它是内核同款尺子。
# 错: container_memory_usage_bytes / limit > 0.9 就报  # → 误报
# 对: container_memory_working_set_bytes / limit > 0.9  # → 与内核同口径
坑 3 · RSS 正常就断定容器安全 — 症状:每个 worker 的 RSS 都不高,容器却被 OOM 杀了。原因:进程 RSS 与容器 working_set 是两个视角,容器盒里不止你的进程,还有缓存等开销。正解:容器会不会被杀,看 k1/k2 的 working_set 口径,别用进程数相加推算。
# 错: 4 个 worker RSS 加起来 6G < 10G → 「不可能 OOM」
# 对: 看 container_memory_working_set_bytes 对 limit 的距离(k1)
坑 4 · 容器盘无数据当成「坏了」或「健康」 — 症状:有人报修「容器盘全空」,也有人把空盘当「一切正常」。原因:cAdvisor 未部署,整盘无数据——既不是故障也不是健康,是「还没装电表」。正解:记住口径卡(k0)的说明:部署后自动生效,期间容器内存靠进程面板间接盯。
# 错: 「k1 没数据 = 不会 OOM」        # → 没电表不等于没用电
# 对: cAdvisor 未部署 → 过渡期盯 m101/m128 的 RSS + deriv
坑 5 · 忘了单位是 bytes — 症状:「worker 才用了 0.003 内存?」或「3 万 G?!」。原因:所有内存指标单位是字节,直接读原数没有意义。正解:Grafana 面板选 GiB 单位;手算时除以 1024 的三次方。
# 错: 看到 34359738368 以为要爆炸        # → 这是 32GiB 的字节数
# 对: 34359738368 / 1024^3 = 32 GiB → 再对 10G 限额判断
坑 6 · 除法没做 clamp_min 兜底 — 症状:使用率曲线偶尔出现 NaN 或天文数字。原因:某容器限额标签缺失时分母为 0。正解:照抄 k2 口径:分母套 clamp_min(..., 1)。
# 错: working_set / sum by (service) (limit)         # → 分母可为 0
# 对: working_set / clamp_min(sum by (service) (limit), 1)
坑 7 · 三种口径互相硬套 — 症状:「RSS 4G 而 working_set 5G,谁错了?」——谁都没错。原因:口径不同(进程视角 vs 容器 cgroup 视角,统计范围不同)。正解:各答各的问题:进程健康看 RSS 形态,容器生死看 working_set 距离,不互相校验。
# 错: 拿 m101 的 RSS 之和去对 k1 的 working_set 找「差值 bug」
# 对: 两把尺子各量各的:RSS 管进程,working_set 管盒子

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

Q1 · 用酒店类比一句话区分 VMS、RSS、working_set?

参考答案:VMS 是在前台登记「可能要住 100 间」——登记不等于给房,虚高只参考;RSS 是实际住着的 3 间房——真实占用,进程面板主口径;working_set 是房间地板上摊开正在用的东西——最近用过、扔不掉的部分,消防(内核 OOM)按它判定超不超载。

Q2 · 为什么判「容器会不会被 OOM 杀」要用 working_set 而不是 usage_bytes?

参考答案:因为 usage_bytes 把 page cache 也算进去了——系统顺手缓存的文件内容,急缺内存时第一个被扔掉,根本不算真占用。拿 usage 判 OOM 会因缓存冲高天天误报。working_set 减掉了「可回收部分」,和内核 memcg 决定杀不杀容器用的是同一把尺子,所以只有它有资格回答这个问题。

Q3 · VMS 比 RSS 高好几倍,正常吗?要处理吗?

参考答案:完全正常,不用处理。操作系统给每个进程一个巨大的虚拟地址空间,malloc、mmap 都只是先在账本上记账,真正分配要等真的读写——所以 VMS 几乎永远比 RSS 大数倍。它那条高高的水平线就是个「登记单」,看看就好;该报警的是 RSS 的形态和 deriv,跟 VMS 没关系。

Q4 · 面板上看到「内存 90%」,你的第一反应应该是什么?

参考答案:先问「这是哪个口径的 90%」。进程面板(m101/b9)的数字是 RSS,90% 本身不直接等于危险,要看形态和 deriv(是不是在持续爬);容器面板(k2)的 90% 是 working_set ÷ limit,那才是 OOM 倒计时,超过且持续 5 分钟就要动手。口径不同,同样的数字含义完全不同。

← 上一课:连接池:图书馆借书 📚 课程目录 下一课:OOM kill:内存超限直接枪毙 →