性能之巅第3章:CPU性能分析

📑 目录

CPU是系统的"心脏",但很多人只会看top里的百分比数字。真正的高手,懂得用火焰图看见时间花在哪里,用perf数清每个周期的去向。


第3章 CPU性能分析:从top到火焰图的进阶之路

3.1 故事:那个让全公司加班的"慢查询"

2024年3月,某物流平台的轨迹查询接口突然变慢。平时200ms的响应,飙到了8秒。

DBA小赵第一时间查数据库:"SQL执行计划没变,索引也走了,数据库CPU只有20%。不是数据库问题。"

后端开发小钱看了应用日志:"代码也没改,上周还好好的。"

两人僵持不下,决定一起看火焰图。

perf采集了60秒数据,生成的火焰图让两人同时倒吸一口凉气——java.util.HashMap.get()占了35%的CPU时间。

"HashMap.get()? 这不可能啊,就是个简单的查询。"小钱不解。

继续放大,发现get()下面连着一长串:hash()ThreadLocalRandom.next()Unsafe.getLong()

问题浮出水面:新版本的JDK在某个更新中改了ThreadLocalRandom的实现,在特定并发场景下产生了严重的伪共享(False Sharing)。HashMap在并发读取时,多个线程的随机数生成器在相邻的内存位置竞争缓存行。

根因:JDK版本升级引入的性能退化,表现为"HashMap变慢"。

修复:降级JDK版本 + 改用ConcurrentHashMap(内部实现不同,避开了这个问题)。响应时间恢复到180ms。

教训:没有火焰图,两个人可能吵到第二天也找不到原因。


3.2 CPU性能指标全景

graph TD
    A[CPU性能指标] --> B[利用率]
    A --> C[饱和度]
    A --> D[错误]
    
    B --> B1[user% 用户态]
    B --> B2[sys% 内核态]
    B --> B3[iowait% 等待I/O]
    B --> B4[steal% 被虚拟机偷走]
    B --> B5[idle% 空闲]
    
    C --> C1[load average]
    C --> C2[runnable队列长度]
    
    D --> D1[Machine Check]
    
    style B fill:#e3f2fd
    style C fill:#fff3e0
    style D fill:#ffebee

关键指标解读

# top 输出解读
top - 14:32:01 up 30 days
Tasks: 234 total
%Cpu(s): 25.0 us, 5.0 sy, 0.0 ni, 65.0 id, 5.0 wa

# us(user):用户态CPU,应用程序消耗
# sy(system):内核态CPU,系统调用消耗
# id(idle):空闲CPU
# wa(iowait):等待磁盘I/O的CPU时间
# st(steal):被hypervisor分配的给其他VM的时间

关键洞察

  • us高 → 应用代码热点,看火焰图
  • sy高 → 系统调用频繁,看syscall
  • wa高 → 磁盘I/O瓶颈,看iostat
  • id低 + load高 → 可能是锁竞争或大量 Runnable 线程

3.3 负载平均值(Load Average)

什么是Load Average?

graph LR
    A[Load Average] --> B[1分钟平均]
    A --> C[5分钟平均]
    A --> D[15分钟平均]
    
    E[含义] --> F[正在运行的进程]
    E --> G[等待运行的进程]
    E --> H[不可中断睡眠的进程]
    
    style B fill:#ffebee
    style C fill:#fff3e0
    style D fill:#e8f5e9

Load = 正在运行 + 等待运行 + 不可中断睡眠的进程数

# 查看负载
uptime
# 14:32:01 up 30 days, load average: 2.50, 2.30, 2.10
#                                     1min   5min   15min

# 理想情况:load ≈ CPU核心数
# load > CPU核心数 → 有进程在排队(饱和度)
# load < CPU核心数但id很低 → 可能在等I/O或锁

解读技巧

场景负载CPU使用率含义
健康系统空闲
CPU瓶颈计算密集型,需要扩容或优化
I/O瓶颈进程等磁盘,wa会高
锁竞争进程等锁, Runnable 多但CPU没充分利用

3.4 perf:Linux性能分析神器

perf能做什么?

mindmap
  root((perf
能力)) 硬件事件 CPU周期 缓存命中/miss 分支预测 软件事件 上下文切换 页错误 系统调用 跟踪点 内核事件 调度事件 文件系统事件 动态探针 uprobes kprobes

常用perf命令

# ===== 基础采样 =====
# 采集CPU热点(生成火焰图数据)
perf record -F 99 -a -g -- sleep 60

# 查看报告(TUI界面)
perf report

# 实时热点Top
perf top

# ===== 计数模式 =====
# 统计程序运行期间的事件数
perf stat -e cycles,instructions,cache-references,cache-misses ./program

# 输出解读:
#  cycles: CPU周期数
#  instructions: 指令数
#  IPC (instructions per cycle) = instructions / cycles
#  IPC < 1 → CPU在等待(缓存miss、分支预测失败等)
#  IPC > 1 → 指令级并行好

# ===== 跟踪系统调用 =====
perf trace -e 'syscalls:sys_enter_*' ./program

# ===== 查看调度延迟 =====
perf sched record -- sleep 10
perf sched latency

# ===== 查看缓存行争用 =====
perf c2c record -a sleep 10  # 需要特定内核配置
perf c2c report

3.5 火焰图实战

生成火焰图的完整流程

graph TD
    A[perf record
采集样本] --> B[perf script
输出文本] B --> C[stackcollapse
折叠栈] C --> D[flamegraph.pl
生成SVG] D --> E[浏览器打开
交互分析] style A fill:#e3f2fd style D fill:#e8f5e9 style E fill:#fff3e0
# 一键生成火焰图
git clone https://github.com/brendangregg/FlameGraph
cd FlameGraph

# 采集(-F 99:每秒99个样本,避免与100Hz定时器对齐)
sudo perf record -F 99 -a -g -- sleep 30

# 生成(管道方式,省磁盘)
sudo perf script | \
  ./stackcollapse-perf.pl | \
  ./flamegraph.pl > flamegraph.svg

# 差分火焰图(对比优化前后)
sudo perf record -F 99 -a -g -- sleep 30
sudo perf script | ./stackcollapse-perf.pl > out.folded1
# ... 修改代码 ...
sudo perf record -F 99 -a -g -- sleep 30
sudo perf script | ./stackcollapse-perf.pl > out.folded2

./difffolded.pl out.folded1 out.folded2 | ./flamegraph.pl > diff.svg
# 红色:第一版有、第二版减少的(优化掉的部分)
# 蓝色:第二版新增的

火焰图分析技巧

# 火焰图上的关键信息
# 1. 平顶(宽且顶部平):实际消耗CPU的函数
# 2. 尖塔(窄但高):调用链深,但本身不占时
# 3. 颜色:随机分配,无特殊含义
# 4. 宽度 = 该路径下所有样本占总样本的比例

示例分析

████████████ java.util.HashMap.get 35.2%
    ████████ java.lang.ThreadLocalRandom.next 20.1%
        ████ sun.misc.Unsafe.getLong 12.3%

→ HashMap.get()是热点,但其中超过一半的时间花在生成随机数上。这很奇怪,值得深挖。


3.6 Off-CPU分析

CPU分析只能看到"在CPU上干什么"。但如果进程在等锁、等I/O、等网络呢?

graph LR
    A[进程状态] --> B[On-CPU
正在执行] A --> C[Off-CPU
等待中] B --> D[火焰图分析] C --> E[Off-CPU火焰图] E --> F[等锁] E --> G[等I/O] E --> H[等网络] E --> I[等定时器] style B fill:#e8f5e9 style C fill:#ffebee
# 使用eBPF采集Off-CPU时间(需要bcc或bpftrace)
sudo offcputime-bpfcc -df 30 > out.offcpu
./flamegraph.pl --color=io < out.offcpu > offcpu.svg

# bpftrace方式
sudo bpftrace -e '
tracepoint:sched:sched_switch {
    $prev = args->prev_comm;
    $next = args->next_comm;
    @start[$prev] = nsecs;
}

kprobe:finish_task_switch {
    if (@start[comm]) {
        @us[comm, kstack] = (nsecs - @start[comm]) / 1000;
        delete(@start[comm]);
    }
}
'

3.7 调度延迟分析

进程从"可运行"到"真正运行"要等多久?

# 采集调度事件
sudo perf sched record -- sleep 10

# 查看延迟统计
sudo perf sched latency

# 输出示例:
# ------------------------------------------------------------------------------
#  Task                  |   Runtime ms  | Switches | Avg delay ms | Max delay ms
n# ------------------------------------------------------------------------------
#  java:1234             |   1234.56     |   5678   |    0.05      |   12.34
#  ...
# 
# Avg delay:平均等待时间
# Max delay:最坏情况(可能意味着优先级问题或CPU过载)

调度延迟高的常见原因:

  1. CPU过载:Runnable队列太长
  2. 优先级问题:低优先级进程被饿死
  3. CFS参数:sched_min_granularity_ns太小,切换太频繁
  4. NUMA问题:进程在等远程内存

3.8 本章总结

graph TD
    A[CPU性能问题] --> B{看top}
    B -->|us高| C[火焰图找热点函数]
    B -->|sy高| D[perf trace找syscall]
    B -->|wa高| E[iostat找磁盘]
    B -->|load高| F[看Runnable队列]
    
    C --> G[优化算法/数据结构]
    D --> H[批量处理减少syscall]
    E --> I[优化I/O或减少访问]
    F --> J[减少线程/优化锁]
    
    style C fill:#e8f5e9
    style D fill:#e8f5e9
    style E fill:#e8f5e9
    style F fill:#e8f5e9

核心工具链

场景工具命令
整体概览top/vmstattop, vmstat 1
单核分布mpstatmpstat -P ALL 1
进程CPUpidstatpidstat -u 1
热点函数perf+火焰图perf record -F 99 -a -g
系统调用perf traceperf trace -e syscalls:*
调度延迟perf schedperf sched record
Off-CPUbcc/bpftraceoffcputime-bpfcc

"CPU分析不是找谁用了最多的CPU,而是找CPU时间在做什么——以及不在CPU上的时间在等什么。"