Linux 性能问题的本质是资源争抢:CPU、内存、磁盘 IO,三个核心资源中任何一个达到瓶颈,系统都会"卡"。但卡的表现千差万别:负载高未必 CPU 忙,内存少未必真不够,磁盘满未必 du 算得出。诊断的关键是分清"使用率"和"饱和度"。
Netflix 性能大师 Brendan Gregg 提出的诊断框架。每遇到性能问题,对每个资源(CPU/内存/磁盘/网络)依次问这三个问题——能覆盖 80% 以上的性能故障定位。
# top 看 %Cpu(s) 那一行,这几个字段含义不同,根因完全不同 top # 或更精确 mpstat -P ALL 1
| 字段 | 含义 | 对应根因 |
|---|---|---|
us (user) | 用户态 CPU | 应用进程跑得猛(Java/Python 死循环、计算密集) |
sy (system) | 内核态 CPU | 系统调用频繁(IO/上下文切换/锁竞争) |
wa (iowait) | 等 IO 完成 | 磁盘慢,CPU 在等数据(不是 CPU 的锅) |
si (softirq) | 软中断 | 网卡大流量、中断风暴 |
hi (hardirq) | 硬中断 | 硬件中断频繁 |
st (steal) | 被 hypervisor 偷走 | 云主机超卖(虚拟机才有) |
# 1. 找进程 top # 按 P 按 CPU 排序 ps aux --sort=-%cpu | head # 2. 找到具体哪个线程(Java/多线程应用必看) top -Hp <pid> # 按 P 排序,看 PID 列(其实是 TID) # 3. Java 应用:把 TID 转 16 进制,在 jstack 里搜 printf "%x\n" <tid> # 比如得到 1a2b jstack <pid> | grep -A 20 "nid=0x1a2b" # 直接看到死循环或热点代码栈 # 4. 通用方法:perf 抓函数热点 perf top -p <pid>
# 上下文切换次数(看 cs 列) vmstat 1 # cs > 几万/s 通常是问题,看是不是线程太多了 # 按进程看上下文切换 pidstat -w 1 # cswch/s 自愿切换(锁等待) nvcswch/s 被动切换(CPU 抢占) # 系统调用追踪 strace -c -p <pid> # 几秒后 Ctrl+C,看哪个系统调用占比高
# wa 高了直接跳去看磁盘 IO,见 Case 08 iostat -x 1 iotop # 看哪个进程在猛读写
# 看中断分布 cat /proc/softirqs # 大流量服务器看 NET_RX 是否暴涨 cat /proc/interrupts # 网络中断集中在 CPU0,负载不均 cat /proc/irq/<irq-num>/smp_affinity # 改 affinity 把中断分散到多核(RPS/RSS)
wa 高 ≠ CPU 不够。iowait 是"CPU 闲着但任务在等 IO 完成"的状态,加 CPU 没用,要加磁盘性能或优化 IO 模式。
Linux 的 Load Average 不是"CPU 使用率",而是 正在运行 + 不可中断睡眠 (D 状态) 的进程数。D 状态进程不消耗 CPU,但会拉高 load。
# 1. 找出所有 D 状态进程 ps -eo state,pid,user,cmd | grep "^D" # 或更直观 ps aux | awk '$8 ~ /^D/ {print}' # 2. 实时看 D 状态进程数 vmstat 1 # 关注 procs 段的 b 列(blocked on I/O) # 3. 看具体卡在哪个系统调用 cat /proc/<pid>/stack # 内核栈,看堵在哪 cat /proc/<pid>/wchan # 进程在等什么 # 4. 检查所有 mount 点是否健康 df -h # 卡住的 NFS 挂载点会让 df 也 hang 住! # 5. 测试每个挂载点能不能访问 timeout 3 ls /mnt/nfs1 && echo OK || echo HANG
soft,intr,timeo=30 选项,服务端不响应时客户端会超时报错而不是死等。hard 模式下 NFS 服务挂了能让整台机器跟着挂。
free -h
total used free shared buff/cache available Mem: 15Gi 12Gi 300Mi 50Mi 2.7Gi 2.5Gi Swap: 2.0Gi 500Mi 1.5Gi
| 列 | 含义 | 正确解读 |
|---|---|---|
used | 已用内存 | 包含 cache,看起来吓人但不全是真用 |
free | 完全空闲 | 低不代表危险,Linux 喜欢用满 cache |
buff/cache | 缓存 | 可被回收,不算"真正占用" |
available | 实际可用 | 这才是判断的关键!低于 10% 才警惕 |
# 按 RSS(实际物理内存)排序找进程 ps aux --sort=-rss | head # top 里按 M 排序也行 top # 按 M # 单个进程详细内存使用 pmap -x <pid> | tail -1
| 指标 | 含义 | 关注度 |
|---|---|---|
VSZ | 虚拟内存(包括没用到的) | 低 — 虚的,JVM 会很大但不一定真占 |
RSS | 常驻物理内存(实际占用) | 高 — 这才是真正吃内存的量 |
SHR | 共享内存 | 中 — 多进程共享的库 |
所有进程 RSS 加起来远小于 used 内存——这种情况看 内核内存:
# slab 内核对象缓存(dentry/inode 等) cat /proc/meminfo | grep -E "Slab|SReclaim" # slab 详细分类 slabtop # 看哪个 slab 占用大 # 大量小文件操作会让 dentry cache 暴涨,手动清 echo 2 > /proc/sys/vm/drop_caches # 清 dentry 和 inode cache echo 3 > /proc/sys/vm/drop_caches # 清所有 cache
# 1. 看 OOM 历史 dmesg -T | grep -i "killed process" grep -i "oom" /var/log/messages # 2. 详细的 OOM 现场(很重要,有内存快照) dmesg -T | grep -B 2 -A 20 "oom-killer" # 输出会列出当时所有进程的内存占用,排序后的 oom_score
内核根据 oom_score 选受害者,分数越高越先被杀。计算因素:
# 查看进程的 oom_score cat /proc/<pid>/oom_score cat /proc/<pid>/oom_score_adj # 保护重要进程不被 OOM(设为 -1000 永不杀) echo -1000 > /proc/<pid>/oom_score_adj # 反过来,让某进程优先被杀 echo 1000 > /proc/<pid>/oom_score_adj
# 1. 加内存(最直接) # 2. 关闭过度内存提交 sysctl vm.overcommit_memory=2 # 0=启发式(默认), 1=允许超量, 2=严格控制 # 3. JVM 应用必须限制堆大小 # -Xmx 不要超过物理内存的 70%,留出空间给堆外 # 4. 找出内存泄漏(应用层问题) pmap -x <pid> # 观察一段时间 RSS 是否持续增长
# 1. 看 swap 总体使用 free -h swapon -s # 2. 看哪些进程用了 swap for file in /proc/*/status; do awk '/VmSwap|Name/{printf $2 " " $3}END{print ""}' $file done | sort -k 2 -n -r | head # 3. 实时监控 swap in/out vmstat 1 # 看 si(swap in)和 so(swap out)列,持续不为 0 就是 thrashing
# 当前值(默认 60,值越高越倾向于使用 swap) cat /proc/sys/vm/swappiness # 数据库/Redis 等内存敏感服务,建议调到 1-10 sysctl -w vm.swappiness=10 # 永久生效,写入 /etc/sysctl.conf echo "vm.swappiness = 10" >> /etc/sysctl.conf # 临时禁用 swap(谨慎,可能触发 OOM) swapoff -a swapon -a # 重新启用
df -h 显示某个分区 95%+,几乎满了du -sh / 加起来远小于 df 显示的占用Linux 删除文件的机制:rm 只是从目录里删除引用,如果还有进程持有这个文件的 fd,文件实际不会被释放,但 du 看不到它(目录里没了),df 却显示空间被占。
# 1. 确认是不是这个问题 df -h # 看分区使用 du -sh /* # 加起来对比,差距大就是删除未关闭 # 2. 找出"已删除但被占用"的文件(关键命令!) lsof | grep "deleted" # 或更精确 lsof +L1 # 列出链接数为 0 的文件(已删除但被打开) # 3. 输出示例 # java 12345 root 3w REG 8,1 10737418240 /var/log/app.log (deleted) # ↑ 10GB 占着空间
# 方案 A:重启占用文件的进程(最干净) systemctl restart <service> # 方案 B:不能重启服务,用 /proc 清空文件内容 # 比如 lsof 显示 java 12345 持有 fd 3 : > /proc/12345/fd/3 # 文件被截断为 0,空间立即释放 # 但应用还在写,新写入会从 0 开始 # 方案 C:重启大法(最后手段) reboot
rm 删除,但应用没有重新打开文件(没用 logrotate 的 copytruncate)。正确做法:不直接删日志,而是 truncate -s 0 app.log 清空,或者发 SIGUSR1 让应用重新打开日志文件。
copytruncate 选项,自动处理这个问题。
df -h 显示空间充足每个文件占用一个 inode,文件系统创建时 inode 数量就固定了。海量小文件场景(邮件、缓存、session)会先把 inode 用完,空间还很多但写不下。
# 1. 看 inode 使用 df -i # 看 IUse% 列,接近 100% 就是这个问题 # 2. 找哪个目录小文件最多 for i in /*; do echo -n "$i: "; find $i 2>/dev/null | wc -l; done | sort -t: -k2 -n # 或者一层一层往下找 find /var -xdev -type f | cut -d/ -f 1-4 | sort | uniq -c | sort -rn
# 1. 直接删除小文件大户(找到后) find /var/spool/postfix -type f -delete # 2. 文件太多,rm * 会爆 argument list too long find /path/to/dir -type f -delete # 或 ls | xargs -n 100 rm
-N)。XFS 默认动态分配,不容易遇到这问题。
# 看磁盘性能(关键命令) iostat -x 1 # -x 显示扩展信息,1 = 每秒刷新
| 字段 | 含义 | 警戒值 |
|---|---|---|
%util | 磁盘繁忙度 | > 80% 接近饱和 |
await | IO 平均等待 ms | > 20ms 慢,> 100ms 严重 |
r/s, w/s | 每秒读写次数 | 对比硬件标称 IOPS |
rkB/s, wkB/s | 每秒读写量 | 对比带宽上限 |
avgqu-sz | 平均队列长度 | > 1 有排队,> 10 严重 |
svctm | 服务时间 | 已废弃,别看 |
iotop # 类似 top,但看 IO iotop -o # 只显示有 IO 的进程 # 或者 pidstat pidstat -d 1 # kB_rd/s 读速率, kB_wr/s 写速率
| 存储类型 | 4K 随机 IOPS | 顺序带宽 | 典型延迟 |
|---|---|---|---|
| 机械硬盘 7200rpm | ~150 | ~150 MB/s | 10-15 ms |
| SATA SSD | ~10K | ~500 MB/s | 0.1 ms |
| NVMe SSD | ~100K+ | ~3500 MB/s | < 0.1 ms |
| NFS(千兆网) | ~5K | ~100 MB/s | 1-5 ms |
fio -name=test -ioengine=libaio -direct=1 -rw=randread -bs=4k -size=1G -iodepth=32
uptime
10:23:01 up 30 days, 3:21, 2 users, load average: 2.05, 4.50, 8.10
| 1m / 5m / 15m | 含义 | 处理 |
|---|---|---|
| 2.0 / 4.5 / 8.1 | 负载在下降(峰值过了) | 观察即可 |
| 8.1 / 4.5 / 2.0 | 负载在上升(故障中!) | 立即排查 |
| 5.0 / 5.0 / 5.0 | 稳定高负载 | 正常业务,看是否符合预期 |
load 的合理上限 ≈ CPU 核数。8 核机器 load 持续 > 8 才算饱和。
nproc # 看核数 cat /proc/cpuinfo | grep processor | wc -l
vmstat 1 看 r 列(运行队列)和 b 列(阻塞队列)