一、概述
1.1 背景介绍
故障发生在一套图片缩略图服务的晚高峰时段。版本发布后不到 90 分钟,业务侧先出现 P99 RT 抖动,随后节点 MemAvailable 持续下滑,容器开始间歇性被 OOMKilled,同节点上的日志采集和边车也跟着抖。现场最容易犯的错是看到内存高就先重启,或者只看 free -h 就把锅甩给 page cache。这样做能把告警按掉,但根因还在,第二波流量一来还会复发。
这次复盘的价值不在于“某个命令怎么用”,而在于把四个工具的职责拆清楚:free 负责判断整机是否真的在失血,smem 负责把共享页和私有页拆开,pmap 负责把可疑进程的映射类型摊平,perf 负责把“谁在不停分配”收敛到调用栈。四个工具单独用都不难,难的是现场顺序。顺序一乱,结论就会飘。
1.2 现场影响与入口信号
| 项目 | 现场表现 | 值班影响 |
|---|---|---|
| 业务接口 | 缩略图生成接口 P99 从 320ms 升到 2.4s | 上游图片服务重试放大 |
| 节点资源 | MemAvailable 从 18GiB 降到 2GiB | 触发全节点内存回收 |
| 容器事件 | 20 分钟内出现 7 次 OOMKilled | Pod 重启,连接池反复建立 |
| 系统日志 | journalctl -k 出现 Out of memory 和 Killed process | 说明不止容器内 RSS 高 |
| 业务日志 | 个别请求耗时与大图解码强相关 | 指向 native 库和缓冲区 |
1.3 工具分工表
| 工具 | 回答的核心问题 | 第一眼先看什么 | 适用阶段 |
|---|---|---|---|
| free | 是整机匿名内存上涨,还是 page cache 波动 | available 、buff/cache、swap | 第一分钟 |
| vmstat / sar -r | 回收、换页、内存压力是否同步出现 | si/so 、pgscan、kbmemfree | 第一轮判断 |
| smem | 哪个进程真正吃掉了物理内存 | PSS 、USS、进程组分布 | 进程定位 |
| pmap -x / smaps | 涨的是匿名段、文件映射还是共享页 | anonymous 、private dirty | 第二轮下钻 |
| perf | 谁在持续分配、拷贝、解码 | 热点符号、调用链 | 根因确认 |
1.4 故障判断链路
先证明问题是整机持续失血,而不是 page cache 的正常波动。
再证明是单类业务进程的 USS/PSS 异常,而不是共享库映射带来的假大户。
再证明涨的是匿名私有映射,而不是文件映射、tmpfs 或 JIT 缓冲。
最后用调用栈把分配热点收敛到具体 native 解码库的异常分支。
这条链路看起来长,实际比“看到 RSS 大就重启”更省时间。因为只要证据链建立完整,后面的止血、回滚、回归验证都会跟着变简单。
1.5 环境与证据保全策略
| 项目 | 实际值 | 说明 |
|---|---|---|
| 宿主机 | 32C / 64GiB | 混部 10 个业务 Pod |
| 容器限制 | requests.memory=4Gi ,limits.memory=6Gi | 超限先杀容器,再拖低节点 |
| 内核版本 | 5.15.x | 支持 perf 与现代 cgroup 观测 |
| 运行时 | containerd 1.7 | 通过 crictl 可关联容器与 PID |
| 发布变更 | 引入新版图片解码 .so | 故障与版本变更时间高度重合 |
现场纪律只有三条:
没做快照前不要重启可疑进程;
没拆清共享页前不要直接按 RSS 排名甩锅;
没把 OOM、smaps、perf 串起来前,不要轻易下“代码泄漏”结论。
1.6 先把现场归到哪一类
| 类别 | 典型信号 | 第一反应 |
|---|---|---|
| 真泄漏 | USS 、Anonymous、Private_Dirty 单向上涨 | 重点抓 smaps 与 perf |
| page cache 幻觉 | used 高但 available 稳 | 不要急着重启 |
| 共享页放大 | RSS 高、PSS/USS 不高 | 先拆共享页 |
| cgroup 过小 | Pod OOM 明显早于节点压力 | 先核对 limit 与工作集 |
| tmpfs / shm 膨胀 | /dev/shm 或挂载目录飙高 | 先查共享内存 |
二、详细步骤
2.1 故障背景与时间线
这次事故不是“机器突然不够用”,而是一次上线引入的 native 图像解码路径在异常图像输入下没有及时释放中间缓冲,导致匿名私有页持续上涨。由于服务本身吞吐不低,单次泄漏不大,但在持续流量下累积明显,最终表现成整机内存缓慢坠落。
| 时间 | 事件 | 证据 |
|---|---|---|
| 19:05 | 新版本灰度 20% | 发布记录与镜像 digest |
| 19:22 | MemAvailable 开始持续下降 | 节点监控 |
| 19:34 | 图片接口 P99 明显抬高 | APM 与 ingress 指标 |
| 19:41 | 首个 Pod 被 OOMKilled | kubectl describe pod |
| 19:48 | 节点开始出现回收和 direct reclaim | vmstat 、journalctl -k |
| 19:56 | 灰度回滚后曲线放缓 | 发布记录与监控对照 |
| 20:20 | 现场采样完成,锁定 native 解码库 | smaps + perf |
2.1.1 快速确认整机内存是不是在持续失血
free -h vmstat 1 10 sar -r 1 10
total used free shared buff/cache available Mem: 62Gi 57Gi 1.8Gi 512Mi 3.4Gi 2.2Gi Swap: 0B 0B 0B
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 4 1 0 1898040 48240 3521100 0 0 40 6120 812 2311 41 12 39 8 0 6 1 0 1651120 48240 3498124 0 0 64 7104 901 2488 43 11 35 11 0
第一轮判断点只有两个:
available 在持续掉,不是上下抖几百 MiB;
buff/cache 没有同步膨胀,说明不是 page cache 在顶高“已用内存”。
如果 free 显示 used 高但 available 还稳,或者 buff/cache 高但可回收,那不是这篇文章讨论的泄漏现场,别把正常缓存当故障。
2.1.2 把内核回收信号一起看
grep -E 'pgscan|pgsteal|pswpin|pswpout' /proc/vmstat | head cat /proc/pressure/memory
some avg10=12.54 avg60=8.31 avg300=2.20 total=145986126 full avg10=1.73 avg60=0.88 avg300=0.19 total=18201132
这里的价值在于区分两件事:
进程真的在涨内存:MemAvailable 持续降,memory pressure 升;
只是瞬时波动:available 会回升,psi 不会持续拉高。
2.1.3 用节点与 Pod 视角对齐时间
kubectl top pod -n media | sort -k4 -hr | head kubectl get events -n media --sort-by=.lastTimestamp | tail -20
thumbnail-svc-7b6d4d7d5f-krp2m 120m 5820Mi thumbnail-svc-7b6d4d7d5f-z9qwn 118m 5712Mi log-agent-g8t6b 80m 210Mi
节点和容器两个视角必须对齐。否则你会遇到一种常见误判:容器看着只用了 5.7Gi,却认为节点没理由掉得这么快。实际上,同节点多个副本同步上涨,再加上回收开销和边车缓存,就足够把整机拖进回收。
2.2 第一轮判断:先分整机问题还是 page cache 幻觉
2.2.1 不要只盯 used
| 指标 | 现场表现 | 正确解释 | 常见误判 |
|---|---|---|---|
| used | 高 | 只是总占用,不区分缓存 | 看见高就说泄漏 |
| available | 连续下降 | 真正可用内存在变少 | 被忽略 |
| buff/cache | 基本稳定 | 缓存不是主因 | 被误认为“都能回收” |
| swap | 为 0 | 没有 swap 缓冲空间 | 内存压力更直接传导到 OOM |
2.2.2 结合 slab 与 tmpfs 排除其他大户
slabtop -o | head -20 df -h /dev/shm du -sh /run /tmp 2>$null | sort -hr | Select-Object -First 10
Active / Total Objects (% used) : 9382842 / 9553180 (98.2%) Active / Total Slabs (% used) : 221439 / 221439 (100.0%) OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
这一步是为了排除三个容易把方向带偏的对象:
slab 异常增长,尤其是 dentry、inode、网络缓存;
tmpfs 或共享内存打满;
临时目录或运行时目录堆积,表现成文件页而不是匿名页。
如果这些项平稳,再继续往用户态进程下钻就更稳。
2.3 第二轮判断:用 smem 把共享页和私有页拆开
2.3.1 按 PSS/USS 找真大户
smem -tk smem -rs pss -k | head -20 smem -P thumbnail-svc -c "pid user command pss uss rss" | head -20
PID User Command PSS USS RSS 23111 app /usr/local/bin/thumbnail-svc 4312M 4098M 5220M 23198 app /usr/local/bin/thumbnail-svc 4276M 4061M 5174M
为什么 smem 比 ps aux --sort -rss 更适合第一轮定位:
RSS 会把共享页全部算进去,多个副本看上去都很大;
PSS 会把共享页按比例摊分,更接近真实物理占用;
USS 基本可以看成进程私有不可共享的部分,涨得快时非常敏感。
这次现场两个 thumbnail 进程的 USS 一起涨,说明不是共享库映射大,也不是语言运行时把共享页放大,而是每个实例都在单独囤匿名私有内存。
2.3.2 按时间采样,而不是只看单点
for ($i=0; $i -lt 6; $i++) { smem -P thumbnail-svc -c "pid pss uss rss" | Select-Object -First 5; Start-Sleep 30 }
PID PSS USS RSS 23111 4312M 4098M 5220M 23111 4470M 4254M 5381M 23111 4628M 4411M 5534M
单点的大不一定有问题,持续上涨才有问题。线上值班经常踩的坑是采一张截图就下结论,结果只是刚好遇到一批大图请求。这里 3 轮采样都在涨,方向就足够清楚了。
2.3.3 把容器 PID 和宿主机 PID 对齐
crictl ps | grep thumbnail-svc crictl inspect| jq '.info.pid' nsenter -t -m -u -i -n -p -- ps -o pid,ppid,cmd -p 1
如果 PID 没对齐,后面的 pmap 和 perf 就可能采错对象。Kubernetes 现场里这一步经常被省略,结果抓了 pause 容器或 sidecar,后面排查全偏。
2.4 第三轮下钻:pmap 与 smaps 把映射类型摊平
2.4.1 用 pmap -x 看匿名段是不是主因
pmap -x 23111 | tail -20 pmap -x 23111 | grep -E 'anon|zero|rw---' | tail -20
Address Kbytes RSS Dirty Mode Mapping 00007f5de0000000 524288 520112 520112 rw--- [ anon ] 00007f5e00000000 262144 259800 259800 rw--- [ anon ] total kB 5481120 5220188 5179948
现场最关键的信号是 [ anon ] 匿名私有段持续膨胀,且 Dirty 高。这说明内存不是文件映射进来的,也不是共享库只读段,而是真有人在申请并持有用户态堆或大块缓冲。
2.4.2 用 smaps 聚合 private dirty / anonymous
grep -E '^(Size|Rss|Pss|Private_Dirty|Anonymous):' /proc/23111/smaps | head -40
Size: 524288 kB Rss: 520112 kB Pss: 520112 kB Private_Dirty: 520112 kB Anonymous: 520112 kB
为了避免在几千行 smaps 里迷路,现场更实用的是做一次字段聚合。
awk '
/^Size:/ {size+=$2}
/^Rss:/ {rss+=$2}
/^Pss:/ {pss+=$2}
/^Private_Dirty:/ {pd+=$2}
/^Anonymous:/ {anon+=$2}
END {printf("Size=%dKB Rss=%dKB Pss=%dKB Private_Dirty=%dKB Anonymous=%dKB
", size,rss,pss,pd,anon)}
' /proc/23111/smaps
Size=5481120KB Rss=5220188KB Pss=5219062KB Private_Dirty=5179948KB Anonymous=5182200KB
这组数字把根因方向收得很窄:高的是匿名私有脏页。如果这里看到的是文件映射大,就应该回头查内存映射文件、模型权重、共享内存、JIT cache;如果看到 Shared_Clean 高,就不该再往“每个实例都泄漏”上硬靠。
2.4.3 映射类型判断矩阵
| 映射信号 | 更像什么问题 | 下一步该看什么 |
|---|---|---|
| Anonymous 持续升高 | 堆内存、native 缓冲、未释放对象 | perf 、jemalloc/tcmalloc 统计 |
| 文件映射大 | mmap 文件、模型、索引、缓存文件 | lsof -p 、映射文件路径 |
| Shared_Clean 大 | 共享库、共享页、只读映射 | 不要直接按 RSS 下结论 |
| Private_Dirty 高 | 真实私有写脏内存 | 重点排查申请后未释放 |
2.4.4 把大图请求和内存增长对齐
grep 'decode image' /var/log/thumbnail/app.log | tail -20
grep 'decode image' /var/log/thumbnail/app.log | awk '{print $1" "$2" "$NF}' | tail -20
2026-03-07 1912 req_id=8f3a size=17834x12920 format=tiff decode image slow path 2026-03-07 1913 req_id=1c5e size=18400x13110 format=tiff decode image slow path
单看 smaps 只能说明“内存涨了”,还不能解释“为什么是这个时段”。业务日志和请求画像对齐后,可以看到大尺寸 TIFF 输入开始增多,而这批请求恰好走到新引入的 native 库慢路径。
2.5 第四轮确认:用 perf 把热点收敛到调用栈
2.5.1 先看热点符号
perf top -p 23111 -g --call-graph dwarf
32.14% thumbnail-svc libimgdecode.so [.] allocate_decode_buffer 18.66% thumbnail-svc libimgdecode.so [.] tiff_decode_tiles 11.42% thumbnail-svc libc.so.6 [.] __memmove_avx_unaligned_erms
如果热点一直落在 malloc 或业务解码函数附近,就说明不是单纯回收慢,而是业务线程确实在持续申请和处理大块缓冲。接下来要做的是把这条栈采下来,给研发和回滚决策提供证据。
2.5.2 录一段能复现的调用栈
perf record -F 99 -g -p 23111 -- sleep 30 perf report --stdio | head -80
# Overhead Command Shared Object Symbol # ........ .............. .................. ............................. 31.22% thumbnail-svc libimgdecode.so allocate_decode_buffer 8.11% thumbnail-svc libimgdecode.so expand_tile_cache 7.98% thumbnail-svc libimgdecode.so decode_tiff_frame
2.5.3 结合版本差异锁定回归点
strings /usr/lib/libimgdecode.so | grep -E 'version|commit' | head rpm -qa | grep imgdecode
libimgdecode version 2.4.19 commit 9b7c1ea
这一步不是为了做研发调试,而是为了把“问题是不是跟发布强相关”固定住。现场回滚是否值得,取决于你能不能把版本、时间、栈热点、请求画像四条线对齐。
2.6 根因分析:为什么不是 page cache、不是共享页、不是语言运行时
2.6.1 根因矩阵
| 假设 | 证据支持情况 | 为什么排除 / 保留 |
|---|---|---|
| page cache 挤占内存 | 弱 | buff/cache 稳定,available 持续降,不成立 |
| slab 或 tmpfs 异常 | 弱 | slabtop 和 /dev/shm 无异常,不成立 |
| 共享库映射看起来大 | 弱 | USS/PSS 同步上涨,Shared_Clean 不是主因,不成立 |
| Java/Go 运行时保留堆未归还 | 中 | 业务不是托管语言主进程,且匿名段与 native 解码栈一致,基本排除 |
| native 图像解码库缓冲未释放 | 强 | smaps 匿名私有页大、perf 热点在解码库、版本变更一致,成立 |
2.6.2 根因结论
最终根因是新版 libimgdecode.so 在 TIFF 分块解码的异常路径下,申请的中间缓冲在错误分支没有及时释放。正常流量下不明显,晚高峰大图请求密集时,每个 worker 都会把匿名私有页向上顶,容器先被打到 limits,节点再进入内存回收,导致整机业务 RT 抖动。
2.6.3 为什么 cgroup OOM 和节点压力会一起出现
| 现象 | 解释 |
|---|---|
| 先出现 OOMKilled | 单 Pod 先撞上容器 limit |
| 节点 MemAvailable 继续降 | 同节点多副本在一起涨 |
| 最后出现内核 OOM 迹象 | 节点总体回收空间也被吃掉 |
这类场景容易让人误判成“只是单 Pod limit 太小”。实际上 limit 只是第一道保险,真正的问题是多个实例在同一节点同步持有异常匿名页。只调大单 Pod limit,会把问题从容器层推到节点层。
2.7 修复动作与止血顺序
2.7.1 当班止血动作
| 顺序 | 动作 | 原因 |
|---|---|---|
| 1 | 暂停灰度并回滚到旧镜像 | 先止住新增泄漏源 |
| 2 | 降低大图接口并发阈值 | 避免回滚期间继续放大 |
| 3 | 对最重节点做有序摘流和滚动替换 | 避免整节点一起抖 |
| 4 | 保留至少一个故障实例做取证 | 保障根因能闭环 |
2.7.2 回滚与限流命令
kubectl rollout undo deployment/thumbnail-svc -n media kubectl scale deployment/thumbnail-svc -n media --replicas=6 kubectl cordon gpu-node-03 kubectl drain gpu-node-03 --ignore-daemonsets --delete-emptydir-data
2.7.3 热修复前的风险边界
resources: requests: cpu:"2" memory:"4Gi" limits: cpu:"4" memory:"6Gi" env: -name:MAX_TIFF_PIXELS value:"120000000" -name:DECODE_WORKERS value:"4"
上面的配置不是最终修复,而是当班期间的风险边界:缩小可接受的大图尺寸,压低解码并发,先让业务稳定。
2.8 回归验证:验证要回到证据链,而不是只看服务恢复
2.8.1 回滚后重复同样的采样
free -h smem -P thumbnail-svc -c "pid pss uss rss" pmap -x $(pgrep -f thumbnail-svc | Select-Object -First 1) | tail -20
2.8.2 压测回放异常输入
curl -s -o /dev/null -w '%{http_code} %{time_total}
'
-X POST http://thumbnail.media.svc/resize
-H 'Content-Type: application/json'
-d '{"url":"http://sample/big.tiff","width":512,"height":512}'
200 0.842
2.8.3 验收标准
| 检查项 | 验收标准 |
|---|---|
| MemAvailable | 1 小时内无持续下降趋势 |
| USS/PSS | 在压力下有波动但不单向累积 |
| OOMKilled | 归零 |
| 业务 RT | 恢复到发布前 10% 波动内 |
| perf 热点 | 不再被解码库分配函数长期占据 |
2.8.4 回归验证周期不要只看 10 分钟
| 时间窗口 | 要看什么 | 目的 |
|---|---|---|
| 10-15 分钟 | 是否立刻再次 OOM | 验证止血是否有效 |
| 1 小时 | USS/PSS 是否继续累积 | 验证慢泄漏是否仍在 |
| 1 个高峰周期 | 大图和异常输入回来后是否复发 | 验证修复不是侥幸 |
很多泄漏问题在低流量窗口里看不出来。复盘里如果不把“高峰期再验证”写进去,值班手册就只有止血,没有闭环。
三、示例代码和配置
3.1 现场采样命令清单
date hostname uptime free -h vmstat 1 5 sar -r 1 5 smem -rs pss -k | head -20 journalctl -k --since '30 min ago' | tail -200
3.2 脚本一:mem_scene_collect.sh
#!/usr/bin/env bash # 文件名:mem_scene_collect.sh # 作用:一次性采集整机与进程内存现场信息 # 适用场景:节点内存告警、容器被 OOMKilled 前后的应急取证 # 使用方式:bash mem_scene_collect.sh# 输入参数:目标进程 PID、输出目录 # 输出结果:生成 free/vmstat/smaps/pmap/journalctl 等快照 # 风险提示:会读取 /proc 和系统日志,perf 未包含在内,默认低风险 # 结果解读:优先看 summary.txt 中的 available、uss、anonymous 汇总 set -euo pipefail pid="${1:?pid required}" out="${2:-/tmp/mem-scene-$(date +%F-%H%M%S)}" mkdir -p "$out" { echo"time=$(date '+%F %T')" echo"host=$(hostname)" echo"pid=$pid" } > "$out/summary.txt" free -h > "$out/free.txt" vmstat 1 5 > "$out/vmstat.txt" cat /proc/pressure/memory > "$out/memory.psi.txt" smem -P "$(tr '�' ' ' < /proc/$pid/cmdline | awk '{print $1}')" -c "pid pss uss rss command" > "$out/smem.txt" pmap -x "$pid" > "$out/pmap.txt" cp "/proc/$pid/smaps""$out/smaps.txt" journalctl -k --since '30 min ago' > "$out/kernel.log"
3.3 脚本二:smaps_rollup.py
#!/usr/bin/env python3 # 文件名:smaps_rollup.py # 作用:聚合 smaps 关键字段,快速判断匿名页和私有脏页占比 # 适用场景:pmap 看出匿名段异常后做二次汇总 # 使用方式:python smaps_rollup.py /proc//smaps # 输入参数:smaps 文件路径 # 输出结果:JSON 汇总结果 # 风险提示:读取大进程 smaps 文件时会有少量 IO 开销 # 结果解读:Anonymous、Private_Dirty 高且持续上涨时优先看 native 分配路径 import json import re import sys keys = {"Size": 0, "Rss": 0, "Pss": 0, "Private_Dirty": 0, "Anonymous": 0} pattern = re.compile(r"^(Size|Rss|Pss|Private_Dirty|Anonymous):s+(d+)s+kB$") with open(sys.argv[1], "r", encoding="utf-8") as f: for line in f: m = pattern.match(line.strip()) if m: keys[m.group(1)] += int(m.group(2)) print(json.dumps(keys, ensure_ascii=False, indent=2))
3.4 脚本三:pmap_growth_diff.sh
#!/usr/bin/env bash
# 文件名:pmap_growth_diff.sh
# 作用:比较两次 pmap 快照的差异,定位增长最快的映射段
# 适用场景:进程内存持续上涨但不确定涨在哪类段
# 使用方式:bash pmap_growth_diff.sh before.txt after.txt
# 输入参数:两次 pmap -x 输出文件
# 输出结果:按增长量排序的映射段
# 风险提示:依赖 pmap 输出格式,系统版本差异需先验证
# 结果解读:若 [ anon ] 段增长显著,优先排查堆与 native buffer
set -euo pipefail
before="${1:?before required}"
after="${2:?after required}"
awk '
FNR==NR && $1 ~ /^0/ {a[$1]=$3" "$6; next}
FNR!=NR && $1 ~ /^0/ {
split(a[$1], old, " ")
diff=$3-old[1]
if (diff != 0) printf "%10d KB %-8s %s
", diff, $1, $6
}
'"$before""$after" | sort -nr | head -30
3.5 脚本四:oom_evidence_pack.sh
#!/usr/bin/env bash # 文件名:oom_evidence_pack.sh # 作用:打包 OOM 相关系统日志、容器事件和 cgroup 指标 # 适用场景:容器或节点刚发生 OOM,需要保留证据 # 使用方式:bash oom_evidence_pack.sh# 输入参数:命名空间、Pod 名称、输出目录 # 输出结果:生成事件、describe、dmesg、memory.current 等文件 # 风险提示:依赖 kubectl 与节点权限;事件窗口过久时会丢失历史 # 结果解读:若 cgroup 接近 limit 且 kernel log 出现 killed process,说明限制已真正触发 set -euo pipefail ns="${1:?namespace required}" pod="${2:?pod required}" out="${3:-/tmp/oom-pack-$(date +%F-%H%M%S)}" mkdir -p "$out" kubectl describe pod "$pod" -n "$ns" > "$out/pod.describe.txt" kubectl get events -n "$ns" --sort-by=.lastTimestamp > "$out/events.txt" journalctl -k --since '1 hour ago' > "$out/kernel.log" dmesg -T | tail -300 > "$out/dmesg.tail.txt"
3.6 业务日志样例
2026-03-07 1912 level=warn req_id=8f3a path=/resize format=tiff width=17834 height=12920 msg="decode image slow path" 2026-03-07 1912 level=warn req_id=8f3a msg="native buffer growth" alloc_bytes=268435456 2026-03-07 1913 level=error req_id=8f3a msg="decoder fallback retry"
3.7 Prometheus 告警规则样例
groups:
-name:node-memory-risk
rules:
-alert:NodeMemAvailableLow
expr:node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes<0.08
for:10m
labels:
severity:critical
annotations:
summary:"节点可用内存低于 8%"
-alert:PodOOMKilledBurst
expr:increase(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[15m])>2
for:5m
labels:
severity:warning
3.8 回归后的预期输出样例
MemAvailable: stable around 17.2GiB-18.1GiB for 60m thumbnail-svc uss: fluctuate within 3.2GiB-3.4GiB, no monotonic growth OOMKilled: 0
3.9 cgroup 快照脚本:cgroup_mem_snapshot.sh
#!/usr/bin/env bash # 文件名:cgroup_mem_snapshot.sh # 作用:采集容器 cgroup 内存限制、当前使用量和事件计数 # 适用场景:需要区分 Pod limit 命中还是节点整体内存失血 # 使用方式:bash cgroup_mem_snapshot.sh# 输入参数:容器主进程 PID # 输出结果:输出 memory.current、memory.max、memory.events # 风险提示:只读采集,低风险 # 结果解读:若 current 长期贴近 max,先从容器限制和业务峰值入手 set -euo pipefail pid="${1:?pid required}" root="/proc/$pid/root/sys/fs/cgroup" find "$root" -maxdepth 2 -type f ( -name memory.current -o -name memory.max -o -name memory.events ) -print -exec cat {} ;
3.10 perf 火焰图准备脚本:perf_capture_prepare.sh
#!/usr/bin/env bash # 文件名:perf_capture_prepare.sh # 作用:短时间采集 perf record 数据,供后续火焰图或 report 分析 # 适用场景:已经锁定到单进程,需要留调用栈证据 # 使用方式:bash perf_capture_prepare.sh# 输入参数:目标 PID、采样时长 # 输出结果:生成 perf.data # 风险提示:生产环境控制采样时长;高峰期避免长时间采样 # 结果解读:分配热点持续落在同一库和函数上时,更接近真泄漏 set -euo pipefail pid="${1:?pid required}" secs="${2:-20}" perf record -F 99 -g -p "$pid" -- sleep "$secs"
四、最佳实践和注意事项
4.1 最佳实践
| 场景 | 建议做法 | 原因 |
|---|---|---|
| 节点内存告警 | 先做整机 + 进程双快照 | 先分系统级和进程级问题 |
| 混部节点 | 保留一个问题实例取证,其余实例先摘流 | 兼顾稳定与定位 |
| 容器环境 | 先对齐容器 PID 与宿主机 PID | 避免抓错对象 |
| 引入 native 库 | 上线前做异常输入压测 | 很多泄漏只在错误路径触发 |
| 线上验证 | 压测回放要覆盖问题输入 | 只跑健康样本没有意义 |
4.2 注意事项
RSS 大不等于真实物理占用大,先看 PSS/USS。
看到 buff/cache 高不要立刻说“都能回收”,要看 available 和回收压力。
perf 在生产环境要控制采样时长和频率,避免长时间高开销。
如果节点启用了 cgroup v2,采集时记得同时看 memory.current、memory.events。
4.2.1 容器场景的几个放大器
| 放大器 | 表现 | 风险 |
|---|---|---|
| limits.memory 贴边 | 容器先被杀 | 还没看清根因实例就没了 |
| 多副本同节点 | 内存曲线一起涨 | 节点比单 Pod 更早告警 |
| sidecar 混部 | Pod 内总内存被放大 | 容易误判为主容器单点问题 |
| 无 swap | 回收缓冲空间小 | OOM 更直接 |
4.3 实际应用案例
4.3.1 案例一:真泄漏与 page cache 误判
free -h cat /proc/pressure/memory
如果 available 稳定、buff/cache 高,且 psi 不升,这更像缓存,不像泄漏。真正泄漏的现场往往是 available 单向下降,psi memory 也同步抬高。
4.3.2 案例二:共享页放大导致 RSS 看起来很吓人
smem -P thumbnail-svc -c "pid pss uss rss"
多个副本共享同一套库映射时,RSS 都会显得很大,但 USS 不一定大。这个案例是线上最常见的“看着像泄漏,实际不是”的误判源。
4.3.3 案例三:native 库慢路径造成匿名私有页持续增长
pmap -x| grep anon | tail -20 perf top -p -g
只要 [ anon ] 和 Private_Dirty 持续涨,再叠加 native 函数热点,方向就可以稳定收敛到用户态缓冲释放问题。
4.4 值班交接要点
1. 当前是否已回滚到旧镜像 2. 是否保留了一台问题实例做 perf/smaps 取证 3. 当前 MemAvailable 曲线是否稳定 4. 大图流量是否已限流 5. 开发侧是否已拿到热点栈和问题样本
4.5 容易误判的现场
| 误判 | 实际更像什么 | 正确动作 |
|---|---|---|
| RSS 最大的进程就是泄漏点 | 可能是共享页放大 | 先看 PSS/USS |
| buff/cache 高就一定不是问题 | 还要看 available 和回收压力 | 补 psi 与 vmstat |
| 容器先 OOM 就只是 limit 小 | 可能是多副本同向增长 | 同时看节点面 |
| 进程一重启内存降了就算修好了 | 只是掩盖现象 | 继续回归验证 |
五、故障排查和监控
5.1 快速排查清单
free -h cat /proc/pressure/memory smem -rs pss -k | head -20 pmap -x| tail -20 journalctl -k --since '20 min ago' | tail -100
5.2 监控指标建议
| 层次 | 关键指标 | 告警建议 |
|---|---|---|
| 节点 | MemAvailable 、psi memory、major page fault | 连续 10 分钟恶化才升级 |
| Pod | working_set 、OOMKilled、restart count | 15 分钟内连续重启即告警 |
| 进程 | USS/PSS 、匿名私有页 | 建议做周期采样而不是只看容器内存 |
| 业务 | P99 RT 、大图请求占比 | 与内存曲线做对齐 |
5.3 常见分支与排除方法
| 现象 | 第一怀疑对象 | 快速排除方法 |
|---|---|---|
| used 高但 available 稳 | page cache | 看 buff/cache 与 psi |
| RSS 高但 USS 不高 | 共享页或映射文件 | 看 PSS/Shared_Clean |
| 容器 OOM 但节点没压力 | limit 太小 | 看 cgroup memory.current |
| 节点和容器都抖 | 多副本同向增长 | 看节点上同类 Pod 分布 |
5.4 回归验证动作
kubectl top pod -n media kubectl get pods -n media -o wide | grep thumbnail-svc for pid in $(pgrep -f thumbnail-svc); do python smaps_rollup.py /proc/$pid/smaps done
5.5 监控面板建议
面板一:节点 MemAvailable / PSI / OOM 数 面板二:Pod WorkingSet / RestartCount / OOMKilled 面板三:业务 RT / 请求量 / 大图比例 面板四:进程 USS/PSS 周期采样
5.6 阈值与告警建议
| 指标 | 建议阈值 | 说明 |
|---|---|---|
| MemAvailable / MemTotal | 持续低于 8%-10% | 节点级风险 |
| psi memory some/full | 连续 5-10 分钟抬高 | 回收压力已影响调度 |
| OOMKilled | 15 分钟内 > 0 | 容器限制已经击穿 |
| USS 增长斜率 | 连续多个采样窗口单向增长 | 慢泄漏高价值信号 |
六、总结
6.1 技术要点回顾
free 先判整机是不是持续失血,不负责给进程定责。
smem 负责把共享页和私有页拆开,避免被 RSS 误导。
pmap / smaps 负责回答“到底是哪类映射在涨”。
perf 负责把增长路径收敛到调用栈,给回滚和修复提供直接证据。
6.2 值班动作优先级
先留证据,再重启实例。
先分 page cache、共享页、匿名私有页,再谈泄漏。
先做止血回滚,再做代码级修复。
回归验证一定回到同一条证据链,不要只看告警消失。
6.3 参考资料
man free
man smem
man pmap
man perf-record
Linux /proc/
附录
A. 命令速查表
| 目标 | 命令 |
|---|---|
| 看整机内存 | free -h |
| 看内存压力 | cat /proc/pressure/memory |
| 看进程真实占用 | smem -rs pss -k |
| 看映射类型 |
pmap -x |
| 看内核 OOM | journalctl -k |
| 看热点栈 |
perf top -p |
B. 根因矩阵
| 根因类型 | 典型信号 | 推荐动作 |
|---|---|---|
| page cache 偏高 | available 稳、buff/cache 高 | 观察回收,不要急着重启 |
| cgroup limit 太小 | Pod OOM 明显早于节点 | 调整 limit 并复核应用峰值 |
| native 泄漏 | USS 、Anonymous、Private_Dirty 一起涨 | 回滚版本、抓栈、修代码 |
| 共享映射放大 | RSS 高、PSS 不高 | 不要按 RSS 甩锅 |
C. 术语表
| 术语 | 含义 |
|---|---|
| RSS | Resident Set Size,进程驻留物理页总量 |
| PSS | Proportional Set Size,共享页按比例摊分后的实际占用 |
| USS | Unique Set Size,进程独占的物理页 |
| Anonymous | 匿名映射,常见于堆、栈、动态申请缓冲 |
| Private_Dirty | 私有且被写脏的页,常能反映真实持有的可疑内存 |
D. 值班复盘清单
1. 是否保留了问题实例和问题样本 2. 是否采集了 free/vmstat/smem/pmap/perf 五类证据 3. 是否将版本、时间线、监控、日志对齐 4. 是否验证回滚后 USS/PSS 不再单向上涨 5. 是否补充了大图异常输入的回归压测
E. 证据链顺序速查
| 顺序 | 工具 / 证据 | 目的 |
|---|---|---|
| 1 | free / vmstat / psi | 判断是不是整机失血 |
| 2 | smem | 定位真大户 |
| 3 | pmap / smaps | 判断是哪类映射在涨 |
| 4 | 业务日志 / 请求画像 | 对齐触发条件 |
| 5 | perf / 版本信息 | 把根因收敛到具体路径 |
F. 现场输出解读样例
场景一:available 下降 + uss 上升 + anonymous 上升 结论:更像真泄漏或 native 缓冲未释放 场景二:rss 高 + pss 不高 + shared_clean 高 结论:更像共享页放大,不宜直接按 RSS 定责 场景三:pod 先 OOM + 节点 available 也在掉 结论:不是单 Pod limit 问题,需同步看节点分布
G. 交接模板
故障开始时间: 影响的节点 / Pod: 当前是否已回滚: 当前是否保留了故障实例: 已采集证据: 当前未完成动作: 下个高峰前需要完成的验证:
H. 指标阈值速查
| 指标 | 观察方式 | 触发动作 |
|---|---|---|
| MemAvailable | 连续下滑且不回升 | 先抓整机快照 |
| USS | 多轮采样持续上涨 | 继续看 smaps |
| Private_Dirty | 与 Anonymous 同向上涨 | 重点看 native 分配路径 |
| OOMKilled | 15 分钟内出现 | 先查 cgroup 与 Pod 分布 |
I. 输出样例库
free -h 正常样例: Mem: 62Gi total, 24Gi used, 28Gi free, 9Gi buff/cache, 35Gi available 说明:used 看着不低,但 available 很充足,不像泄漏 free -h 可疑样例: Mem: 62Gi total, 57Gi used, 1.8Gi free, 3.4Gi buff/cache, 2.2Gi available 说明:available 过低,且 cache 不高,更像真实失血
smem 正常样例: PID PSS USS RSS 2311 520M 410M 910M 说明:有共享页,但私有页不夸张 smem 可疑样例: PID PSS USS RSS 2311 4.3G 4.1G 5.2G 说明:私有页极高,更像真泄漏或 native buffer 问题
J. 信号与动作对照表
| 信号组合 | 更像什么 | 第一动作 | 第二动作 |
|---|---|---|---|
| available 下降 + USS 上升 | 真泄漏 | 抓 smaps | 抓 perf |
| RSS 高 + PSS 不高 | 共享页放大 | 不要按 RSS 定责 | 查共享映射 |
| Pod 先 OOM + 节点也抖 | 多副本同向增长 | 看节点分布 | 做摘流与回滚 |
| tmpfs 高 | 共享内存问题 | 查 /dev/shm | 查业务使用模式 |
K. 取证顺序模板
1. 先抓整机快照 2. 再抓进程内存分布 3. 再抓映射类型 4. 再抓调用栈 5. 最后回到版本、日志和请求样本
L. smaps 字段解读速查
| 字段 | 代表什么 | 值班意义 |
|---|---|---|
| Rss | 驻留物理页 | 不能单独定责 |
| Pss | 按共享比例摊分后的实际占用 | 更适合做跨进程比较 |
| Private_Dirty | 私有脏页 | 真正可疑的私有写内存 |
| Anonymous | 匿名映射 | 常见于堆和大缓冲 |
M. 样本保留模板
问题图片 URL / 样本: 问题请求时间: 相关 req_id: 问题实例 PID: 是否已保留 perf.data: 是否已保留 smaps / pmap:
全部0条评论
快来发表一下你的评论吧 !