做了这么多年性能测试,我越来越觉得内存分析是三大件里最容易被“差不多”糊弄过去的一环。CPU 飙起来一眼就能看见,磁盘 IO 慢下来监控曲线也骗不了人,唯独内存,很多人压测时就是敲一下 free,看到 used 不高就放行了,结果压测跑到一半 TPS 莫名掉底,再回头看 vmstat 和 sar 才追悔莫及。
这篇文章想把我日常压测里用 free、vmstat、sar 做内存分析的方法完整讲一遍:每个工具到底在告诉你什么、哪些字段才是真正需要盯的、三个工具怎么串成一条排查链路。适合同样在做压测但被内存问题绕晕的测试开发同学,也适合刚接触性能测试、想建立系统资源分析体感的新人。文章不会只停留在“参数是什么”的层面,我会把内核在背后做的那些事一起讲透,因为看不懂背后逻辑,你就永远只能背参数。
1. free 命令:一次快照背后是内核的缓存与回收策略
1.1 逐列拆解 free 的输出:真正的主语是“可回收”
拿最普通的free -h输出说话:
$ free -h total used free shared buff/cache available Mem: 15Gi 7.4Gi 987Mi 356Mi 7.5Gi 7.7Gi Swap: 8.0Gi 0B 8.0Gitotal 是物理内存总量,内核在启动阶段探测后基本固定。used 的官方定义是total - free - buff/cache,也就是说 used 里并不包含 buff/cache 部分。free 是真正“没人碰过”的空闲页,而 shared 指的是 tmpfs 这类文件系统占用的内存,多进程共享内存也会体现在这一列。
但我最想让压测同学先改掉的习惯是:第一眼看 available,不是看 free,更不是看 used。available 是内核 3.14 之后才加入的估算值,含义是“在不触发明显 swap 的前提下,新进程还能申请到多少内存”。内核算这个数的时候会统计可回收的 page cache、可回收的 slab,扣除正在被使用的内核内存,所以它通常比 free 大得多,也更接近“真实还能用的内存水位”。
压测中最常见的第一类误判,就是把 free 列当成“剩余内存”。一台机器 free 列只剩 200MB,但 available 还有 12GB,这时候你根本不用慌;反过来,free 列剩 4GB,available 只有几百 MB,那问题已经迫在眉睫了。两者的差异,本质上就是内核有多少缓存页“随时可以被回收”。
1.2 buff/cache 不是“已用内存”,而是内核的弹性缓冲池
很多初学性能测试的人第一次看到 buff/cache 占了快一半内存,都下意识觉得“完了,内存被吃光了”。其实恰恰相反,buff/cache 是内核主动做的一次“投资”:空闲内存闲着也是闲着,不如拿来缓存读过写过的文件块,下次再用的时候直接命中,不用重新走磁盘。
buffer 和 cache 在早期内核里分得比较清楚:buffer 主要面向块设备,缓存文件系统元数据和脏块;cache 主要面向文件页缓存。现代内核里两者边界已经很模糊了,所以 free 从 3.x 开始干脆合并成 buff/cache 一列。
这个设计有点像家里囤日用品:冰箱里囤的菜不是浪费,而是在降低“下次买菜”的成本。真到没米下锅的时候,你也能把囤的菜吃掉再出门——这些缓存页就是可以“吃”的那部分。
内核在内存紧张时通过 shrink 机制回收 page cache,把脏页写回磁盘,腾出物理页给用户程序使用。所以 buff/cache 高不等于内存有压力,相反它在大多数情况下是好事。真正要警惕的信号是 available 持续走低、同时 swap 区域开始出现动静,那说明系统连“可回收的弹性空间”都快掏空了,这正好接上下面的实战内容。
1.3 free 的实战姿势:频率、参数和数据解读习惯
压测期间 free 不能只敲一次。它更像一张照片,而压测是两三个小时的过程,你要的是连续照片组成的相册。我习惯这样用:
free -h -s 5每 5 秒刷新一次,既能感知趋势,又不至于输出太密。需要记录归档的时候,我会把它接到 shell 循环里带上时间戳:
while true; do echo "---- $(date '+%F %T') ----"; free -h; sleep 5; done >> free_trend.log如果觉得输出太宽,可以试试free -w,它会把 buffer 和 cache 单独拆开,排查文件系统相关缓存问题时更清楚。
还有两个容易踩的坑。第一个,free 命令的默认单位在不同版本、不同发行版上不一样,有的显示 KiB,有的显示 GiB,脚本化解析时建议用free -k或free -b固定单位,否则线上和本地结果对比,一个单位差异就可能把你带到沟里去。第二个,老版本内核(3.14 以前)没有 available 列,很多老旧环境的输出解读方式和新环境完全不同,跨环境对比前先确认一下版本。
再强调一次,free 有天然短板:它只告诉你“系统总共还剩多少内存”,不告诉你“哪个进程在吃内存、吃得有多快”。所以 free 适合做第一眼判断,不适合做根因定位。要看具体进程,还得靠后面的 vmstat 和 sar 把问题一层层剥开。
2. vmstat:把内存状态变成速率,盯住 si/so 的动态变化
2.1 两段式输出的结构与第一行的陷阱
vmstat 和 free 最大的不同,是它把系统状态变成了“速率”。默认执行vmstat时,输出分两段:
$ vmstat 1 3 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 655348 1203456 34860 68812124 0 0 3 18 120 130 14 6 80 0 0 1 0 655348 1203340 34860 68812124 0 0 0 0 125 135 15 7 78 0 0 1 0 655348 1203312 34860 68812124 0 0 0 0 131 140 16 7 77 0 0第一行比较特殊,它显示的是自开机以来的平均值,后续每一行才是采样时间窗内的瞬时值。很多人直接拿第一行当“当前状态”读,这是新手最常犯的错误。看动态趋势必须跳过第一行,从第二行开始。
memory 区的 swpd、free、buff、cache 四项和 free 命令含义一致,单位是 KB。不过对压测来说,vmstat 里更值得盯的其实是另外几组:swap 区的 si/so、io 区的 bi/bo、system 区的 in/cs、cpu 区的 wa/st。它们组合起来,才能还原“内存压力”的完整现场。
2.2 si/so 才是内存压力的体温计
si 是 swap-in 的速率,so 是 swap-out 的速率,单位是 KB/s,表示每秒从交换分区换入或换出多少数据。这两个数是我在内存分析里最先盯的指标:正常情况下都应该趋近于 0,偶尔出现一次两次小波动没大问题。一旦 si 和 so 开始持续非零,基本可以断定物理内存已经撑不住了。
打个比方,swap 就像一个临时工仓库。内存里放不下的匿名页会被赶到这个仓库里,等真正要访问的时候再从仓库搬回来。搬进搬出都需要时间和磁盘 IO,而“搬”的过程就是 so 和 si。CPU 在等数据搬回来,于是你会看到 cpu 列里的 wa 升高,进程响应时间被拉长,压测场景下直接表现就是 TPS 下跌、P95 变大。
实际操作中判断内存压力的标准,我一般这么看:
- si/so 偶尔小幅度抖动:可能只是瞬时波动,继续观察。
- si/so 持续非零且数值在变大:内存真实不足,内核在积极换页。
- swpd 持续上涨且居高不下:已有大量匿名页被换出,后续访问这些页时还需要重新换入,压力在累积。
顺手补充一个底层对应关系:vmstat 本身不凭空造数据,它本质上是读取并计算/proc/meminfo和/proc/vmstat这两个虚拟文件。想深入排查时,可以直接去/proc/vmstat里查 pgscan、pgsteal 这类字段,它们比 vmstat 的摘要更接近内核回收机制的真实动作。
2.3 队列、IO 与内存:vmstat 里容易被忽略的交叉信号
vmstat 的价值还在于它能把内存和 CPU、IO 之间的联动呈现出来。我遇到过这种场景:free 显示 available 还行,但 vmstat 的 r 列(运行队列)持续上涨,wa 列也在抬头。如果只盯着内存看会觉得很莫名,把几列连起来就明白了——内核正在回收内存,回收过程要写回脏页,于是 bi/bo 出现明显波动,磁盘变忙,任务排队,r 列上升。
这类联动经常是成串出现的:si/so 大于 0 往往伴随 bi/bo 上浮,因为换出脏页和读回换入页都要走磁盘;wa 升高让任务执行变慢,运行队列堆积;运行队列堆积又让 CPU 的 sy 比例抬升。你在压测报告里看到的每一条曲线,本质上都是这套联动关系的对外表现。
压测期间我建议用vmstat 1 5开头,先取 5 个一秒样本看波动幅度;遇到可疑窗口再加长采样,比如vmstat 1 60。关键不是采样多久,而是采样窗口和压测脚本的业务阶段对齐:压测是阶梯加压还是持续恒压?峰值出现在第几分钟?这些时间点如果不记录,后面分析 vmstat 数据就只能靠猜。
3. sar:压测全程的历史回放能力是内存分析的底气
3.1 sysstat 的采集链路与数据归档机制
free 和 vmstat 的一个共同短板,是“你盯着它的时候它才有数据”。可是内存异常往往不会按照你盯着的时间窗口出现——压测跑了两个小时,到第三个小时才出问题;或者凌晨批量任务把内存吃光,第二天早上你才发现。这时候如果没有历史数据,一切只能靠猜。sar 存在的意义,就是解决这个问题。
sar 是 sysstat 工具包里的核心命令。sysstat 装上之后会通过 cron 后台任务周期性调用 sadc 采集系统状态,采集结果写入/var/log/sysstat/saXX(XX 是当天日期,部分发行版目录是/var/log/sa/)。sar 命令本身做的事情其实就是读取并解析这些归档文件。所以哪怕你忘记了手动监控,只要 sysstat 服务是开着的,数据就在持续积累。
默认采集间隔通常是 10 分钟一次,看系统常识够了,对性能测试来说太粗。压测场景我会把采集间隔改成 60 秒,方法是修改/etc/cron.d/sysstat里 sa1 的调用参数:
# 默认 */10 * * * * root /usr/lib64/sa/sa1 1 1 # 改成每分钟 * * * * * root /usr/lib64/sa/sa1 1 1Debian/Ubuntu 的二进制路径可能是/usr/lib/sa/sa1,原理一样。改完重启 crond 生效。另外,sa 文件不会无限保留,默认保留天数在/etc/sysconfig/sysstat(部分发行版是/etc/sysstat/sysstat)的 HISTORY 参数里,长周期压测项目记得提前调大。
3.2 内存相关三件套:-r、-B、-W
sar -r是内存使用率的主视图,重点看 %memused、kbmemfree、kbbuffers、kbcached,还有一个容易被忽略的 kbcommit 和 %commit。
kbcommit 对应/proc/meminfo里的 Committed_AS,含义是当前所有进程已经向系统“承诺”的虚拟内存地址空间总和。它本质上是一个记账值,系统根据它决定还能不能给新进程分配内存。如果 %commit 长期高企甚至超过 100%,即使 free 显示 available 不错,内存分配也会变得风险很高——默认 overcommit 策略下内核允许一定程度超卖,但超卖严重时,某些分配请求会失败或被 OOM Killer 优先清理。我压测一个 Java 服务时遇到过这类情况:物理内存还剩一半,%commit 已经爬到 170%,新连接创建的线程栈在分配时就报错,应用异常率陡增。
sar -B看分页统计,重点看 pgpgin/s、pgpgout/s、pgmajfault/s 和 vmeff%。pgmajfault 是主缺页次数,意味着一个虚拟页不在物理内存里,必须去磁盘拿数据或者重新建立映射。这个值正常运行时应该非常低。压测中如果 pgmajfault 持续明显上升,说明内存可用页太少,程序在不断踩缺页,这是内存压力的典型信号。vmeff% 是页回收有效率,低于 80% 说明内核在“低效地折腾”,通常对应频繁回收但效果不佳的状态。
sar -W是交换统计,pswpin/s 表示每秒换入的页数,pswpout/s 表示换出的页数。它和 vmstat 的 si/so 是同一件事的两个视角:si/so 按 KB 算,sar -W 按页数算。默认页大小通常 4KB,换算关系一目了然。压测复盘里靠它确认 swap 活跃的持续时间,比 vmstat 临时抓几个样本要可靠得多。
3.3 用 sar 做压测窗口切片回放
压测项目结束后,最常用的操作是圈定时间窗口。比如压测从 14:00 开始,15:30 出现波动,15:40 恢复,我可以一次命令把全过程的 %memused、%commit、pgmajfault、pswpout 都翻出来:
sar -r -f /var/log/sysstat/sa15 -s 14:00:00 -e 16:00:00 sar -B -f /var/log/sysstat/sa15 -s 14:00:00 -e 16:00:00 sar -W -f /var/log/sysstat/sa15 -s 14:00:00 -e 16:00:00拿到输出之后,我会把时间轴对齐到 Jmeter 或 LoadRunner 的 TPS 曲线上,观察内存指标和业务指标的变化顺序。是 TPS 先掉、内存后涨,还是内存先涨、TPS 后掉?这个先后顺序非常关键:前者说明业务接入量变化是主因,后者说明内存累积到某个阈值后触发了业务退化。
到这里,三个工具各自的定位已经比较清楚了:
| 工具 | 定位 | 时间维度 | 主要回答的问题 |
|---|---|---|---|
| free | 系统快照 | 当前瞬时 | 现在还剩多少可分配内存 |
| vmstat | 实时抽样 | 秒级动态 | 当前内存与 CPU、IO、swap 的联动状态 |
| sar | 历史归档 | 分钟级回放 | 某一段压测窗口里到底发生了什么 |
4. 一次真实内存异常的完整排查链路:free、vmstat、sar 如何配合
4.1 现象:TPS 掉底,free 却显示内存“很健康”
之前有一个真实的压测场景,特别适合拿来说明三个工具的联动方式。被测对象是一个电商后端的订单查询服务,Jmeter 从 200 线程起压,前 30 分钟 TPS 稳定在 1200 上下,可用性很好。到第 40 分钟左右,TPS 开始无规律下跌,最差掉到 400,然后自己又恢复,反复震荡,P95 明显拉长。
第一反应自然是查系统资源。我敲了一下free -h:
$ free -h total used free shared buff/cache available Mem: 31Gi 18Gi 4.2Gi 1.2Gi 8.8Gi 12Gi Swap: 16Gi 2.3Gi 13Giavailable 还有 12Gi,swap 只用了 2.3Gi,单看这一瞬间确实“健康”。但有两个异常点:available 已经从压测刚开始时的 24Gi 降到了 12Gi,swap 也不是 0。单独一张快照没问题,放到时间轴上就是“已经走完一半下坡路”。这就是 free 的局限——它只负责告诉你现在站在哪里,不负责告诉你刚才发生了什么。
4.2 排查链路:free 快照 -> vmstat 实时 -> sar 回放
我立即开了vmstat 1 5盯实时状态,重点看 si/so 和 r、wa:
$ vmstat 1 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 8 0 2490504 3560012 80320 9231210 0 68 0 72 2100 4100 20 22 48 10 0 9 0 2490632 3559800 80320 9231210 0 72 0 80 2200 4300 20 23 45 12 0 7 0 2490656 3559710 80320 9231210 0 90 0 95 2100 4200 21 22 47 10 0so 持续非零,说明内存在持续把匿名页换到 swap;wa 有 10% 左右,CPU 有一部分时间在等内存换页。单看 r 列 7-9 不算高,但这是在 TPS 已经下跌的前提下——业务线程真正在跑 CPU 的时间变少了,大量时间消耗在换页和等 IO 上。到这里问题方向基本确定:物理内存不足,swap 被迫频繁介入。
接着用 sar 做全时段回放。把 14:00 到 16:00 的数据翻出来,重点看 %commit 和 pgmajfault。结果是 kbcommit 从最初的 18Gi 一路爬到 27Gi,%commit 从 110% 升到 170%;pgmajfault 在 TPS 下跌前的 10 分钟就开始明显爬坡。这说明问题不是“某一瞬间内存不够”,而是一个渐进累积过程:虚拟内存承诺持续上涨,物理内存回收压力逐步增大,主缺页增加,业务线程频繁被换页打断,TPS 因此掉下来。
再配合sar -B里的 pgpgin/s,波动窗口平均接近 1800 页每秒,约 7MB/s 的页输入速率,和 wa 升高的现象完全对得上。整条链路闭环了。
4.3 进入应用层:堆外内存、本地缓存与 RSS
到了这一步,问题已经定性为“物理内存相对不足”,但还没回答最关键的“为什么”:是谁把内存吃掉的?
我在另一个终端用ps aux --sort=-rss看了进程排行,一个 Java 进程的 RSS 从压测初期的 6Gi 涨到了 12Gi。接着用jstat -gcutil看堆内指标,GC 频率和暂停时间没有明显恶化,说明问题很可能出在堆外——本地缓存、线程栈或者 JNI 分配的 Native 内存。
翻代码后定位到一个“临时方案”:为了性能,把一批订单数据放进了 Guava Cache,key 按用户维度拆分。但每个用户的查询条件都不一样,缓存几乎命中不上,新对象反而不断被塞进去,堆外内存被越顶越高。而且这段代码上线后没有加容量上限,完全是无界缓存的用法。根因清楚了:应用层无界缓存导致 RSS 持续增长,最终把物理内存和 swap 一起拖垮。
修复思路分两层。应用层给 Cache 加 maximumSize 和过期策略,把 JVM 的-Xmx和容器上限对齐;系统层在测试环境验证合适的 vm.swappiness 值,这个场景我们希望优先使用 page cache 而不是激进换出匿名页,所以从默认 60 调到 10,具体数值看业务特性逐步试。改完重新压测,free 的 available 稳定在 20Gi 上下,vmstat 的 si/so 归零,sar 回放全时段 %commit 不再单调上涨,TPS 曲线恢复成 1200 的平直线。
4.4 几个容易误判的侧面场景
最后补充几个压测内存分析里特别容易翻车的侧面场景。
第一个是 cgroup 限制环境。现在大量服务跑在容器里,你敲 free 看到的是宿主机视图,而容器实际能用的上限是 cgroup 限制值。两种视图差异可以很大:宿主机 available 还剩 30Gi,容器 limit 只有 4Gi,Java 堆直接超限被 OOMKilled。在容器环境分析内存,第一件事是确认cat /sys/fs/cgroup/memory.max(老版本是 memory.limit_in_bytes),再看容器内进程实际占用,主机的 free 输出反而是干扰项。
第二个是内存回收导致的延迟毛刺。有时候 si/so 都是 0,P99 还是时不时飙一下,这就要怀疑内核的 direct reclaim(直接回收)。它在进程申请内存的路径上阻塞等待回收,表现为一次短暂的停顿。可以通过/proc/vmstat里的 direct_reclaim_success 和相关字段确认。出现这种毛刺,单纯加内存不一定有效,要查是不是有进程一次性申请了大块内存,或者透明大页导致分配路径变慢。
第三个是 swappiness 的使用误区。很多人一看内存紧张就立刻把 vm.swappiness 改成 0,认为“不用 swap 就安全”。实际不是这样:swappiness=0 只是让内核尽量不主动换出匿名页,不代表禁止换出;而且物理内存真的耗尽时,禁用 swap 只会让内核更快走 OOM Kill 路线,对在线业务杀伤更大。调优的前提是搞清楚瓶颈在哪,而不是把参数当成开关乱拨。
如果你现在只想给自己搭一套“先跑起来”的内存监控基线,我直接把平时用的组合贴出来:
nohup vmstat 1 60 > vmstat_trend.log 2>&1 & nohup bash -c 'while true; do echo "---- $(date "+%F %T") ----"; free -h; sleep 60; done' > free_trend.log 2>&1 & sar -r -B -W -f /var/log/sysstat/sa$(date +%d) > sar_memory_summary.txt 2>&1这套组合我已经用了很多年,压测项目里十有八九的内存问题都能在半小时内定性:free 负责告诉你“现在怎样”,vmstat 负责告诉你“正在发生什么”,sar 负责告诉你“到底从什么时候开始的”。三者合起来,内存分析就不再是玄学。