2. 内存带宽瓶颈识别:用 perf stat、sar、vmstat 初步判断
好,咱们进入正题。内存带宽不足这事儿,很多时候不是一眼能看出来的。CPU 利用率不高、业务响应变慢,你第一反应可能是“代码写得烂”或者“磁盘 IO 扛不住了”。但我在项目中遇到过好几次,折腾半天,最后发现是内存带宽把 CPU 给“饿”着了。
所以这一节,我带你用几个最常用的工具,先做个快速筛查。说白了,就是给系统把个脉,看看它是不是“吃不饱饭”。
2.1 为什么先看这些工具?
你想想看,排查性能问题,最忌讳一上来就上大杀器。perf、sar、vmstat 这三个工具,几乎每个 Linux 发行版都自带,或者很容易装上。它们能给你一个宏观的、初步的判断。
我个人习惯是:先跑一轮这些命令,看看有没有“异常信号”。如果有,再深入用 perf stat 去抓具体事件。如果没有,那大概率是别的问题,别在内存带宽上浪费时间。
核心思路: 内存带宽瓶颈的典型表现是——CPU 在大量时间处于“ stalled ”状态,也就是停顿等待数据。工具就是帮我们抓这个“停顿”的。
2.2 用 vmstat 看系统整体“饥饱”
vmstat 是我最常用的第一道手。它报告的是整个系统的统计信息,简单粗暴。
# 每隔 1 秒输出一次,共输出 5 次
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 |
| 2 0 | 0 1623456 1234 567890 | 0 0 | 10 20 | 1500 2000 | 30 15 55 0 0 |
看哪里?重点关注 cpu 列下的 wa(iowait) 和 system 列下的 cs(context switch)。
- wa 不高,但 us 也不高? 嗯,这里要注意。如果 CPU 利用率(us+sy)不高,但业务就是慢,而且 wa 也很低,那 CPU 很可能在“空转等待内存”。vmstat 不会直接告诉你,但这是一个重要的间接信号。
- cs(上下文切换)异常高? 比如每秒几万甚至几十万次。这通常意味着有大量线程在争抢锁或者资源。但有时候,内存带宽不足导致 CPU 停顿,操作系统会误以为 CPU 空闲,从而调度其他线程上来,反而加剧了切换。我曾经在一个数据库服务器上看到 cs 冲到 20 万,查了半天,最后发现是 NUMA 节点间内存访问延迟导致的带宽问题。
我的小技巧: 如果 vmstat 里的 r(运行队列)很大,但 us 却不高,比如 r=8 但 us=40%,这基本就是 CPU 在等数据。八九不离十是内存或者缓存的问题。
2.3 用 sar 看内存带宽的“影子指标”
sar 是系统活动报告器,它能记录历史数据。我个人习惯用 sar -B 来看分页活动,这跟内存带宽有很强的关联。
# 实时查看,每隔 1 秒一次
sar -B 1 3
输出关键列:
| pgpgin/s | pgpgout/s | fault/s | majflt/s | pgfree/s | pgscank/s | pgscand/s | pgsteal/s | %vmeff |
|---|---|---|---|---|---|---|---|---|
| 0.00 | 12.00 | 15.00 | 0.00 | 20.00 | 0.00 | 0.00 | 0.00 | 0.00 |
重点看 pgscank/s 和 pgscand/s。这两个是“页面回收”相关的指标。如果它们持续大于 0,尤其是 pgscand/s(直接页面扫描)很高,说明内存压力很大,系统在拼命回收内存页。这会导致严重的性能抖动,因为回收过程本身就要占用内存带宽。
为什么会这样?因为内存带宽不足时,数据在内存和 CPU 之间“跑不动”,导致缓存命中率下降,进而触发更多的缺页中断和页面回收。这是一个恶性循环。
避坑指南: 我曾经在排查一个 Java 应用时,看到 pgscand 很高,以为是内存泄漏。结果用 perf 一抓,发现是 JVM 的 GC 线程在疯狂扫描内存,而根本原因是 NUMA 内存分配策略不对,导致跨节点访问,带宽被严重拖累。所以,sar 的指标是“果”,不是“因”。
2.4 用 perf stat 抓“真凶”
好了,vmstat 和 sar 给了我们“嫌疑”,现在该 perf 上场了。perf stat 可以直接读取 CPU 的性能计数器,这是最直接的证据。
# 统计整个系统的 CPU 周期和 stalled 周期
perf stat -e cycles,stalled-cycles-frontend,stalled-cycles-backend -a sleep 5
输出示例:
Performance counter stats for 'system wide':
12,345,678,901 cycles # 2.47 GHz
4,567,890,123 stalled-cycles-frontend # 37.0% frontend cycles idle
6,789,012,345 stalled-cycles-backend # 55.0% backend cycles idle
5.001234567 seconds time elapsed
这里有两个关键指标:
- stalled-cycles-frontend:前端停顿。指 CPU 在等待指令获取(比如指令缓存 miss)。这通常跟代码体积大、分支预测失败有关,但也可能跟内存带宽有关(如果指令缓存被冲掉)。
- stalled-cycles-backend:后端停顿。指 CPU 在等待数据(比如数据缓存 miss、内存访问)。这个指标跟内存带宽直接相关。 如果它超过 40%,甚至 50% 以上,基本可以断定内存带宽是瓶颈。
我个人习惯是,如果 stalled-cycles-backend 占比超过 30%,我就会进一步用 perf stat -e LLC-load-misses,LLC-store-misses 去看 LLC(最后一级缓存)的 miss 情况。
perf stat -e LLC-load-misses,LLC-store-misses -a sleep 5
如果 LLC-load-misses 很高,说明数据在 LLC 里找不到,必须去内存里拿。这时候内存带宽的压力就非常大了。
一个简单的判断标准:
- stalled-cycles-backend < 20%:内存带宽大概率不是问题。
- stalled-cycles-backend 20% ~ 40%:有压力,需要进一步分析。
- stalled-cycles-backend > 40%:内存带宽很可能是主要瓶颈。
2.5 一张图理清思路
下面这张图,是我自己总结的排查流程。你照着走一遍,基本不会漏掉关键点。
2.6 小结一下
这一节的内容,说白了就是三板斧:
- vmstat 看宏观异常(r 队列 vs us 的背离、高 cs)。
- sar -B 看内存回收压力(pgscand)。
- perf stat 看 CPU 停顿(stalled-cycles-backend)。
这三步走完,你心里基本就有数了。如果三个工具都指向内存带宽,那别犹豫,下一节我们就要深入用 perf 去抓具体是哪个程序、哪段代码在“抢带宽”了。
嗯,记住,工具是死的,思路是活的。别光看数字,要结合你的业务场景去理解这些数字背后的含义。
最后提醒一句: 跑 perf stat 的时候,记得加上 -a 参数统计整个系统,或者用 -p 指定进程。别只盯着一个 CPU 看,内存带宽是全局资源。