云服务器内存排查:从 RSS、页缓存到 cgroup OOM 的证据链


一台 2 GiB 云服务器同时运行博客、接口和定时构建。监控显示内存使用率 93%,凌晨接口还重启过一次。最直观的处理是“清缓存、加交换分区、重启应用”,但这三步可能只会抹掉证据,让同一故障下周再次发生。

本文用一个假设案例讨论定位顺序。数字用于说明判断方法,命令是 Linux 上的排查示例,并未在某台生产服务器上实际执行。目标不是找出最大的数字,而是解释:哪个资源边界先被撞到,谁在什么时间受到影响。

先把三个“内存”分开

主机物理内存、进程驻留内存、服务 cgroup 计费内存描述的是不同范围。主机看起来还有空间,某个被限制为 512 MiB 的服务也可能发生 OOM;进程 RSS 没怎么涨,服务的文件缓存、子进程或内核对象仍可能增长。

/proc/meminfo 中的 MemAvailable 是可供新应用使用内存的估计值,包含可回收部分;RSS 描述驻留页,PSS 按比例分摊共享页。把多个进程 RSS 相加会重复计算共享区域。需要较细的快照时可看 smaps_rollup,而不是把虚拟地址空间 VmSize 当作物理占用。Linux 内核的 /proc 文档

建议先建立一张同时刻的表:主机可用内存、服务当前计费量、服务上限、主进程匿名页、工作进程数量。没有“同一时刻”这个约束,很容易把重启后的低 RSS 和重启前的高占用拼成一个不存在的故事。

1
2
3
4
5
6
# 先确认目标服务名称;以下读取不修改服务配置。
date --iso-8601=seconds
free -h
systemctl show notes-api.service \
-p MainPID -p ControlGroup -p MemoryCurrent -p MemoryHigh -p MemoryMax
ps -eo pid,ppid,rss,vsz,comm --sort=-rss | head -n 12

free -h 适合扫一眼量级,后续做差值最好保留字节或 KiB 原值,避免四舍五入掩盖几十 MiB 的变化。head 只显示一部分进程,因此它不能证明剩余进程没有累计占用。

先证明发生了什么,再解释为什么

“退出码 137”只是一个线索,它可能来自 SIGKILL,也可能是包装脚本显式返回的同一数值。不要看到它就写下“系统 OOM”。先保留服务日志、内核日志和重启次数,时间范围应覆盖故障前后几分钟。

1
2
3
4
5
journalctl -u notes-api.service --since "2026-10-18 01:50:00" \
--until "2026-10-18 02:10:00" --no-pager
journalctl -k --since "2026-10-18 01:50:00" \
--until "2026-10-18 02:10:00" --no-pager
systemctl show notes-api.service -p Result -p NRestarts -p ExecMainStatus

如果日志权限不足,应按运维授权读取,而不是把空输出解释为“没有 OOM”。内核环形缓冲、日志保留周期以及启动次数都可能影响历史证据。日志里明确出现受害进程、cgroup 路径和时间,才有条件继续归因。

假设记录显示:主机 MemAvailable 尚有 850 MiB,接口服务 memory.max 为 512 MiB,同时 oom_kill 增量为 1。此时“升级到 4 GiB 就能解决”缺乏因果支持,因为服务仍受原限额约束。扩主机容量和调整工作负载边界是两个不同操作。

cgroup 文件要看增量,也要看祖先

在 cgroup v2 中,memory.current 反映该组及其后代的当前使用量;memory.high 超限可导致回收与节流,memory.max 是更硬的边界。memory.events 的计数通常包含后代,局部事件可看 memory.events.local;max 增长不自动等于已经杀进程,需结合 oom_kill。cgroup v2 内存接口

下面先确认 /sys/fs/cgroup/cgroup.controllers 存在,再读取 systemd 报告的路径。非 v2 系统、容器内裁剪过的视图以及已退出服务,需要按环境调整,不能硬套同一目录。

1
2
3
4
5
6
7
8
9
10
test -f /sys/fs/cgroup/cgroup.controllers
cg=$(systemctl show notes-api.service -p ControlGroup --value)
case "$cg" in
/*) ;;
*) printf '%s\n' '未获得有效 cgroup 路径'; exit 1 ;;
esac
for name in memory.current memory.high memory.max memory.events memory.stat; do
printf '\n%s\n' "$name"
cat "/sys/fs/cgroup${cg}/${name}"
done

这里的 printf 是为了区分输出,未写入内核控制文件。记录两次 memory.events,相减后再判断本轮请求造成了什么;累计值为 3 可能是三天前发生过 3 次,而不是当前每分钟都出错。还要检查父级 slice 的限制:叶子显示 max 不代表它没有被祖先约束。

用增长曲线区分泄漏与并发放大

设接口静态基线为 180 MiB,每个在途图像任务额外占 22 MiB。8 个并发会增加约 176 MiB,再加 70 MiB 缓冲与暂存,估算总量为 426 MiB。若任务队列没有上限,突发到 14 个并发,估算值会到 558 MiB。这个模型即使没有任何泄漏,也足以触发 512 MiB 限制。

这是近似容量模型,不是内核公式。图像解码后的像素阵列、并行任务共享程度和分配器保留空间都需要实测。它的价值在于提出一个可验证预测:若把并发限制为 6,峰值应明显下降,任务排队时间则会上升。如果峰值不变,就该检查任务是否实际上被限制、是否还有另外的工作进程。

另一种曲线是:每次负载结束后,空闲基线仍从 180、220、260 MiB 逐轮抬高。它支持“长期保留对象”的假设,但仍不能单凭 RSS 宣判泄漏。运行时可能保留堆页供下次复用。下一步应检查堆快照、缓存键数量、未释放原生缓冲和连接池,而不是直接要求操作系统归还所有页面。

为了比较不同日期的曲线,还要按完成任务数归一化。例如一天处理十万张图后增加二十 MiB,另一天只处理一千张就增加二十 MiB,虽然最终占用相同,增长机制可能完全不同。记录每轮的输入尺寸分布也很重要:同样一百张图片,四千万像素与一百万像素的工作集需求差异巨大。把“请求数相同”误当成“负载相同”,会让压测结果看似矛盾。

容量指标之外,再看停顿时间

页缓存增长可能改善读性能;真正需要关心的是回收是否已经让请求停顿。PSI 的 memory some 表示至少部分任务因内存资源停顿的时间比例,full 表示所有非空闲任务同时停顿的比例;total 使用微秒累计,平均值提供不同时间窗口。Linux PSI 文档

1
2
3
cat /proc/pressure/memory
cat "/sys/fs/cgroup${cg}/memory.pressure"
vmstat 1 10

把一次采样中 some avg10=8.00 直接翻译成“8% 内存不足”是不对的,它表达时间维度。告警应把它与接口 p95、排队长度共同记录:可用内存较低但停顿几乎为零,通常优先观察;停顿持续增加且尾延迟同步恶化,应降低并发或迁移批任务。阈值应从自身业务基线建立,不能从某篇文章复制一个百分比。

vmstat 第一行常含开机以来的汇总信息,判断当前变化要关注后续区间。交换活动也要看持续速率:交换分区“已有占用”不能证明当前发生频繁换入换出。

一次可重复的验证应该怎样做

先在隔离测试环境定义固定输入:相同图像集合、固定并发、固定运行时长;记录预热后基线、峰值、冷却后基线,以及每千次任务的错误率。接着只改一个变量,例如并发由 12 降到 6,或把构建任务移到低峰,重复三轮,比较峰值与尾延迟。每一轮都保留限额和应用版本,否则结果无法复现。

为了保持证据,实验不需要主动制造主机 OOM。给测试服务设置有限内存、使用小规模数据就足以验证“并发增加是否放大工作集”。若必须观察 OOM 行为,应在可丢弃虚拟机中进行,并事先明确哪些任务会被终止。

最后的方案取舍通常很具体:限制并发节省内存但增加等待;增加服务限额减少局部 OOM,却可能挤压同机数据库;增加交换空间可以缓冲短时压力,但不能替代稳定的工作集预算。一次合格的修复报告,应说明选择保护谁、允许谁排队,以及通过什么指标判断修复有效。

复盘时保留的四项结论

结论可以写成“边界、触发、影响、证据”:512 MiB 服务限额;构建与图像高并发重叠;接口被终止并重启;同一分钟的事件增量和任务曲线相互印证。相比“服务器内存不够”,这种结论能直接指导下一次容量调整。

用于查阅的官方接口还包括 systemd 资源控制说明。具体指令以服务器上 man 和 systemd --version 对应版本为准,本文没有对读者环境执行任何限额修改。


 上一篇
用 systemd 管好一个后台服务:启动成功、就绪、重启与权限边界 用 systemd 管好一个后台服务:启动成功、就绪、重启与权限边界
把程序放进 systemd 不只是开机自启。本文围绕一个回环监听的笔记接口,区分进程启动与业务就绪,推导重启退避和停止时间预算,展示最小权限配置,并设计能够验证失败恢复与数据完整性的演练步骤。
2026-10-18
下一篇 
CameraX 图像分析:读懂 YUV 平面步长,才能正确处理背压与帧生命周期 CameraX 图像分析:读懂 YUV 平面步长,才能正确处理背压与帧生命周期
相机预览正常而分析画面错位,往往不是算法错误,而是把有填充的 YUV 平面当成连续数组。本文从像素寻址推导裁剪区域的读取方式,结合 ImageProxy 生命周期与背压策略,设计可验证、内存受控的分析管线。
2026-10-18
  目录