Linux 性能排查与火焰图
性能分析方法论
USE 方法(Utilization / Saturation / Errors)
USE 方法是由 Brendan Gregg 提出的系统性能分析框架,适用于所有资源(CPU、内存、磁盘、网络)的快速排查。对每种资源,回答三个问题:
| 指标 | 含义 | 典型值 |
|---|---|---|
| Utilization(利用率) | 资源忙于服务的时间占比 | CPU 100% 表示满载;磁盘 %util 接近 100% 表示磁盘饱和 |
| Saturation(饱和度) | 资源上积压的等待队列长度 | CPU run queue > 核心数 × 2;内存 swap 使用量 > 0 |
| Errors(错误数) | 错误事件计数 | 网络接口 errors/drops;磁盘 media_error |
排查优先级:先看 Errors,再看 Saturation,最后看 Utilization。高利用率不一定有问题,但高饱和度通常意味着性能下降。
60 秒快速排查清单
当接到性能告警时,依次执行以下命令快速定位瓶颈:
# 1. uptime — 负载均衡概览
uptime
# 输出示例: 14:32:10 up 3 days, 2:15, 2 users, load average: 4.32, 3.89, 2.75
# 2. dmesg | tail — 内核错误(OOM、软锁等)
dmesg -T | tail -20
# 3. vmstat 1 — 系统级 CPU/内存/IO 汇总
vmstat 1 5
# 4. mpstat -P ALL 1 — 单核 CPU 分解
mpstat -P ALL 1 3
# 5. pidstat 1 — 进程级 CPU/上下文切换
pidstat 1 5
# 6. iostat -xz 1 — 磁盘 IO
iostat -xz 1 3
# 7. free -m — 内存使用
free -m
# 8. sar -n DEV 1 — 网络接口吞吐
sar -n DEV 1 3
# 9. sar -n TCP,ETCP 1 — TCP 状态与重传
sar -n TCP,ETCP 1 3
# 10. top — 实时进程排名
top -b -n 1 | head -20CPU 性能排查
top / htop
top 是最常用的实时进程监控工具。关键列含义:
| 列名 | 含义 |
|---|---|
| us | 用户空间 CPU 时间占比 |
| sy | 内核空间 CPU 时间占比 |
| ni | 低优先级(nice)进程 CPU 时间占比 |
| id | 空闲 CPU 时间占比 |
| wa | 等待 IO 完成的时间占比(IO 等待) |
| hi | 硬件中断处理时间占比 |
| si | 软件中断处理时间占比 |
| st | 被虚拟机偷取的时间(steal,仅虚拟化环境) |
常用操作:
# 按 CPU 使用率排序(默认)
top -o %CPU
# 按内存使用率排序
top -o %MEM
# 显示每个 CPU 核心的使用情况
top -1
# htop 更友好的交互界面(支持鼠标、颜色标识)
htop如果 wa 偏高(> 30%),说明磁盘 IO 是瓶颈。如果 hi 或 si 偏高,说明中断处理消耗过多 CPU。
mpstat 各列含义
mpstat -P ALL 1 3输出列说明:
| 列 | 含义 |
|---|---|
| CPU | 核心编号(all 表示所有核心平均) |
| %usr | 用户空间时间占比(不包含 nice) |
| %nice | 低优先级用户空间时间占比 |
| %sys | 内核空间时间占比 |
| %iowait | 等待磁盘 IO 完成的时间占比 |
| %irq | 硬件中断处理时间占比 |
| %soft | 软件中断处理时间占比 |
| %steal | 虚拟机偷取时间占比 |
| %guest | 运行虚拟 CPU 的时间占比 |
| %gnice | 运行低优先级虚拟 CPU 的时间占比 |
| %idle | 空闲时间占比 |
排查经验:
- %idle 接近 0 且 %usr 很高:CPU 被用户态进程占满
- %idle 接近 0 且 %sys 很高:内核态开销大,可能是驱动、锁竞争、系统调用过多
- %iowait 持续高于 30%:磁盘 IO 是瓶颈
- %steal 持续高于 10%:宿主机超卖,需联系云服务商
CPU 上下文切换(vmstat)
vmstat 1 5与 CPU 上下文切换相关的列:
| 列 | 含义 |
|---|---|
| r | 可运行队列长度(正在运行 + 等待 CPU 的进程数) |
| b | 处于不可中断睡眠状态的进程数(通常等待 IO) |
| cs | 每秒上下文切换次数(context switch) |
| us | 用户 CPU 时间 |
| sy | 系统 CPU 时间 |
| id | 空闲 CPU 时间 |
| wa | IO 等待时间 |
| st | 被偷取的时间 |
阈值参考:
r值持续超过 CPU 核心数 × 2:CPU 饱和cs值超过 100,000+:上下文切换过于频繁,考虑减少线程数或使用协程b值持续 > 0:存在 IO 阻塞
查看进程级上下文切换:
# 查看进程的 voluntary(自愿)和 nonvoluntary(非自愿)上下文切换
pidstat -w 1 5
# 查看特定进程
pidstat -w -p PID 1 5非自愿上下文切换过多(nonvoluntary > 1000/s)通常意味着 CPU 资源竞争。
中断分析
# 查看每个 CPU 核心的中断分布
cat /proc/interrupts
# 查看软中断统计
cat /proc/softirqs
# 实时监控软中断
watch -n 1 -d cat /proc/softirqs通过 /proc/interrupts 可以查看中断是否集中在一个核心上(可能导致该核心过载)。可以通过 irqbalance 服务或 smp_affinity 手动绑定来分散中断。
load average 解析
uptime
# load average: 4.32, 3.89, 2.75三个数字分别表示过去 1 分钟、5 分钟、15 分钟的负载均值。负载均值等于处于运行状态(runnable)和不可中断睡眠状态(uninterruptible sleep)的进程数之和。
判断标准(以 N 为 CPU 核心数):
- load < N × 0.7:正常
- load ≈ N:值得关注
- load > N × 2:严重过载
趋势分析:
- 1 分钟 > 15 分钟:负载在上升
- 1 分钟 < 15 分钟:负载在下降
- 三者均高且接近:持续过载
获取 CPU 核心数:
nproc
# 或
lscpu | grep '^CPU(s):'perf top 热点函数定位
perf 是 Linux 内核性能分析工具集,基于硬件性能计数器和内核追踪点。
# 实时显示 CPU 热点函数
perf top
# 指定采样频率和进程
perf top -F 99 -p PID
# 查看特定 CPU
perf top -C 0
# 显示调用链
perf top -g
# 记录性能数据后分析
perf record -F 99 -a -g -- sleep 30
perf report -g graphperf top 输出说明:
| 列 | 含义 |
|---|---|
| Overhead | 该符号占用的 CPU 时间百分比 |
| Shared | 符号所属的共享对象(可执行文件或库) |
| Symbol | 函数符号名 |
常见热点分析:
- 内核函数(
tcp_v4_rcv、_raw_spin_lock等):可能是网络或锁竞争问题 - 用户态函数(如 Java 的
JVM_FindSignal):应用层性能问题 - 库函数(
memcpy、malloc等):内存操作密集
内存性能排查
free -m 各列含义
free -m
# total used free shared buff/cache available
# Mem: 7825 3200 1200 350 3425 4100
# Swap: 2047 0 2047| 列 | 含义 |
|---|---|
| total | 物理内存总量 |
| used | 已使用内存(total - free - buff/cache) |
| free | 完全未使用的内存 |
| shared | tmpfs 等共享内存占用量 |
| buff/cache | 内核缓存(Buffer + Cache) |
| available | 在不触发 swap 的前提下,可分配给新进程的内存估算值 |
关键解读:
available是实际可用内存的最准确指标。不要因为free小就认为内存不足——Linux 会尽量利用空闲内存做缓存。buff/cache在内存紧张时会被内核自动回收,所以available ≈ free + 可回收的 cache。- Swap
used> 0 说明物理内存不足,需要关注。
vmstat 内存列
vmstat 1 5内存相关列:
| 列 | 含义 |
|---|---|
| swpd | 已使用的交换空间总量(KB) |
| free | 空闲内存(KB) |
| buff | 块设备缓存(Buffer)大小(KB) |
| cache | 文件系统缓存(Page Cache)大小(KB) |
| si | 每秒从磁盘交换到内存的数据量(swap in,KB) |
| so | 每秒从内存交换到磁盘的数据量(swap out,KB) |
排查要点:
si和so持续 > 0:内存严重不足,进程在频繁换入换出free趋于 0 但si/so也为 0:内存被 cache 占用,属于正常现象swpd高但si/so为 0:只是曾经发生过 swap,当前未活跃交换
slabtop 内核内存
# 查看内核 slab 分配器使用情况(按内存大小排序)
slabtop -s c
# 只显示前 10 行
slabtop -s c | head -20slab 分配器管理内核对象缓存(如 dentry、inode、tcp_bind_bucket 等)。
常见高占用对象:
| 对象 | 含义 | 异常时可能原因 |
|---|---|---|
| dentry | 目录项缓存 | 目录/文件数量过多,无法被回收 |
| inode_cache | inode 缓存 | 大量小文件占用 |
| tcp_bind_bucket | TCP 连接绑定表 | 大量短连接或 TIME_WAIT |
| radix_tree_node | 页缓存索引节点 | Page Cache 占用大量内存 |
查看 /proc/meminfo 中的 slab 信息:
grep -E '^(Slab|SReclaimable|SUnreclaim)' /proc/meminfoSUnreclaimable 不可回收的 slab 内存持续增长可能是内核内存泄漏。
OOM Killer 日志分析
当系统内存耗尽,内核会触发 OOM(Out Of Memory)Killer 杀死进程释放内存。
查看 OOM 日志:
# 通过 dmesg 查看
dmesg -T | grep -i 'oom\|out of memory'
# 通过系统日志查看(CentOS/RHEL)
grep -i 'oom\|out of memory' /var/log/messages
# 通过系统日志查看(Ubuntu/Debian)
grep -i 'oom\|out of memory' /var/log/syslogOOM 日志关键信息解读:
[ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
[ 12345] 1000 12345 1500000 1200000 43008000 0 0 java| 字段 | 含义 |
|---|---|
| pid | 被杀死进程的 PID |
| total_vm | 进程虚拟内存总量(pages) |
| rss | 驻留内存大小(pages) |
| oom_score_adj | OOM 分数调整值 |
| name | 进程名 |
OOM Killer 选择机制:内核为每个进程计算 oom_score,分数最高的进程会被优先杀死。oom_score 基于进程内存占用 + oom_score_adj(-1000 可豁免)。
预防 OOM:
# 查看进程的 OOM 分数
cat /proc/PID/oom_score
# 调整 OOM 优先级(-1000 到 1000)
echo -500 > /proc/PID/oom_score_adj
# 关闭 overcommit(不允许超额分配)
sysctl vm.overcommit_memory=2
sysctl vm.overcommit_ratio=80
# 限制进程内存使用(cgroup)
systemd-run --user -p MemoryMax=1G --unit=myapp.service /path/to/app内存泄漏检测
valgrind
Valgrind 的 memcheck 工具可以检测 C/C++ 程序的内存泄漏。
# 基本使用
valgrind --tool=memcheck --leak-check=full ./myapp
# 输出到文件
valgrind --tool=memcheck --leak-check=full --log-file=valgrind.log ./myapp
# 显示详细泄漏点
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./myapp输出解读:
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 5
==12345== at 0x4C2B800: malloc (vg_replace_malloc.c:309)
==12345== by 0x4005E4: main (test.c:10)| 泄漏类型 | 含义 |
|---|---|
| definitely lost | 内存一定泄漏(没有指针指向该内存块) |
| indirectly lost | 间接泄漏(指向该内存的指针本身也泄漏了) |
| possibly lost | 可能泄漏(指针仍存在但指向了结构体内部) |
| still reachable | 内存未释放但仍有指针指向(程序退出时未清理) |
heaptrack(KDE 工具,推荐替代 valgrind)
# 安装
# Ubuntu: apt install heaptrack heaptrack-gui
# CentOS: yum install heaptrack
# 运行并追踪
heaptrack ./myapp
# 生成分析报告
heaptrack_print heaptrack.myapp.PID.gz
# 图形界面分析
heaptrack_gui heaptrack.myapp.PID.gzheaptrack 的优势在于性能开销比 valgrind 小得多,适合生产环境。
其他语言的内存泄漏工具
# Python
pip install memory_profiler
python -m memory_profiler myapp.py
# 或使用 tracemalloc(Python 3.4+)
python -X tracemalloc myapp.py
# Java(使用 jcmd 堆分析)
jcmd PID GC.heap_dump /tmp/heap.hprofIO 性能排查
iostat
iostat -xz 1 3| 列 | 含义 |
|---|---|
| r/s | 每秒读请求数 |
| w/s | 每秒写请求数 |
| rkB/s | 每秒读取数据量(KB) |
| wkB/s | 每秒写入数据量(KB) |
| rrqm/s | 每秒合并的读请求数 |
| wrqm/s | 每秒合并的写请求数 |
| await | IO 请求平均等待时间(包含队列 + 服务时间,ms) |
| r_await | 读请求平均等待时间(ms) |
| w_await | 写请求平均等待时间(ms) |
| svctm | IO 请求平均服务时间(ms,已废弃,仅供参考) |
| %util | 设备工作时间占比(接近 100% 表示设备饱和) |
排查经验:
%util接近 100%:磁盘硬件饱和await远大于svctm:存在 IO 队列等待,磁盘过载r/s+w/s超过设备的 IOPS 上限:请求积压rrqm/s/wrqm/s较高:IO 调度器在合并请求,说明 IO 模式偏随机
iotop
按进程查看 IO 使用情况:
# 实时显示进程 IO(需要 root)
iotop
# 非交互模式,采样 3 次
iotop -b -n 3
# 只看实际磁盘 IO(排除缓存读写)
iotop -o
# 累计模式
iotop -a关键列:
| 列 | 含义 |
|---|---|
| DISK READ | 进程每秒读取的磁盘数据量 |
| DISK WRITE | 进程每秒写入的磁盘数据量 |
| IO | IO 百分比(基于 IO 时间) |
| SWAPIN | 进程处于 swap in 的时间百分比 |
dd 基准测试
# 顺序写测试
dd if=/dev/zero of=/tmp/test bs=1M count=1024 conv=fdatasync oflag=direct
# 顺序读测试
dd if=/tmp/test of=/dev/null bs=1M count=1024 iflag=direct
# 使用 fio 做更精确的测试(推荐)
fio --name=randread --ioengine=libaio --rw=randread --bs=4k --size=1G --numjobs=4 --runtime=30 --group_reporting
fio --name=randwrite --ioengine=libaio --rw=randwrite --bs=4k --size=1G --numjobs=4 --runtime=30 --group_reportingoflag=direct 绕过 Page Cache,直接测试磁盘真实性能。如果不加 direct,测试结果反映的是内存到内存的拷贝速度。
文件系统缓存(Page Cache / Dirty 页)
# 查看内存中 dirty 页的数量
cat /proc/meminfo | grep -E '^(Dirty|Writeback|NFS_Unstable)'
# 查看 Page Cache 大小
cat /proc/meminfo | grep -E '^(Buffers|Cached|SwapCached)'
# 查看脏页刷新相关内核参数
sysctl vm.dirty_ratio
sysctl vm.dirty_background_ratio
sysctl vm.dirty_expire_centisecs
sysctl vm.dirty_writeback_centisecs关键内核参数:
| 参数 | 默认值 | 含义 |
|---|---|---|
| vm.dirty_ratio | 30 | 脏页达到系统内存的此百分比后,写操作会阻塞直到刷盘 |
| vm.dirty_background_ratio | 10 | 脏页达到此百分比后,后台内核线程开始刷盘 |
| vm.dirty_expire_centisecs | 3000 | 脏页超过此时间(百分之一秒)必须刷盘 |
| vm.dirty_writeback_centisecs | 500 | 后台刷盘线程的唤醒间隔(百分之一秒) |
排查要点:
Dirty持续偏高且Writeback> 0:磁盘写入速度跟不上生成速度Dirty长时间接近dirty_ratio阈值:IO 瓶颈可能导致应用程序写阻塞- 大量小文件写入时适当降低
dirty_ratio可避免内存暴涨
查看文件系统缓存命中率(使用 cachestat):
# cachestat 来自 perf-tools(Brendan Gregg)
cachestat 1 5网络性能排查
sar(网络/CPU/内存综合)
sar 是 sysstat 包中的工具,可以收集和报告系统活动。
# 安装
# Ubuntu: apt install sysstat
# CentOS: yum install sysstat
# 启用数据收集(编辑 /etc/default/sysstat,设置 ENABLED="true")
systemctl enable --now sysstat网络统计:
# 网络接口吞吐
sar -n DEV 1 3
# 输出说明
# rxpck/s 每秒接收的包数
# txpck/s 每秒发送的包数
# rxkB/s 每秒接收的数据量(KB)
# txkB/s 每秒发送的数据量(KB)
# rxcmp/s 每秒接收的压缩包
# %ifutil 接口利用率(百分比)# TCP 连接统计
sar -n TCP,ETCP 1 3
# 输出说明
# active/s 主动 TCP 连接数(SYNC 发送)
# passive/s 被动 TCP 连接数(SYNC 接收)
# iseg/s 接收段数
# oseg/s 发送段数
# atmptf/s 连接失败次数
# estres/s 连接重置次数
# retrans/s TCP 重传数 --- 重要!重传率高说明网络质量差
# perrs/s 入站错误数
# orets/s 出站重传数CPU 统计:
sar -u 1 3
sar -P ALL 1 3内存统计:
sar -r 1 3
# kbmemfree/kbmemused/kbbuffers/kbcached/kbcommit/%commit
sar -S 1 3
# kbswpfree/kbswpused/kbswpcad/%swpused/%swpcadnicstat 网卡统计
nicstat 提供更详细的网卡利用率统计。
# 安装 nicstat
# 下载: https://sourceforge.net/projects/nicstat/
# 基本使用
nicstat 1 5
# 输出说明
# Int 接口名
# rKB/s 每秒接收 KB
# wKB/s 每秒发送 KB
# rPk/s 每秒接收包数
# wPk/s 每秒发送包数
# rAvs 平均接收包大小(字节)
# wAvs 平均发送包大小(字节)
# %Util 接口利用率百分比
# Sat 饱和度(0.00 无饱和,1.00 饱和)ss 连接队列
# 查看监听套接字的连接队列
ss -nltp
# 查看连接队列溢出统计
ss -s
# 查看当前连接数统计
ss -tan | awk 'NR>1 {state[$1]++} END {for (s in state) print s, state[s]}'
# 查看特定端口连接队列
ss -tnp 'sport = :8080'连接队列概念:
对于 TCP 监听 socket 有两个队列:
| 队列 | 含义 | 相关参数 |
|---|---|---|
| syn queue(半连接队列) | 收到 SYN 但尚未完成三次握手的连接 | net.ipv4.tcp_max_syn_backlog、net.core.somaxconn |
| accept queue(全连接队列) | 已完成三次握手但尚未被 accept 的连接 | min(somaxconn, backlog) |
# 查看全连接队列溢出次数
netstat -s | grep -i 'listen queue'
# 查看半连接队列溢出次数
netstat -s | grep -i 'SYN.*overflow'
# 设置队列大小
sysctl -w net.core.somaxconn=1024
sysctl -w net.ipv4.tcp_max_syn_backlog=1024连接队列溢出时,客户端会出现连接超时或连接被拒绝。
netstat 状态统计
# 按 TCP 状态统计连接数
netstat -tan | awk 'NR>2 {state[$6]++} END {for (s in state) print s, state[s]}'
# 输出的典型状态
# LISTEN 监听中
# ESTABLISHED 已建立连接
# TIME_WAIT 主动关闭后等待 2MSL
# CLOSE_WAIT 被动关闭,等待应用 close
# SYN_SENT 主动发起连接
# SYN_RECV 收到 SYN
# FIN_WAIT1/2 主动关闭中常见问题:
| 状态 | 数量过高时 | 排查方向 |
|---|---|---|
| TIME_WAIT | > 30000 | 短连接过多,考虑开启 tcp_tw_reuse(内核 5.9+ 已移除该参数)或改用长连接 |
| CLOSE_WAIT | > 1000 | 应用层未正确调用 close(),是代码 bug |
| SYN_RECV | > 500 | 可能遭受 SYN Flood 攻击,检查 tcp_max_syn_backlog |
| ESTABLISHED | 远超预期 | 应用层连接泄漏,检查连接池配置 |
# 网络错误统计
netstat -s | grep -E 'segments retransmited|packets reassembled|failed|errors'
# 连接跟踪表(有状态防火墙)
cat /proc/net/nf_conntrack | wc -l
sysctl net.netfilter.nf_conntrack_max火焰图(Flame Graph)
原理
火焰图是由 Brendan Gregg 发明的性能可视化技术,对采样堆栈进行可视化呈现。
采样堆栈:
通过以固定频率(如每秒 99 次)对程序堆栈进行采样,记录每个采样点正在执行的函数调用链。
折叠(Fold):
将所有采样点中相同的调用栈进行合并统计,得到一个调用栈及其出现次数的列表。
x 轴 / y 轴含义:
| 轴 | 含义 |
|---|---|
| x 轴(横向) | 采样空间,按函数名称字母排序,不代表时间先后。宽度越宽,说明该函数被采样到的次数越多,即 CPU 占用越高 |
| y 轴(纵向) | 调用栈深度,从下到上依次为根调用到叶子函数。越高说明调用链越深 |
颜色含义:
| 颜色 | 含义 |
|---|---|
| 橙色/红色 | CPU 热点函数,宽度大的橙色块是需要关注的重点 |
| 蓝色/紫色 | 内核态函数 |
| 绿色 | 用户态函数,通常表示 IO 相关操作 |
| 灰色 | 空闲或等待 |
| 其他颜色 | 根据具体火焰图生成工具而定 |
读图要点:
- 寻找 x 轴上最宽的「平顶山」——这是 CPU 消耗最大的函数
- 沿着宽平顶山的调用链向上看,找到具体的叶子函数
- 关注 y 轴上异常深的调用栈(可能存在不必要的层级)
CPU 火焰图生成全流程
# 第 1 步:perf record 采样(-F 采样频率,-a 所有核心,-g 记录调用链,-- sleep 采样时长)
perf record -F 99 -a -g -- sleep 30
# 如果只想采样特定进程
perf record -F 99 -p PID -g -- sleep 30
# 采样结果保存在 perf.data 文件中# 第 2 步:perf script 将二进制数据转换为文本格式
perf script > out.perf# 第 3 步:下载 FlameGraph 脚本
git clone https://github.com/brendangregg/FlameGraph.git
# 第 4 步:折叠堆栈
FlameGraph/stackcollapse-perf.pl out.perf > out.folded
# 第 5 步:生成 SVG 火焰图
FlameGraph/flamegraph.pl out.folded > cpu_flame.svg
# 一步完成
perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > cpu_flame.svg生成后的 cpu_flame.svg 可以用浏览器打开,支持鼠标悬停显示函数名和占比,点击可放大查看特定调用链。
perf record 参数说明:
| 参数 | 含义 |
|---|---|
| -F 99 | 采样频率 99 Hz(避免与某些周期性行为同步导致采样偏差) |
| -a | 采集所有 CPU 核心 |
| -g | 记录调用链(call graph) |
| -- sleep N | 采样时长 N 秒 |
| -p PID | 只采集指定进程 |
| -e | 指定性能事件(如 -e cache-misses) |
无 root 权限时的替代方案:
# 使用系统范围的 perf(需在 /proc/sys/kernel/perf_event_paranoid ≤ 2 时)
# 查看当前值
cat /proc/sys/kernel/perf_event_paranoid
# 临时调整
echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid差异火焰图
差异火焰图(Diff Flame Graph)用于比较两个采样周期之间的性能变化,常用于回答「为什么版本升级后 CPU 变高了?」
# 假设有两个折叠后的文件 out.folded.before 和 out.folded.after
# 生成差异火焰图
FlameGraph/difffolded.pl out.folded.before out.folded.after | FlameGraph/flamegraph.pl > diff_flame.svg颜色含义:
| 颜色 | 含义 |
|---|---|
| 红色 | 新的采样中占比增加 |
| 蓝色 | 新的采样中占比减少 |
| 黄色/淡色 | 占比没有明显变化 |
差异火焰图将两个折叠文件逐行对比,计算每个调用栈的采样次数差值,并用颜色可视化。
内存火焰图
用于分析内存分配热点,找出哪些代码路径分配了大量内存。
# 使用 perf 的内存事件(需要较新内核)
perf record -e cpu/mem-stores/p -e cpu/mem-loads/p -a -g -- sleep 10
perf script | FlameGraph/stackcollapse-perf.pl > out.mem.folded
FlameGraph/flamegraph.pl --color=mem out.mem.folded > mem_flame.svg
# 或使用 brk/mmap 跟踪(捕获堆分配)
perf record -e syscalls:sys_enter_brk -e syscalls:sys_enter_mmap -a -g -- sleep 10对于 Java 程序,也可以直接使用 Async-profiler:
# 分配内存采样
./profiler.sh -e alloc -d 30 -f alloc_flame.html PIDoff-CPU 火焰图
off-CPU 火焰图分析进程被阻塞的原因(IO 等待、锁等待、睡眠等),与 CPU 火焰图互补。
# 方法一:使用 perf sched
perf sched record -g -- sleep 10
perf sched latency
perf sched map
perf sched timehist
# 方法二:使用 eBPF(BCC 工具)
# 需要安装 bcc 工具集
offcputime -K -p PID 30 > out.offcpu.stack
FlameGraph/flamegraph.pl --color=io out.offcpu.stack > offcpu_flame.svg
# 方法三:使用 perf 跟踪 sched_switch
perf record -e sched:sched_switch -a -g -- sleep 10
perf script | FlameGraph/stackcollapse-perf.pl > out.sched.folded
# 需要过滤出被调度出去的调用栈(是下一个进程而非当前进程)off-CPU 火焰图读图要点:
- 图上最宽的部分对应进程最常阻塞的地方
- 常见阻塞原因:文件 IO、网络 IO、锁竞争(
futex)、select/poll等待
完整排查案例
案例一:CPU 100% 排查
现象:线上告警某台服务器 CPU 使用率持续 100%。
排查步骤:
# Step 1: 确认负载
uptime
# load average: 12.34, 10.21, 8.15 (8 核 CPU,负载远超 8)
# Step 2: 查看 CPU 分解
mpstat -P ALL 1 3
# 发现 %usr 在 95% 以上,%idle 接近 0,说明是用户态进程耗尽 CPU
# 且各核心负载均匀(不是中断偏移问题)
# Step 3: 找出 CPU 消耗最高的进程
top -b -n 1 -o %CPU
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 12345 app 20 0 2.5g 1.2g 300m R 800 20 15:30.67 java
# 该 Java 进程占用了 800% CPU(8 核中的 8 核)
# Step 4: 查看进程内线程
top -H -p 12345
# 发现多个工作线程 CPU 占用都在 90%+
# Step 5: 采样生成火焰图
perf record -F 99 -p 12345 -g -- sleep 30
perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > java_cpu.svg
# 从火焰图中发现:
# - 最宽的平顶是 com.example.service.OrderService.calculatePrice
# - 进一步追溯,calculatePrice 内部频繁调用 BigDecimal 除法运算
# - 且没有缓存计算结果,导致重复计算
# Step 6: 定位到具体代码行(添加 JVM 调试符号或使用 async-profiler)
# 确认是热点后,通过代码审查发现问题:循环中重复计算相同数据解决方案:
- 对计算结果添加本地缓存(Caffeine)
- 将
BigDecimal计算改为预计算 + 查表 - 优化后 CPU 从 100% 降至 15%
案例二:内存 OOM 排查
现象:应用每隔数小时被 OOM Killer 杀死。
排查步骤:
# Step 1: 查看 OOM 日志
dmesg -T | grep -A10 'Out of memory'
# [Tue Jul 7 14:32:10 2026] Out of memory: Killed process 12345 (java)
# [Tue Jul 7 14:32:10 2026] oom_kill_process: 12345 (java) score 950
# Step 2: 查看系统内存使用趋势
sar -r -S 1 10
# kbmemused 持续增长,kbmemfree 接近 0
# kbswpused 从 0 增加到 2GB(开始换出)
# Step 3: 查看进程内存映射
cat /proc/12345/smaps | grep -E '^(Size|Rss|Pss):' | head -20
# Step 4: 查看堆内存使用
pmap -x 12345 | sort -k3 -rn | head -10
# 发现一个匿名内存块(anon)占用了 1.5GB
# Step 5: 堆转储分析(Java 应用)
jmap -dump:live,format=b,file=/tmp/heap.hprof 12345
# 分析 heap.hprof(使用 Eclipse MAT 或 jhat)
# 发现有个 ConcurrentHashMap 中缓存了所有数据库记录,未做大小限制
# Step 6: 设置 JVM GC 日志(下次复现时分析)
# -Xlog:gc*:file=gc.log:time,uptime,level,tags解决方案:
- 限制缓存大小(Guava Cache 或 Caffeine 设置 maximumSize)
- 添加内存熔断机制(Heap 使用率 > 85% 时降级)
- 配置 JVM
-Xmx为合理值,限制最大堆内存 - 添加监控告警:堆内存使用率超过 80% 时触发告警
案例三:IO 高延迟排查
现象:数据库查询耗时飙升,应用接口 P99 延迟从 10ms 升至 2s。
排查步骤:
# Step 1: 检查 CPU 和 IO 等待
vmstat 1 5
# r b swpd free buff cache si so bi bo in cs us sy id wa st
# 2 15 0 500000 30000 800000 0 0 5000 8000 2000 5000 5 10 20 65 0
# b > 0(15 个进程在等待 IO),wa = 65%(CPU 大量时间等待 IO)
# Step 2: 查看磁盘统计
iostat -xz 1 3
# Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s await svctm %util
# sda 0 10 1000 2000 40000 50000 150 0.5 99.8
# %util = 99.8% 磁盘饱和,await = 150ms(正常应 < 10ms)
# Step 3: 找出 IO 密集进程
iotop -o -b -n 3
# TID PRIO DISK READ DISK WRITE SWAPIN IO> COMMAND
# 6789 be/4 30.00M/s 50.00M/s 0.00% 75.00% java
# Step 4: 分析 IO 类型(随机/顺序)
iostat -x 1 3 | grep -E '(r/s|w/s|avgqu-sz)'
# avgqu-sz = 50(请求队列深度很大)
# r/s 高且 avgqu-sz 高:随机读密集
# Step 5: 查看进程 IO 系统调用
strace -p 6789 -e trace=read,write -c -S time
# % time seconds usecs/call calls errors syscall
# 60.00 0.600000 30 20000 read
# 40.00 0.400000 20 20000 write
# Step 6: 进一步确认是数据库数据文件 IO
lsof -p 6789 | grep '\.ibd'
# 确认大量 IO 集中在 MySQL 的 .ibd 文件上根因分析:
- MySQL 查询未使用索引,导致全表扫描
- 全表扫描产生大量随机读 IO
- 单个磁盘 IOPS 上限约 5000,实际请求量达到 10000+,产生 IO 等待
解决方案:
- 添加合适的索引,将全表扫描改为索引查询
- 存储层从 HDD 迁移到 SSD(SSD IOPS 可提升 10-100 倍)
- 增加数据库缓冲池大小(
innodb_buffer_pool_size),减少磁盘 IO - 添加数据库查询超时熔断,避免慢查询拖垮整个数据库
附录:常用工具速查
| 场景 | 工具 | 关键命令 |
|---|---|---|
| 系统概览 | top/htop | top -o %CPU |
| CPU 分解 | mpstat | mpstat -P ALL 1 |
| 上下文切换 | vmstat / pidstat | vmstat 1 / pidstat -w 1 |
| 热点函数 | perf | perf top -g |
| 内存概览 | free / vmstat | free -m |
| 内核内存 | slabtop | slabtop -s c |
| OOM 日志 | dmesg | dmesg -T | grep oom |
| 磁盘 IO | iostat / iotop | iostat -xz 1 |
| 网络吞吐 | sar -n DEV | sar -n DEV 1 |
| TCP 状态 | ss / netstat | ss -tan |
| 火焰图 | perf + FlameGraph | perf script | stackcollapse-perf.pl | flamegraph.pl |
| 内存泄漏 | valgrind / heaptrack | valgrind --leak-check=full ./app |
| 进程追踪 | strace | strace -p PID -c -S time |
| 文件系统缓存 | /proc/meminfo | grep -E 'Dirty|Writeback' /proc/meminfo |