看到内存用了 90%、磁盘 %util 达到 100%,很容易立刻想加内存、换 SSD。但这些数字究竟描述谁、哪个时间窗口、什么单位?如果口径没对齐,优化可能只是把钱花在没有证据的猜测上。
本文使用 free、vmstat、iostat、sar 建立从“当前状态”到“资源压力”再到“历史关联”的排障思路。它们用于提出和检验假设,不是四个自动诊断按钮。
系列阅读:学习地图 · 进程与线程排障 · 本篇 · ELF 与链接 · 传输与任务调度。Shell 管道基础可以回看 原有 Shell 笔记。
先统一观察口径
读一个监控数字前,先确定四件事:
| 问题 | 例子 | 不核对会发生什么 |
|---|---|---|
| 对象 | 整机、虚拟机、容器、进程、某个设备 | 把其他工作负载算到自己头上 |
| 时间 | 即时状态、最近一秒、启动以来平均 | 将历史均值误判成当前峰值 |
| 单位 | B、KiB、MB/s、毫秒、百分比 | 比较结果差几个数量级 |
| 边界 | CPU 配额、内存上限、设备层级 | 宿主有余量但容器已经被限制 |
下面的 GIF 是实际命令输出的教学重排,不是屏幕录像。它们展示工具操作和字段关系,不是这台机器的性能基准,更不能证明某个资源已经构成瓶颈。每项实际验证状态见 validation.json,完整输入输出见 recorded-sessions.json。
在完整网站源码仓库根目录执行:
bash docker/linux-tools-lab/run.sh build |
环境与产物契约见 实验 README。本篇不要求在宿主机全局安装监控工具,也不要求为了演示制造 OOM 或大量写满磁盘。
free、vmstat 由 procps-ng 提供,iostat、sar 属于 sysstat。先记录版本,不能因为命令名字相同就默认字段定义永远不变:
free --version |
free:内存少了,还是可回收缓存多了
不要只盯 free 那一列
free -h |
-h 便于快速阅读;-m 使用 MiB 量级输出,方便比较相邻样本;-w 把 buffers 与 cache 分列;-s、-c 给出重复采样间隔和次数。别在一次计算里混用自动缩放后的 GiB 与 MiB 数值。
free 从 /proc/meminfo 获取数据。free 列是当前未使用页,available 是不发生交换时可供新工作负载使用的估计,考虑了可回收部分,并不等于把所有 cache 原封不动加回去。procps-ng free 手册
看动画时先比较 free 与 available,再看缓存和 swap。Linux 把暂时不用的内存用于缓存通常是正常策略;内存充足的机器也可能有很低的 free。
used 的等式有版本差异
Ubuntu 22.04 所带旧版文档将 used 解释为 total - free - buffers - cache;较新的 procps-ng 手册使用 total - available。因此截图里的 used 不同,未必说明某个进程突然增加了占用。Ubuntu Jammy free 手册、当前上游定义
一个纯教学算例:某次相同口径快照中,total 为 16 GiB,free 为 1 GiB,buffers 与 cache 合计 5 GiB,available 为 4 GiB。旧式计算得到 used=10 GiB,较新式计算得到 used=12 GiB;两者表达的概念不同,不能只按名字判定数据错了。
shared 主要涉及 tmpfs 等共享内存统计,不是“所有进程 RSS 重复部分之和”。cache 中也有不同可回收程度的内容,所以把每一列都当互不重叠的集合相加,往往会得到矛盾。
它不能指出谁泄漏了
free 描述全局内存状态,不能直接证明哪个进程在泄漏。进一步回到 ps/top 找候选,再观察该进程 RSS、PSS、映射类型及增长过程。进程内存视角
分配器保留、内存池、文件映射、缓存增长和真正丢失引用的泄漏,可能都表现为 RSS 上升。需要将增长与请求次数、数据量、对象生命周期联系起来,而不是看到一次大数就宣称泄漏。
在更底层,可以对照:
rg '^(MemTotal|MemFree|MemAvailable|Buffers|Cached|Shmem|SwapTotal|SwapFree):' \ |
这些读取发生在不同时间点,系统也在持续变化,所以两次命令的数字不必逐字一致。保留时间、单位和变化趋势,比强求末位相等更合理。Linux /proc 内存统计
最小练习:不用制造内存耗尽
在实验容器中连续采三次,写下每次的 free、available 与 swap;解释它们分别回答什么问题。然后查看容器内存上限,确认工具展示的 total 是否等于这个上限。
不要为了让 GIF 更明显而执行 drop_caches。清缓存会改变全局工作集与后续 I/O,属于干预实验,既不是常规释放内存的修复,也不应该在共享系统随意执行。
Swap 已用量不为零,不代表此刻正在频繁换页。是否出现当前交换压力,需要看接下来 vmstat 的速率和业务延迟;“曾经换出过”和“现在正在抖动”不是同一个事实。
vmstat:看资源是否让任务等着
先分清第一行
vmstat -w -t 1 4 |
第一份报告中,CPU 和许多速率字段反映启动以来的平均;之后才是采样间隔内变化。进程状态和内存字段即使在第一行仍是即时值,不能把整行笼统叫“都是开机平均”。支持 -y 的版本可跳过第一份报告;旧环境先用 --help 核对。procps-ng vmstat 手册
阅读时不要孤立截取一个 wa 或 r。先确定这是不是间隔样本,再把运行队列、交换、块 I/O 和 CPU 分项放在同一行里比较。
用四组字段提出假设
r 是可运行任务数量,含正在运行与等待 CPU;b 提示阻塞等待 I/O 的任务。若 r 持续大于实际可用 CPU 并且 CPU 时间繁忙,才更支持排队竞争;不能用一瞬间的尖峰定性。
swpd 是已使用 swap 的量,si、so 是交换读入/写出速率。持续交换并伴随延迟恶化,才是值得追查的组合;单独一个非零 swpd 没有相同含义。
bi、bo 描述块设备输入/输出速率,in、cs 是中断与上下文切换速率。更多并发连接、短任务和正常唤醒都可能提高 cs;不要直接把“上下文切换多”翻译成“锁有问题”。
us、sy、id、wa、st 分别帮助区分用户态、内核态、空闲、I/O 等待计量与虚拟化 steal。各版本可能显示更多字段,先读表头再写脚本,不要永远按固定第 13 列取 CPU 使用率。
iowait 不是某个进程的等待时长
wa 是 CPU 统计口径里的 I/O 等待计量,不是所有任务阻塞时间的总和,也不是磁盘利用率。内核文档还明确提醒,多核归属和计数变化使 iowait 不能作为可靠的单一判据。Linux /proc/stat 的 iowait 说明
例如 CPU 一边做别的计算、一边有任务等待磁盘,wa 未必很高;程序等待网络远端响应也不能简单用 wa 解释。要结合具体设备的延迟、业务调用栈和 I/O 路径。
使用反例校准判断
如果 r 上升、us 上升、磁盘基本无变化,优先调查计算竞争;如果 b、设备延迟和业务读取耗时同时上升,才更支持 I/O 方向。它们是排查分支,不是严格等价关系。
如果 sy 高,可能涉及系统调用、网络栈、内核同步等;需要 strace 或内核/CPU profiling 的进一步证据,不能直接建议“换更快 CPU”。
# 只输出一次累计事件/内存概览,不能当一秒采样 |
-S 不改变所有块 I/O 字段的单位。分析时应保存实际工具版本和表头,尤其是在比较网上截图与自己的输出时。
最小练习:在四份输出中标记哪一份不能拿来代表当前一秒;再找出瞬时数量与速率各两列。说明为什么 swpd=非零 与 so=0 完全可以同时成立。
iostat:队列、延迟与吞吐一起看
先定位设备,再谈饱和
iostat -dx -y 1 3 |
-d 关注设备,-x 显示扩展字段,-y 跳过启动以来的首份报告,-m 切换吞吐显示单位。设备可能是分区、逻辑卷、虚拟块设备或物理盘,不能把每一行都当独立硬盘。sysstat iostat 手册
容器可能看不到任何有用设备,或者看到虚拟机共享的设备指标。空表、零 I/O 和其他工作负载带来的变化都是真实可能性;这里不人为补造一组“磁盘瓶颈”数值。
三个量分别回答不同问题
r/s、w/s 反映合并后完成的请求速率;吞吐列反映字节速率。同样 1000 次请求,4 KiB 小块与 1 MiB 大块不是同样的吞吐和访问特征。
await 或分方向的 r_await、w_await 是平均请求完成耗时,包含队列等待和服务时间,不是纯硬件服务时间。它是平均数,不显示 P99;少量很慢请求可能被大量快请求稀释。
aqu-sz 是平均队列长度。它与延迟、请求速率一起看,帮助判断并发与积压;现代并行设备允许多个请求同时进行,队列不为零并不自动意味着系统病了。
假设在一段足够稳定、统计范围一致的样本里,完成速率约 1000 请求/秒、平均耗时约 0.004 秒,平均在途请求量的量级可能约为 4。这是用稳定系统关系进行数量级检查,不是强求每一秒截图都严格满足等式,更不是从平均值推导尾延迟。
%util=100% 不等于 SSD 已到极限
%util 近似描述采样窗口中设备有 I/O 活动的时间比例。对串行处理请求的设备,接近 100% 常有参考价值;但对 RAID 和现代并行 SSD,它不是“已使用硬件最大能力百分比”,上游手册明确提醒不能据此判断性能极限。
同样的 100%,可能来自一个很慢的串行请求,也可能来自许多高效并行请求。判断饱和要结合队列、延迟、吞吐、工作负载请求大小与设备能力,不应仅据一个字段决定换盘。
设备计数来自内核块层。父设备与分区、device-mapper 与底层盘可能重复反映同一 I/O 路径,把所有行相加可能重复统计。计数器还可能在重启或设备重置后归零,做差时要识别重置。Linux 内核 I/O 统计
最小练习:比较两种读法
先运行不带 -y 的版本,再运行带 -y 的版本,指出哪一份含启动以来统计。观察设备名,说明你的容器看到的是哪一层,以及你还缺什么证据才能映射到宿主 SSD。
iostat -dx 1 3 |
不要为演示执行对裸块设备的写入测试。需要受控 I/O 负载时,只在实验批准的独立文件中使用有限大小,明确缓存、文件系统与持久化语义;普通读命中页缓存时,没有明显设备 I/O 也是合理结果。
sar:把当前截图延伸成时间线
安装工具不等于已经有历史
sar 能实时采样,也能读取之前由 sysstat 收集的数据。如果以前没启用采集,就不能在今天安装后询问“昨天 14 点发生了什么”。默认历史目录和调度方式随发行版配置而异,容器也常没有 systemd 或 cron 常驻采集器。sysstat 项目说明
实时采样可以先只看需要的维度:
sar -u 1 3 |
CPU、内存、运行队列、设备和网卡分别回答不同问题。-u 的整机平均可能掩盖单核瓶颈,需要时再选 -P ALL;-n DEV 的吞吐也不等于丢包、重传和业务错误率。
观察重点是采样时间、间隔与字段,而不是一份稳定“答案”。演示采几个样本只证明采集流程,不代表已经开启长期历史监控。
自己保存一段受控记录
# 在实验可写目录保存本次记录,不覆盖已有事故文件 |
-o 保存的是二进制活动文件,-f 用于读取;它不是把表格重定向到文本文件的同一件事。某类活动是否可读,取决于采集阶段是否保存了对应数据。迁移时还要关注 sysstat 文件格式兼容性和版本。sar 上游手册
历史分析时可以按时间范围裁出事故附近:
# 时间只是示意;应替换为实际记录覆盖的窗口 |
如果本次样本不在这个窗口,空输出是正常结果。分享记录时应同时记录日期、时区、主机身份和是否重启;只写“14:05 高了一下”无法对齐应用日志。
从相关性走到因果假设
假设应用延迟在 14:05 上升,同时 sar 显示 swap-out 和设备延迟增加,这支持去检查内存压力与换页链路,但不能直接证明是应用泄漏造成的。还要找哪个进程增长、是否有并发任务、容器限额是否变化。
平均一分钟一次的历史记录,可能完全抹平 200 毫秒的控制循环抖动。排查短暂问题时,采样频率必须能回答该时间尺度的问题;但盲目提高频率又会增加开销和数据量。
最小练习:生成一份只含数个样本的二进制记录,换一个新终端读取 CPU 与内存字段。再选择不覆盖该记录的时间窗口,解释为什么“查不到”不等于系统在那段时间没有工作。
容器内的数字属于谁
/proc 全局视图与 cgroup 限额分开
容器里的 free、vmstat 或 iostat 可能读取虚拟机或主机级内核统计,而进程实际受 cgroup 限制。Docker Desktop 还有一层 Linux 虚拟机:你看到的 Linux MemTotal,不必等于 macOS 物理内存,更不必等于单容器限额。
先识别当前 cgroup 版本和路径:
cat /proc/self/cgroup |
在已经确认挂载根目录就是当前 cgroup 的 v2 容器中,可以只读查看:
cat /sys/fs/cgroup/memory.current |
其他挂载方式需要根据实际 cgroup 路径定位,不能把这些路径当成所有 Linux 的固定承诺。v1 接口不同,文件不存在不表示资源没有限制。
memory.current 是该组及其子组的使用计量,memory.max 是限制或 max;memory.events 可提供达到限制、OOM 等事件线索。cpu.max 的配额/周期与 cpu.stat 的节流计数,有助于解释“整机很空但本容器排队”。这不是把 free 的 total 简单替换成限额就完成所有计算。Linux cgroup v2 官方文档
配额也可以做一个小计算
假设某个 cgroup 的 cpu.max 是下面这个教学数值,而不是本次实验的固定输出:
200000 100000 |
它表示每 100000 微秒周期允许合计使用 200000 微秒 CPU 时间,平均相当于 2 个 CPU 的时间预算。进程可以在多个 CPU 上并行消耗预算,然后在周期剩余时间受到节流;它不表示已经被固定绑到两个特定核心。
因此,宿主显示有 16 个逻辑 CPU,并不能让这个 cgroup 自动获得 16 核预算。还需检查祖先 cgroup 的限制与 cpuset 等约束,不能只读一个叶子文件就宣布最终可用能力。
练习时比较一段采样前后的节流计数增量,再对齐请求延迟。历史累计节流次数非零,只能说明以前发生过,不能证明当前这次延迟就是节流造成的。
PSI 补上“是否真的等资源”
若内核支持,可以查看压力指标:
cat /proc/pressure/cpu |
PSI 的 some 描述至少部分任务因资源不足而停顿的时间比例;full 关注更严重的同时停顿情形,具体资源和系统层级的定义有差异。avg10、avg60、avg300 是相应时间窗口,total 为累计停顿时间。内核 PSI 文档
如果关心单个容器,应查看其 cgroup 对应的 pressure 文件,而不是拿整机 PSI 给某个容器直接定罪。文件或字段不可用时保留限制说明,不用补零伪装“没有压力”。
联合排障与练习
场景一:内存看起来很多,却发生 OOM
先保存 free,再看 cgroup 内存使用、上限与 OOM 事件变化;核对进程退出原因及系统日志。若 cgroup 限额先触发,宿主还有大量 available 完全可能同时成立。
下一步检查进程内存增长、映射类型与输入规模。只有确定生命周期问题,才进入泄漏检测;若是合法工作集超过配置,则应分析容量、批量大小或限额,不要一律称为内存泄漏。
场景二:CPU 不忙,控制节点却很慢
用 vmstat 看间隔状态,用 iostat 看具体设备,用 sar 对齐历史;再回到 进程排障 的调用栈与系统调用,检查它是在等待数据、锁、设备还是外部服务。
若只有 cgroup 节流计数持续增加,要优先分析 CPU 配额和工作负载;若设备延迟稳定、线程在条件变量里等待,则不应该因为曾看到一次 wa 就选择磁盘方向。
交付一份真正有用的监控记录
- 时间:日期、时区、采样起止、间隔、是否包含启动平均。
- 对象:主机/虚拟机/容器、设备层级、cgroup 路径和限额。
- 版本:工具版本、内核、字段表头及单位。
- 业务:同时刻延迟、吞吐、错误率及输入规模。
- 证据:哪些字段一起变化,哪些关键字段没有变化。
- 推理:当前假设、反例、下一步最小验证以及未测边界。
最终练习是拿同一段记录回答三个问题:资源用了多少?资源是否限制了任务推进?具体哪个工作负载导致了问题?四个工具提供的是不同层次的证据,不能用第一个问题的答案替代后两个。