4.6 Linux 系统性能调优实战

预计阅读时间:17 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 掌握 USE 方法论(利用率、饱和度、错误)系统性分析性能瓶颈
  • 使用 mpstatperfiostatvmstat 等工具定位 CPU/内存/磁盘/网络瓶颈
  • 生成并解读 perf 火焰图
  • 使用 stress-ng 进行压力测试和性能隔离实验
  • 调整内核参数(swappiness、脏页回写、NUMA 绑定)

核心知识

  • US E 方法论——Brendan Gregg 提出的性能分析框架:对每种资源检查 Utilization(利用率)、Saturation(饱和度)、Errors(错误)
  • mpstat——多核 CPU 使用率统计,可查看每核的 user/system/iowait/steal 等细分
  • perf——Linux 性能事件采样工具,支持 CPU 周期、缓存未命中、分支预测等硬件事件
  • 火焰图(Flame Graph)——将 perf 采样数据可视化为堆叠图,横轴宽度表示 CPU 占用比例
  • vm.swappiness——内核使用 swap 交换空间的倾向性(0-100),值越低越少使用 swap
  • NUMA(Non-Uniform Memory Access)——多 CPU 架构中,每个 CPU 访问本地内存比远端内存快的架构特性
  • iostat——磁盘 I/O 统计工具,%util 反映磁盘繁忙程度,await 反映 I/O 延迟
  • cgroups v2——Linux 内核资源隔离机制,可限制 CPU、内存、I/O 等资源
  • stress-ng——系统压力测试工具,可模拟 CPU/内存/磁盘/网络负载

知识关联

原理讲解

调优方法论:测量→分析→调整→验证

性能调优最忌讳"猜"——不经测量就调整内核参数。正确的方法是:先测量当前性能指标,分析瓶颈在哪个子系统(CPU、内存、磁盘、网络),然后针对性地调整参数,最后重新测量验证效果。如果调整后没有改善,回滚并重新分析。USE 方法论为这个流程提供了系统性的检查框架。

USE 方法论详解

USE(Utilization、Saturation、Errors)是系统性性能分析的框架——对每个资源检查三个维度:

资源Utilization(利用率)Saturation(饱和度)Errors(错误)
CPU% user + % system运行队列长度、负载均值错误计数/故障(罕见)
内存已使用 / 总量swap 使用量、OOM 事件OOM Killer 触发
磁盘设备繁忙时间 %等待队列长度I/O 错误、smartctl 异常
网络带宽使用量 / 带宽上限接口溢出、丢包统计重传率、CRC 错误

当 Utilization 持续接近 100% 时,通常意味着资源成为瓶颈。Saturation 是更好的预警指标——在 Utilization 到达 100% 之前,Saturation 已经开始增长。Errors 无论多少都是需要立即处理的问题。

火焰图解读

火焰图是 Brendan Gregg 发明的 CPU 热点可视化工具。横轴表示采样占比(越宽的方块占 CPU 时间越多),纵轴表示调用栈(底部是被调用函数、顶部是当前执行函数)。解读关键:关注顶部宽大的方块(这是实际消耗 CPU 的函数),以及调用链中的"瓶颈路径"(从底部到顶部的调用链如果每个节点都很宽,说明这条路径整体有优化空间)。

性能工具矩阵:场景 → 工具

分析场景首选工具关键指标辅助工具
全局 CPU 概览top / mpstat -P ALL 1%usr、%sys、%iowait、%stealuptime(负载均值)
CPU 调用链热点perf record -F 99 -a -g采样占比 top 函数FlameGraph 火焰图
内存与 swapvmstat 1 / free -hsi/so、available、swap usedsar -rps --sort=-%mem
磁盘吞吐与延迟iostat -x 1%util、await、avgqu-sz、r/s、w/ssar -dpidstat -d
磁盘热点进程pidstat -d 1单进程读写速率iotop(交互式)
网络带宽与错误sar -n DEV 1rxkB/s、txkB/s、drop、errsss -sethtool -S
TCP 连接与重传ss -s / nstat -sretrans、TIME_WAIT 数量tcpdumpnstat
单进程资源占用pidstat 1CPU/内存/IO 按进程细分ps aux
长期趋势与历史回放sar(sysstat 后台采集)任意历史时刻指标Prometheus + Grafana

使用顺序:先看全局(top/vmstat/iostat)确定瓶颈子系统,再下钻到进程(pidstat),最后用 perf 采样调用栈。sysstat 默认每 10 分钟自动采集一次,是重建"故障前 24 小时现场"的关键数据源,生产服务器务必安装并开启。

为什么用 USE 方法论而不是只看 CPU 利用率

很多运维人员看到 CPU 利用率接近 100% 就判定"CPU 是瓶颈"并加 CPU,但实际上 Utilization(利用率)是最差的瓶颈指标。原因:① 利用率高不代表饱和——CPU 可能忙于处理大量可并行的任务,队列并不长;② 利用率低也不代表没问题——如果进程被 I/O 阻塞(%iowait 高),CPU 空闲但系统仍然慢。USE 方法论通过同时检查三个维度解决这个问题:利用率告诉你"资源用了多少",饱和度告诉你"有多少任务在排队等这个资源",错误告诉你"资源本身是否有故障"。饱和度是更好的早期预警指标——在利用率达到 100% 之前,饱和度(如 CPU 运行队列长度、内存 swap 活动)已经开始增长。

为什么火焰图选择 99Hz 而不是 100Hz

perf record -F 99 中的 99Hz 是精心选择的数值。如果用 100Hz(精确的整数频率),采样定时器可能与系统中的其他周期性事件(如内核 timer tick 的 100Hz/250Hz/1000Hz、网络 NAPI 轮询)产生谐振(共振),导致采样偏向某些特定函数,产生统计偏差。使用质数或非整数频率(如 99Hz、97Hz)可以避免与任何固定频率的系统事件同步,采样结果更接近真实的 CPU 使用分布。这是统计采样中的经典技巧——任何固定周期的采样器都可能与被采样系统的周期产生 aliasing。

NUMA 为什么会影响性能

在多路 CPU 服务器(2路/4路)中,每个 CPU 有自己直连的本地内存,访问远端 CPU 的内存需要通过互联总线(如 Intel QPI/UPI),延迟增加 1.5-3 倍。这就是 NUMA(Non-Uniform Memory Access)架构。如果一个进程的内存被分配在远端 NUMA 节点,每次内存访问都付出额外延迟,整体性能显著下降。排查方法:numastat -p PID 检查进程的内存分布;调优方法:numactl --cpunodebind=0 --membind=0 将进程绑定到同一 NUMA 节点的 CPU 和内存。虚拟化和容器场景中 NUMA 感知尤为重要——VM/容器可能跨 NUMA 节点分配资源,导致性能不稳定。

示例代码

1. USE 方法论快速诊断

# CPU — Utilization 和 Saturation
mpstat -P ALL 1 3            # 每核利用率
sar -q 1 3                   # 运行队列(runq-sz)+ 负载(ldavg)

# 内存
vmstat 1 5                   # si(swap in)/ so(swap out)> 0 = 内存饱和
free -h                      # available 接近 0 = 内存压力

# 磁盘
iostat -x 1 3                # %util = 磁盘利用率;avgqu-sz = 饱和度
sar -d 1 3                   # tps + await(平均服务时间)

# 网络
sar -n DEV 1 3               # rxkB/s + txkB/s;drop 列 = 错误
ss -s                        # socket 统计
nstat -s | grep -i retrans  # TCP 重传统计(netstat 已废弃)

2. CPU 性能剖析

# 每核使用率分析
mpstat -P ALL 1
# 输出示例:
# 09:30:01  CPU  %usr  %nice   %sys %iowait  %irq  %soft  %steal %guest  %idle
# 09:30:02  all  40.5    0.0   12.3     0.2   0.0    0.5    0.0    0.0   46.5
# 09:30:02    0  65.2    0.0   20.1     0.0   0.0    1.2    0.0    0.0   13.5
# 09:30:02    1  35.1    0.0    8.5     1.1   0.0    0.0    0.0    0.0   55.3

# CPU 绑定
taskset -cp 0-3 $PID              # 将进程绑定到核心 0-3
# cgroups v2 CPU 限制
systemd-run -p CPUQuota=200% --unit=cpulimit-app /usr/bin/myapp  # 限制 2 核

# 查找 CPU 消耗最高的进程
top -b -n 1 | head -20
ps aux --sort=-%cpu | head -10

3. 内存深度分析

# free -h 解读
free -h
#               total        used        free      shared  buff/cache   available
# Mem:           31Gi       8.5Gi       1.2Gi       1.0Gi        21Gi        21Gi
# Swap:          4.0Gi       0.5Gi       3.5Gi
# available = 真正可用的内存(free + 可回收的 buff/cache)

# OOM killer 排查
dmesg | grep -i "out of memory" | tail -5
# 输出: 哪个进程被杀、内存使用情况、score

# NUMA 内存分配
numastat -p $PID                   # 查看进程的 NUMA 内存分布
numactl --cpunodebind=0 --membind=0 /usr/bin/myapp  # 绑定到 NUMA 节点

# 查找内存消耗最高的进程
ps aux --sort=-%mem | head -10

4. perf 火焰图

# 1. 安装 perf
apt install -y linux-tools-$(uname -r)

# 2. 采样
perf record -F 99 -a -g -- sleep 30
# -F 99: 每秒采样 99 次(避免精确 100Hz 与某些定时器共振)
# -a: 所有 CPU
# -g: 调用栈

# 3. 生成火焰图
git clone https://github.com/brendangregg/FlameGraph.git
perf script | ./FlameGraph/stackcollapse-perf.pl > out.perf-folded
./FlameGraph/flamegraph.pl out.perf-folded > flame.svg

# 4. 解读火焰图
# 横轴: 采样占比(越宽 = 耗时越多)
# 纵轴: 调用栈(底部 = 被调用函数,顶部 = 当前执行函数)
# 颜色: 随机(无特殊含义)
# 关注: 顶部宽大的方块和调用链中的"瓶颈函数"

5. stress-ng 压测与 cgroups 隔离

# 安装 stress-ng
apt install -y stress-ng

# CPU 压力(4 个 worker 压 60 秒)
stress-ng --cpu 4 --timeout 60s

# 内存压力(2 个 worker 各分配 256MB)
stress-ng --vm 2 --vm-bytes 256M --timeout 30s

# I/O 压力
stress-ng --hdd 2 --hdd-bytes 1G --timeout 30s

# cgroups v2 性能隔离实验
systemd-run -p CPUQuota=50% -p MemoryMax=512M --unit=stress-test \
  stress-ng --cpu 4 --timeout 60s
cat /sys/fs/cgroup/system.slice/stress-test.service/cpu.max
# 输出: 50000 100000  ← 50% CPU 限制已生效

# 配合 perf 同时采样
perf record -F 99 -g -p $(pgrep -d',' stress-ng) -- sleep 30

6. swappiness 与脏页调优实验

# vm.swappiness 控制内核使用 swap 的倾向(0-100,默认 60)
# 高值: 更早开始 swap(适合低内存环境)
# 低值: 更倾向回收页面缓存(适合高内存环境)

# 设置为 10(低 swap 倾向,推荐服务器)
sudo sysctl -w vm.swappiness=10

# 脏页回写参数
sudo sysctl -w vm.dirty_ratio=20         # 脏页占内存 20% 开始同步写
sudo sysctl -w vm.dirty_background_ratio=5  # 脏页占 5% 后台回写
# 大文件写入场景: dirty_ratio 调大(容忍更多脏页,减少回写频率)
# 低延迟场景: dirty_ratio 调小(更频繁回写,但单次延迟更低)

7. 基准测试工具

# CPU 基准
sysbench cpu --threads=4 run

# 磁盘 I/O 基准
fio --name=randwrite --rw=randwrite --bs=4k --size=1G --numjobs=4 --runtime=60 --group_reporting

# 网络带宽基准(服务端先运行 iperf3 -s)
iperf3 -c server_ip

# 内存延迟基准
lmbench  # apt install lmbench

8. 网络性能调优参数详解

# /etc/sysctl.d/99-netperf.conf
# TCP 窗口缩放(大带宽长链路必开,配合 BDP 自动计算)
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_timestamps = 1

# 放大 TCP 读写缓冲区上限(默认约 6MB,Ubuntu 24.04 的 6.8 内核;高带宽链路可放大)
net.ipv4.tcp_rmem = 4096 131072 6291456
net.ipv4.tcp_wmem = 4096 65536 16777216
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

# 连接队列与端口资源(高并发短连接场景)
net.core.somaxconn = 65535              # accept 队列上限(Nginx 需同步配置)
net.ipv4.tcp_max_syn_backlog = 65535    # SYN 半连接队列
net.ipv4.ip_local_port_range = 1024 65535   # 出站端口范围
net.ipv4.tcp_tw_reuse = 1               # 安全复用 TIME_WAIT(仅出站)
net.ipv4.tcp_fin_timeout = 30           # FIN_WAIT_2 超时

# 长连接保活(穿透 NAT/防火墙场景,防被空闲切断)
net.ipv4.tcp_keepalive_time = 120
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3

# 生效与验证
sudo sysctl -p /etc/sysctl.d/99-netperf.conf
sysctl net.core.somaxconn
# 注意: tcp_tw_recycle 在 Linux 4.12+ 中已被移除,在新内核上执行会报错;且在 NAT 环境下也会导致连接异常,切勿使用

9. 磁盘 I/O 调度器与挂载参数调优

# 查看当前 I/O 调度器
cat /sys/block/sda/queue/scheduler
# 输出: [mq-deadline] none
# 选型: 机械盘/混合负载用 mq-deadline(延迟优先);
#       NVMe 高并发随机读写用 none(直通硬件队列)

# 永久设置(GRUB 内核参数)
# 编辑 /etc/default/grub,GRUB_CMDLINE_LINUX 追加:
# elevator=mq-deadline
sudo update-grub

# 挂载参数: 读多写少场景开启 noatime,省掉每次读文件的 atime 写盘
# /etc/fstab 示例
UUID=xxxx /data ext4 defaults,noatime,nodiratime 0 2
# commit=60: 延长数据提交周期,突发写合并减少 fsync 压力(掉电丢数据窗口相应变大)

# 验证挂载参数
mount | grep /data
# 输出: /dev/sda1 on /data type ext4 (rw,noatime,nodiratime)

# 预读(readahead): 顺序读场景增大,随机读场景保持小值
sudo blockdev --getra /dev/sda
sudo blockdev --setra 4096 /dev/sda

# NVMe 队列深度
cat /sys/block/nvme0n1/queue/nr_requests
echo 1024 | sudo tee /sys/block/nvme0n1/queue/nr_requests

10. 完整调优案例:高并发 Web 服务器

# 背景: 8C16G 云服务器跑 Nginx + PHP-FPM,峰值时延迟从 20ms 飙到 800ms
# 第一步: 测量(USE 快速诊断,先定子系统再动手)
uptime                      # load 14.5(8 核 → 已饱和)
mpstat -P ALL 1 5           # %iowait 平均 35%(磁盘嫌疑)
iostat -x 1 5               # sda %util 98%, await 45ms
vmstat 1 5                  # b 列(阻塞进程)持续 > 10
# 初步结论: 瓶颈是磁盘 I/O,不是 CPU —— 停止"加 CPU"的错误方向

# 第二步: 定位 IO 来源(下钻到进程)
pidstat -d 1 5              # php-fpm worker 每秒写 200MB 到 session 目录
lsof | grep php-fpm | grep deleted   # 发现大量已删除仍被打开的文件
# 根因: 访问日志与 PHP session 文件高频写入同一块数据盘 + 未开 noatime

# 第三步: 调整(一次只改一个,每步验证)
# ① access_log 改到独立盘/内存盘,error_log 保留
# ② 重挂 /var 开启 noatime
sudo mount -o remount,noatime /var
# ③ PHP session 迁移到 Redis(参考 ch24),php.ini: session.save_handler = redis
# ④ 小文件高频写场景的脏页参数
sudo sysctl -w vm.dirty_background_ratio=5
sudo sysctl -w vm.dirty_ratio=20

# 第四步: 验证
uptime                      # 负载回落到 3.2
iostat -x 1 5               # %util 降到 45%
ab -n 10000 -c 100 http://localhost/   # P95 延迟 35ms

# 复盘: CPU 全程未到 100%,真正的瓶颈是磁盘;
# 若第一步就"加 CPU",问题会被掩盖但永远不会解决

常见错误

错误表现根因正确做法
看到 CPU idle 0% 就加 CPU未区分 %iowait%user,瓶颈可能在磁盘而非 CPUmpstat -P ALL 1 查看 %iowait;若 iowait 高则检查磁盘而非加 CPU
关掉 swap 后应用被 OOM killswapoff 导致内存压力完全由物理内存承担,无缓冲余地设置 vm.swappiness=110 而非完全禁用 swap
free 显示 used 很高就认为内存不足未区分 buff/cache 和真正的 used,Linux 会将空闲内存用做缓存available 列(真正可用的内存)而非 used
调整内核参数后性能反而下降一次改多个参数,无法确定哪个改错了一次只改一个参数,改前记录基线值,改后验证效果
NUMA 架构下内存带宽未充分发挥进程跨 NUMA 节点访问内存,延迟显著增加numastat -p PID 检查,用 numactl 绑定 CPU 和内存节点
调整 sysctl 后重启丢失参数只写入运行时,未写入 /etc/sysctl.d/写入 /etc/sysctl.d/99-*.conf 文件,重启后自动生效
压测数据与线上表现严重不符压测未控制变量:客户端瓶颈、并发模型不同、测试数据过小压测机与被测机分开;用 --numjobs/多客户端拉高并发;每个场景跑 3 次取中位数

最佳实践

实践原理示例
建立性能基线不知道"正常"是多少,就无法判断"异常"新服务器上线后记录 idle 时的 mpstat/iostat/free 数据
一次只改一个参数改多个参数无法归因因果关系改 vm.swappiness 后观察 24h 再改 dirty_ratio
使用 cgroups v2 做资源隔离防止单个应用耗尽主机资源影响其他服务systemd-run -p CPUQuota=50% -p MemoryMax=512M ...
服务器设置 swappiness 为 10仅在紧急情况下使用 swap,优先回收页面缓存/etc/sysctl.d/99-performance.conf 中写入
定期运行基准测试跟踪性能趋势,及时发现硬件降级每月运行一遍 sysbench CPU + fio 磁盘 + iperf3 网络
用 sar 保留历史数据故障复盘需要"出事前"的指标作为对照基线sar -u -f /var/log/sysstat/sa$(date +%d -d yesterday) 回放昨日 CPU
按业务类型选择 I/O 调度器延迟敏感用 mq-deadline,吞吐优先用 none数据库盘 mq-deadline,NVMe 缓存盘 none

练习题

  1. (概念)USE 方法论中的 U、S、E 分别代表什么?为什么 Saturation 比 Utilization 是更好的早期预警指标?
  2. (概念)火焰图的横轴和纵轴分别表示什么?如何快速定位 CPU 热点函数?
  3. (实操)在一台服务器上执行 mpstat -P ALL 1 5,记录每核的 %usr、%sys、%iowait、%idle。然后运行 stress-ng --cpu 2 --timeout 30s,重新执行 mpstat 观察变化。解释为什么某些核的 %usr 升高而其他核不变。
  4. (实操)执行 free -h 记录当前内存状态。设置 vm.swappiness=10,运行 stress-ng --vm 2 --vm-bytes 2G --timeout 30s,用 vmstat 1 5 观察 si/so 列。然后设置 vm.swappiness=60 重复实验,比较两个 swappiness 值下的 swap 活跃度差异。
  5. (🔍 挑战)对一台运行生产级应用(如 Nginx 或 MySQL)的服务器执行 perf record -F 99 -a -g -- sleep 30,生成火焰图。分析 top 5 最宽的函数的调用链,判断 CPU 主要消耗在哪个子系统(内核态/用户态、文件系统/网络栈/业务逻辑)。提出至少一条优化建议。
点击查看答案
  1. U=Utilization(利用率)、S=Saturation(饱和度)、E=Errors(错误)。Saturation 在 Utilization 到达 100% 前就开始增长(如 CPU 运行队列变长),因此是更早的预警信号。
  2. 横轴表示采样占比(越宽函数消耗 CPU 越多),纵轴表示调用栈(底部是被调函数,顶部是当前执行函数)。快速定位:找顶部最宽的方块,沿调用链向下看是哪个业务逻辑路径导致的。
  3. stress-ng 起 2 个 CPU worker,仅压满 2 个核,其他核的 %usr 基本不变。mpstat 可看到部分核 %usr 接近 100%,其余核保持较低 idle。
  4. swappiness=10 时 si/so 接近 0(很少 swap);swappiness=60 时同样压力下 si/so 明显增大。表明低 swappiness 更倾向回收缓存而非换出内存。
  5. perf record 采样后 perf report 查看。若 top 5 宽函数在 nginx: workerepoll_wait 中,说明 CPU 主要空闲等待 I/O;若在 ssl_encrypt 中,可考虑硬件加速或调整密码套件。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释系统性能指标和瓶颈分析方法尝试向他人讲解
命令操作能不查文档完成性能监控工具使用、参数调整在终端实际执行
原理掌握能说出 Linux 内核的进程调度、内存管理、IO 调度原理画出流程图
故障排查能独立排查 CPU 使用率高、内存不足、IO 性能差模拟故障并修复
最佳实践能说明为什么需要为生产环境配置性能基线和监控告警对比不同方案

本章总结

性能调优是 Linux 运维的高级技能,核心方法论是 "测量→分析→调整→验证"的闭环。USE(Utilization、Saturation、Errors)框架提供了系统性的资源瓶颈分析路径。CPU 调优用 mpstat + perf 火焰图,内存调优关注 available 列和 swap 使用,磁盘调优用 iostat 定位 %util 和 await,网络调优用 sar 统计带宽和重传。关键原则包括:建立基线、一次只改一个参数、用 cgroups 做资源隔离、用 stress-ng 做压力验证。

速查表

命令/参数用途
mpstat -P ALL 1每核 CPU 使用率
vmstat 1 5内存和 CPU 概览(含 si/so)
iostat -x 1 3磁盘利用率 (%util) 和延迟 (await)
sar -n DEV 1 3网络接口带宽统计
perf record -F 99 -a -g -- sleep 30CPU 采样 30 秒
perf report --stdio查看采样报告
free -h查看内存(关注 available 列)
vm.swappiness=10减少 swap 使用倾向
stress-ng --cpu 4 --timeout 30sCPU 压力测试
taskset -cp 0-3 PID绑定进程到指定 CPU 核心

学习路径建议

  • 学完本章后建议阅读 5.12:eBPF 基础 eBPF 基础(下一代性能观测技术)
  • 学完本章后建议阅读 4.7:压力测试实战 压力测试实战(在受控环境中验证瓶颈)
  • 进阶可看 Brendan Gregg 的《性能之巅》和 Linux 内核文档
  • 容器场景的性能隔离可参考 5.2:Docker 生产实践 Docker 生产实践中的资源限制部分

延伸阅读

常见问题

CPU 使用率不高但负载很高怎么回事?
load average 包括正在运行的进程和等待 I/O 的进程(D 状态)。如果 CPU idle 但负载高,通常是磁盘 I/O 或 NFS 卡住导致。用 iostat -x 1 看 %iowait,或用 ps aux | grep " D" 找 D 状态进程(不可中断睡眠,通常是 I/O 等待)。
OOM killer 杀进程后如何防止再次发生?
查看 dmesg | grep -i oom 确认哪个进程被 kill 以及当时内存状态。解决方案:① 增加物理内存或 swap;② 为 Java/Python 应用设置内存上限(如 -Xmx、--memory);③ 调整 vm.overcommit_memory=2(禁止过量分配);④ 用 cgroups/systemd 设置进程内存限制。
perf 火焰图怎么生成?
三步:① 采样数据:perf record -F 99 -p PID -g -- sleep 30;② 生成折叠栈:perf script | stackcollapse-perf.pl > out.folded;③ 生成火焰图 SVG:./flamegraph.pl out.folded > flame.svg。火焰图 X 轴是采样分布(非时间顺序),Y 轴是调用栈深度,宽条表示该路径消耗 CPU 最多。
↑ 回到顶部