摘要:

前九篇文章分别深入了 CPU、内存、IO、网络、系统调用各个维度的性能分析工具和调优手段。但真实的生产问题从来不会贴标签告诉你”这是 CPU 问题”或”这是 IO 问题”——你收到的是一个含糊的告警:“P99 延迟从 10ms 升到 200ms”,或者”吞吐量突然下降 50%“。本文通过三个来自真实生产场景的案例,展示如何将本专栏的所有工具和方法论组合成一条完整的诊断链路:从告警的第一反应、分层排查假设、工具选择与执行,到最终定位根因并验证修复效果。这不是一个线性的”按步骤操作”指南,而是一个思维过程的展示——为什么在某个节点选择这个工具而不是那个,为什么某个假设被否定,如何在信息不完整时推进诊断。


第 1 章 诊断方法论:在开始之前

1.1 USE 方法论回顾

Brendan Gregg 的 USE(Utilization-Saturation-Errors) 方法论提供了一个系统性的检查清单——对每一个资源(CPU、内存、磁盘、网络),依次检查:

  • Utilization(利用率):资源当前使用了多少(如 CPU 80%)
  • Saturation(饱和度):是否有任务在等待该资源(如 Run Queue 长度、IO 等待队列深度)
  • Errors(错误):是否有错误发生(如网络丢包、磁盘 IO 错误)
# 一次性获取所有关键资源的 USE 指标
# CPU:利用率 + 运行队列
mpstat 1 5                          # 利用率(%idle 越高越好)
vmstat 1 5                          # r 列 = 运行队列(> CPU 数 = 饱和)
 
# 内存:利用率 + swap
free -h                             # Used/Total
vmstat 1 5                          # si/so 列(swap in/out > 0 = 内存饱和)
 
# 磁盘:利用率 + 等待
iostat -x 1 5                       # %util(利用率)、aqu-sz(等待队列)、await(等待延迟)
 
# 网络:利用率 + 错误
sar -n DEV 1 5                      # rxkB/s、txkB/s vs 网卡速度
netstat -s | grep -i "error\|drop"  # 网络错误和丢包
ss -s                               # 连接状态统计

1.2 诊断原则

原则 1:先测量,后假设。不要在没有数据的情况下猜测根因,也不要在没有验证效果的情况下宣布问题解决。每一步调优都要有前后的量化对比。

原则 2:由粗到细,分层递进。先用高层指标(CPU/内存/IO/网络的整体利用率)确定问题所在的子系统,再用专项工具深入。避免在问题可能是 IO 的时候去深入分析 CPU。

原则 3:区分 ON-CPU 和 OFF-CPU。CPU 利用率不高但延迟高,几乎可以确定是 OFF-CPU 问题(等待某种资源)。高 CPU 利用率 + 高延迟,才是 ON-CPU 热点问题。

原则 4:工具选择的开销意识。生产环境下,先用低开销工具(vmstatiostatss),再用中等开销工具(perf statbpftrace),谨慎使用高开销工具(strace)。


第 2 章 案例一:Java 微服务 P99 延迟毛刺

2.1 问题描述与初步观察

现象:Kubernetes 中的 Java 微服务(Spring Boot + gRPC),平均响应时间 5ms,但 P99 延迟突然从 15ms 升到 350ms,且具有周期性(大约每 5-10 分钟出现一次毛刺,持续 1-2 秒后恢复)。服务 CPU 使用率约 40%,内存使用率约 60%,表面上”正常”。

第一反应:排除网络和基础设施问题

# 1. 确认毛刺是服务本身还是外部依赖
# 在服务侧和客户端侧同时打点,对比两侧看到的延迟
# 若服务侧看到的延迟也高 → 问题在服务内部
# 若只有客户端侧延迟高 → 问题在网络传输层
 
# 2. 排除下游依赖(数据库、Redis 等)的延迟贡献
grep "db.query.duration" /var/log/app.log | awk '{print $NF}' | sort -n | \
    awk 'END{print "P99:", int(NR*0.99)"th:", $(int(NR*0.99))}'
# 若下游 P99 < 2ms,说明下游不是问题来源
 
# 3. 检查 GC 日志中 STW 暂停时间
grep "Pause" /var/log/gc.log | awk '{print $NF}' | sort -n | tail -20
# 若最大 STW < 50ms,说明 GC 不是主要原因(但可能是次要原因)

诊断步骤:cgroup CPU Throttling 定位

# 检查 Pod 的 cgroup CPU throttle 状态
kubectl exec <pod> -- cat /sys/fs/cgroup/cpu/cpu.stat
# nr_periods     12345
# nr_throttled   1234    ← !!有 throttle 计数
# throttled_time 12345678901234
 
# 计算 throttle 比例
python3 -c "print(round(1234/12345*100, 1), '% throttled')"
# 10.0% throttled  ← 每 10 个 100ms 周期就有 1 个被暂停
 
# 读取 CPU limit 配置
kubectl exec <pod> -- cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us
# 50000  ← 50ms/100ms = 0.5 核
 
# 实时监控:毛刺时 nr_throttled 是否突增
kubectl exec <pod> -- bash -c "
prev=0
while true; do
    cur=\$(cat /sys/fs/cgroup/cpu/cpu.stat | grep nr_throttled | awk '{print \$2}')
    echo \"\$(date): throttled=\$cur delta=\$((cur-prev))\"
    prev=\$cur
    sleep 1
done"
# 观察:在 P99 毛刺出现时,delta 值是否突然增大
# 若确认毛刺时间与 throttle 增长时间高度吻合 → cgroup throttling 是主因

诊断步骤:叠加 GC 分析

# 启用 GC 详细日志(如未启用)
java -Xlog:gc*:file=/var/log/gc.log:time,level,tags -jar app.jar
 
# 分析 GC 暂停与 P99 毛刺的时间相关性
grep "Pause Full\|Pause Young\|Pause Old" /var/log/gc.log | \
    awk '{print $1, $NF}' | sort -t'[' -k2 -n | tail -50
# 若发现 Pause Full GC > 100ms 出现在毛刺时间点附近 → GC 是次要原因

根因总结:两个原因叠加——cgroup CPU throttling(主因,10% 的周期被暂停 80ms)+ Full GC STW(次因,偶发 100-200ms 暂停)。两者偶尔叠加导致 P99 峰值 350ms。

修复方案与效果验证

# 修复 1:提高 CPU limit,减少 throttling 频率
resources:
  requests:
    cpu: "500m"
  limits:
    cpu: "2000m"    # 允许突发到 2 核
 
# 修复 2:切换到低暂停 GC(ZGC 或 G1GC 调优)
# java -XX:+UseZGC -Xmx4g -Xms4g -jar app.jar
# ZGC 的 STW 暂停 < 1ms(几乎可以忽略)
# 验证:修复后重新观察 throttle 比例和 P99
kubectl exec <pod> -- cat /sys/fs/cgroup/cpu/cpu.stat
# nr_throttled   12   ← 从 1234 降到 12,减少 100 倍!
 
# P99 监控(Grafana/Prometheus):
# 修复前:P99 = 350ms(峰值)
# 修复后:P99 = 18ms(稳定)

第 3 章 案例二:MySQL 查询突然变慢

3.1 问题描述

现象:MySQL 数据库服务器,某天下午 2 点开始,原本 P99 = 5ms 的查询变成 P99 = 500ms,但 CPU 使用率只有 15%,内存充足,看起来数据库本身很空闲。这是最典型的”CPU 闲,但服务慢”场景。

3.2 分层诊断过程

第一层:系统资源全局检查

# CPU + 内存 + IO + 网络 全局快照
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  4   1234 123456  23456 456789    0    0   456  1234  234  567 15  3 42 40  0
#          ↑b=4  等待IO的进程数             ↑bo=1234 磁盘写出量   ↑wa=40% IO等待!
 
# 关键发现:wa(IO Wait)= 40%,说明 CPU 有大量时间在等待 IO
# b(blocked 进程数)= 4,有进程阻塞在 IO 等待

第二层:磁盘 IO 详细分析

# 确认是哪块磁盘、什么类型的 IO
iostat -x 1 5
# Device   r/s   w/s  rkB/s  wkB/s   r_await  w_await  aqu-sz  %util
# nvme0n1  4567   234  73456   3789     0.09     0.12     1.23    45.6  ← r_await 正常
# sdb       456   123   7234    234    89.45    45.67    34.56    98.7  ← !!sdb 高延迟高利用率!
 
# sdb 的 r_await = 89ms(远超正常 HDD 的 5-15ms),%util = 98.7%(接近饱和)
# 但 nvme0n1 是 MySQL 数据目录,为什么 sdb 会有影响?
 
# 检查 sdb 挂载的是什么
df -h | grep sdb
# /dev/sdb1   2.0T   1.9T   50G  97%  /var/log  ← MySQL 日志目录!
 
# 磁盘空间快满了!/var/log 目录 97% 使用率

第三层:定位 IO 来源

# 谁在对 sdb 产生大量 IO
iotop -o
# Total DISK READ: 45.67 M/s | Total DISK WRITE: 890.12 M/s  ← 大量写入!
# PID    PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
# 1234   be/4  mysql         0     890.12 M/s   0.00%  96.78%   mysqld
 
# mysqld 自身产生了 890MB/s 的写入到 sdb(/var/log)
# 这是 MySQL 的 general_log 或 slow_query_log 被意外开启了!
 
# 验证:检查 MySQL 日志配置
mysql -e "SHOW VARIABLES LIKE '%log%'"
# general_log         ON    ← !!general_log 被开启了!
# general_log_file    /var/log/mysql/general.log
# slow_query_log      ON
# slow_query_log_file /var/log/mysql/slow.log
# long_query_time     0.0   ← !!记录所有查询(包括 0ms 的)
 
# 这就是根因:有人将 general_log 和 slow_query_log 都开启了,
# 且 long_query_time=0 导致每条查询都被写入日志
# 生产负载下每秒数万条查询,每条日志 ~200 字节 → 890 MB/s 的日志写入!
# /var/log 磁盘(HDD)被打满,所有 IO 请求排队,延迟飙升到 89ms

修复方案与预防

# 立即修复:关闭 general_log
mysql -e "SET GLOBAL general_log = 'OFF';"
mysql -e "SET GLOBAL slow_query_log = 'OFF';"
# 或者调整 long_query_time
mysql -e "SET GLOBAL long_query_time = 1.0;"  # 只记录超过 1 秒的慢查询
 
# 验证修复效果
iostat -x 1 3
# Device   r/s   w/s  rkB/s  wkB/s  r_await  w_await  aqu-sz  %util
# sdb       12     8    123     89     5.23     4.56     0.12     8.9   ← 恢复正常!
 
# MySQL P99 延迟从 500ms 恢复到 4ms
 
# 根本预防:
# 1. 在 /etc/mysql/my.cnf 中明确配置 general_log = 0
# 2. 对 general_log 的开关设置变更审计(binlog 或 MySQL audit plugin)
# 3. 对 /var/log 磁盘添加告警(磁盘使用率 > 80% 告警)
# 4. 将 MySQL 日志目录与数据目录分离到不同磁盘(避免相互影响)

第 4 章 案例三:Nginx 高并发场景吞吐量瓶颈

4.1 问题描述

现象:一台 32 核的 Nginx 反向代理服务器,处理静态文件请求(平均文件大小 50KB)。压测时,QPS 在 5 万时延迟正常(P99 = 3ms),但继续增压到 6 万 QPS 时,延迟急剧升高(P99 = 2000ms),CPU 使用率只有 40%,网络带宽只用了 60%。这说明有某个非 CPU、非网络带宽的资源成为了瓶颈。

4.2 诊断过程

第一层:确认瓶颈不在 CPU 和网络

# CPU 40% 利用率,还有大量余量
mpstat 1 5 | grep "Average:"
# Average:  all    8.45   0.00  12.34    0.23   0.00    0.00   39.02   0.00   39.96
 
# 网络带宽 60% 利用率(10GbE,6Gbps)
sar -n DEV 1 5 | grep eth0
# eth0    txkB/s: 720000   ← 约 5.76 Gbps,60% 的 10GbE 带宽
 
# IO 等待几乎为零(静态文件全在 Page Cache 中)
vmstat 1 5 | awk '{print "wa:", $16}'
# wa: 1  ← IO 等待 1%,不是 IO 瓶颈

第二层:检查连接和 Socket 状态

# 查看连接状态分布
ss -s
# Total: 1234567
# TCP:   987654 (estab 567890, closed 234567, orphaned 12345, timewait 234567)
#
# Transport Total    IP       IPv6
# RAW       0        0        0
# UDP       123      123      0
# TCP       987654   987654   0
#
# TIME_WAIT 连接数:234567  ← !!23 万个 TIME_WAIT 连接!
 
# 检查本地端口是否耗尽
ss -s | grep "closed"
# 确认端口使用范围
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768 60999  ← 只有约 28000 个端口可用
 
# TIME_WAIT 连接数(234567)远超可用端口数(28000)
# 说明:新连接建立时频繁遇到端口耗尽的情况 → 连接建立超时 → P99 飙升!

第三层:定位 TIME_WAIT 来源

# TIME_WAIT 是由"主动关闭连接的一方"进入的
# Nginx 作为反向代理,连接分两段:
# 1. 客户端 → Nginx(客户端发起,短连接时 Nginx 主动关闭 → Nginx 进入 TIME_WAIT)
# 2. Nginx → 后端服务(Nginx 发起连接,若 Nginx 主动关闭 → Nginx 进入 TIME_WAIT)
 
# 检查哪端(客户端侧还是后端侧)的 TIME_WAIT 更多
ss -ant state time-wait | awk '{print $4}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -5
# 98765  10.0.0.1   ← 客户端 IP(来自负载均衡器)的 TIME_WAIT 最多
# 56789  10.0.0.2   ← 后端服务 IP
#
# 说明:Nginx 与后端之间大量 TIME_WAIT,Nginx 对后端连接使用短连接策略
 
# 确认 Nginx upstream 配置
grep -A 10 "upstream" /etc/nginx/nginx.conf
# upstream backend {
#     server 10.0.0.100:8080;
#     keepalive 0;    ← !!keepalive = 0,与后端完全使用短连接!
# }

根因总结:Nginx 到后端服务之间没有启用连接复用(keepalive = 0),每次请求都建立新的 TCP 连接。高并发时,大量 TIME_WAIT 连接耗尽本地端口,新连接建立失败或超时,导致 P99 飙升。

修复方案

# 修复 1:启用 Nginx upstream keepalive(复用连接,减少 TIME_WAIT)
upstream backend {
    server 10.0.0.100:8080;
    keepalive 256;          # 与后端保持最多 256 个空闲长连接
    keepalive_requests 1000;  # 每个连接最多复用 1000 次
    keepalive_timeout 65;   # 空闲超时 65 秒
}
 
server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;            # 必须 HTTP/1.1
        proxy_set_header Connection "";    # 清除 Connection: close 头
    }
}
# 修复 2:同时调整 sysctl 参数
sysctl -w net.ipv4.tcp_tw_reuse=1         # 允许 TIME_WAIT 端口复用
sysctl -w net.ipv4.ip_local_port_range="1024 65535"  # 扩大端口范围
 
# 验证修复效果
ss -s | grep "timewait"
# 修复前:timewait 234567
# 修复后:timewait 1234   ← 降低 99%!
 
# 重新压测:6 万 QPS 时 P99 = 3ms(与 5 万 QPS 时相同,瓶颈消除)

第 5 章 诊断工具速查表

5.1 按症状选择工具

症状第一工具深入工具
CPU 使用率高top/mpstatperf record + 火焰图
P99 高但 CPU 不高offcputimebpftrace 锁/IO 追踪
IO 等待高iostat -xbiolatency/blktrace
内存不足 / OOMfree/dmesg/proc/<pid>/smaps
网络延迟高ss -ti/pingtcptop/tcpretrans
磁盘写满df -hiotop/lsof
连接数耗尽ss -sss -ant state time-wait
cgroup throttlecpu.statbpftrace 调度追踪
内存碎片/proc/buddyinfonumastat
NIC 丢包ethtool -Ssoftnet_stat

5.2 诊断命令一键检查脚本

#!/bin/bash
# perf_check.sh:30 秒全局性能快照
 
echo "=== 1. 系统基础 ==="
uname -r; uptime; nproc
 
echo "=== 2. CPU ==="
mpstat 1 3 | tail -2
 
echo "=== 3. 内存 ==="
free -h
grep -E "SwapTotal|SwapFree|MemAvailable" /proc/meminfo
 
echo "=== 4. IO ==="
iostat -x 1 3 | grep -v "^$\|^Linux"
 
echo "=== 5. 网络 ==="
ss -s
ethtool -S eth0 2>/dev/null | grep -E "rx_missed|drop|error" | grep -v " 0$"
 
echo "=== 6. 进程 TOP ==="
ps aux --sort=-%cpu | head -10
 
echo "=== 7. 内核消息 ==="
dmesg | tail -20 | grep -i "error\|warning\|killed\|oom"
 
echo "=== 8. cgroup throttle(当前进程)==="
cat /sys/fs/cgroup/cpu/cpu.stat 2>/dev/null
 
echo "=== 完成 ==="

第 6 章 专栏总结:构建完整的性能知识体系

6.1 各篇文章的定位与协同

本专栏的 10 篇文章构成了一个完整的横向整合知识体系,填补了各子系统专栏(进程管理、内存管理、文件系统、网络协议栈)在”调优视角”上的空白:


graph TD
    A["性能问题"] --> B{"CPU 使用率高?"}
    B -->|是| C["01 perf 火焰图<br/>找热点函数"]
    C --> D["02 微架构优化<br/>Cache/分支/SIMD"]
    B -->|否 延迟高| E["OFF-CPU 问题"]
    E --> F["09 BPF + offcputime<br/>找等待热点"]
    F --> G{"等待原因"}
    G -->|"等锁"| H["03 调度延迟<br/>锁竞争分析"]
    G -->|"等IO"| I["05/06 磁盘IO调优<br/>fio / io_uring"]
    G -->|"等调度"| J["03 CPU亲和性<br/>cgroup throttle"]
    G -->|"等内存"| K["04 NUMA / HugePage<br/>内存带宽"]
    G -->|"等网络"| L["07 网络调优<br/>TCP参数 / BBR"]
    B -->|否 系统调用多| M["08 vDSO / io_uring<br/>批量化系统调用"]

    classDef tool fill:#6272a4,stroke:#bd93f9,color:#f8f8f2
    classDef decision fill:#44475a,stroke:#ff79c6,color:#f8f8f2
    classDef root fill:#282a36,stroke:#50fa7b,color:#50fa7b
    class A root
    class B,G decision
    class C,D,E,F,H,I,J,K,L,M tool

6.2 性能调优的三个层次

层次 1:系统调参(无需修改代码)

这是最快捷的优化,通过 sysctlethtool、内核启动参数和 cgroup 配置,调整系统行为以匹配工作负载特征。效果立竿见影,但受限于应用的访问模式无法根本改变。本专栏的 03、04、07 篇主要覆盖此层次。

层次 2:应用配置调优(修改配置,无需改代码)

调整应用程序的运行时配置(JVM 参数、数据库 buffer pool 大小、连接池配置、IO 模式选择)。这一层次的改动需要重启服务,但通常有更大的性能提升空间。本专栏的 04、05、06 篇覆盖此层次。

层次 3:代码级优化(需要修改和重新部署代码)

针对微架构(缓存友好数据布局、消除分支预测失效、SIMD 向量化)、系统调用批量化(io_uringwritev)的代码改动。这一层次成本最高,但效果也最彻底。本专栏的 02、08 篇主要覆盖此层次。

调优顺序建议:先做层次 1(系统调参,成本最低),确认效果后再考虑层次 2(应用配置),必要时才进行层次 3(代码修改)。

6.3 知识图谱连接

本专栏与 Linux 其他专栏形成了紧密的双向链接——相互补充,构建完整的 Linux 性能知识体系:

至此,Linux 系统性能调优专栏完结。从 CPU 的单个时钟周期(微架构优化),到 OS 调度延迟(毫秒级),到跨子系统的全链路诊断(BPF + OFF-CPU)——希望这条从微观到宏观的完整链路,成为你面对任何性能问题时的可靠参考。


思考题

  1. USE 方法从资源视角出发(CPU/Memory/Disk/Network 的利用率、饱和度、错误),RED 方法从服务视角出发(Rate、Errors、Duration)。在微服务架构中,你如何结合两种方法?当 RED 指标显示延迟升高但 USE 指标显示所有资源利用率正常时,瓶颈可能在哪里?
  2. 99.9 分位延迟突增但平均延迟正常——这类尾延迟问题无法通过重现排查。你如何设计 always-on 的观测体系?持续采集 perf 数据的存储成本如何控制?eBPF 的’事件触发采样’(只在延迟超过阈值时记录调用栈)相比 perf 的周期性采样有什么优势?
  3. 容器内 top/free/iostat 显示的是宿主机数据。在 K8s 集群中,cAdvisor 提供容器级 CPU/内存/IO 指标。但 cAdvisor 的采集粒度(默认 15 秒)是否足够捕捉短暂的性能毛刺?nsenter 进入容器 namespace 执行宿主机工具是否更精确?