Load 已经 24,CPU 为什么还有空闲?建立 Linux 性能排查的证据链
凌晨告警响起:一台 16 核服务器的 Load Average 升到 24,接口 P99 延迟翻了几倍。第一反应通常是“CPU 不够,赶紧扩容”,可 top 却显示 CPU 仍有大量空闲。重启服务后指标暂时恢复,几个小时后问题再次出现。
这个现场至少留下了三个问题:Load 统计的究竟是什么?服务是在抢 CPU,还是在等待磁盘、网络存储或内存回收?即使找到了一个高指标,又怎样证明它是根因而不是结果?
性能分析最容易陷入两个误区:一是看到某个指标高就立即下结论,二是记住了很多命令,真正出问题时却不知道下一条命令为什么要运行。本文不打算罗列所有 Linux 性能工具,而是围绕一条可验证的主线展开:
先确认业务现象,再判断哪类资源出现压力;从系统定位到进程、线程,最后落到函数、系统调用或具体文件和连接。
CPU、内存、磁盘和网络不是互相独立的。内存回收可能带来磁盘 I/O,网络小包可能推高软中断,磁盘等待又可能让平均负载上升。真正有效的分析,依靠的是指标之间的关联,而不是某一张孤立的截图。
文中的 /proc、PSI、cgroup、perf、strace 等接口和工具面向 Linux,不能直接照搬到 macOS 或 Windows。命令输出还会随内核、发行版、procps、sysstat 与 cgroup 版本略有差异;生产排查时应先确认环境,并与同一机器的正常基线比较。当前整理环境是 macOS,因此 Linux 专属命令只依据内核和工具文档核对,没有在本机伪造运行结果。
一、面对“Load 24”,为什么不能先猜 CPU 不够?
1. 先描述问题,不要急着猜原因
先把问题说清楚:
- 业务表现是什么:响应变慢、吞吐下降、请求超时、错误率升高,还是进程被杀?
- 问题从什么时候开始:持续发生、周期性发生,还是偶发尖峰?
- 影响范围有多大:单个请求、单个进程、容器、某台机器,还是整个集群?
- 最近发生了什么变化:代码发布、配置修改、流量增长、数据量增长、依赖异常或宿主机迁移?
性能优化最终服务于业务指标。CPU 从 80% 降到 60%,不一定代表优化成功;如果延迟、吞吐和错误率没有改善,这个变化可能没有实际价值。
2. 用 USE 思路缩小范围
对 CPU、内存、磁盘、网卡等系统资源,可以先从三个角度检查:
| 维度 | 含义 | 示例 |
|---|---|---|
| Utilization | 资源使用了多少 | CPU 使用率、网卡带宽使用率、磁盘忙碌时间 |
| Saturation | 是否出现排队和等待 | CPU 运行队列、磁盘请求队列、Socket 队列 |
| Errors | 是否发生错误 | OOM、磁盘错误、网络丢包、TCP 重传 |
一个资源使用率不高,也可能已经出现延迟问题;一个资源使用率很高,也不一定已经饱和。因此,使用率、排队和错误应放在一起看。
3. 按层次向下定位
1 | 业务现象:延迟、吞吐、错误率 |
如果一开始就对某个进程运行 perf 或 strace,很可能花了不少时间,却分析错了方向。
4. 关注趋势,而不是单个瞬时值
- 使用相同采样间隔对比不同工具,实时排查通常从
1s开始。 - 连续观察多组数据,区分持续瓶颈和瞬时尖峰。
- 建立正常时的基线,异常值应与正常业务时段比较。
vmstat、iostat等工具的第一组数据可能是开机以来的平均值,实时分析时重点看后续采样。/proc中很多数据是累计值,应间隔采样并比较增长速度。
5. 从低开销工具开始
推荐顺序:
1 | 系统概览 |
perf、strace、抓包和动态追踪都会带来额外开销。生产环境中应限制目标、采样时间和输出量,先保存现场,再考虑重启或修改参数。
6. 压力指标 PSI
较新的 Linux 内核提供 Pressure Stall Information,用来描述任务因为 CPU、内存或 I/O 资源不足而停顿的时间:
1 | cat /proc/pressure/cpu |
some:至少有部分任务因资源不足而停顿。full:所有非空闲任务都同时停顿。系统级 CPUfull没有可解释的实际含义;部分较新内核为了接口兼容会报告为 0,CPU 压力主要看some。avg10/avg60/avg300:最近 10、60、300 秒的压力比例。
PSI 不直接告诉你根因,但很适合回答一个问题:资源压力是否已经真实影响任务运行。
如果 /proc/pressure/ 不存在,需要检查内核版本、CONFIG_PSI 配置以及启动参数;不能把“文件不存在”解释为系统没有资源压力。
二、CPU 问题分析
CPU 问题不只是“使用率过高”。运行队列过长、上下文切换频繁、单核打满、软中断异常、虚拟机被宿主机抢占 CPU,都会让业务变慢。
排查 CPU 时,先回答三个问题:
- CPU 时间主要消耗在哪里?
- 是谁在消耗,还是谁在等待?
- 最终是哪段代码或哪类内核行为导致的?
2.1 先读懂平均负载
uptime 和 top 中的三个 Load Average,分别表示过去 1、5、15 分钟的平均活跃任务数。
活跃任务包括:
R:正在 CPU 上运行,或正在运行队列中等待 CPU。D:不可中断睡眠,通常正在等待磁盘、NFS 或其他内核 I/O。
1 | uptime |
分析时先与逻辑 CPU 数比较:
- Load 接近 CPU 数:系统基本没有额外调度余量。
- Load 持续高于 CPU 数:运行队列或不可中断任务正在积压。
- 1 分钟值明显高于 15 分钟值:负载近期快速上升。
- 1 分钟值明显低于 15 分钟值:负载正在回落。
Load 高不等于 CPU 使用率高。大量
D状态任务同样会抬高负载。
2.2 CPU 时间告诉我们该往哪里查
| 指标 | 含义 | 异常时优先排查 |
|---|---|---|
%us / %usr |
用户态 CPU 时间 | 应用计算、死循环、低效算法、序列化 |
%nice |
低优先级用户态时间 | 设置过 nice 的计算任务 |
%sy / %system |
内核态 CPU 时间 | 系统调用、上下文切换、内核线程 |
%wa / %iowait |
CPU 空闲但存在未完成 I/O 的时间 | 磁盘、网络存储、Swap |
%hi / %irq |
硬中断时间 | 网卡、存储设备、中断分布 |
%si / %soft |
软中断时间 | 网络收发、定时器等软中断 |
%st / %steal |
被虚拟化宿主机抢占的时间 | 宿主机超卖和资源竞争 |
%id / %idle |
空闲时间 | 持续接近 0 表示 CPU 接近饱和 |
这里有三个容易忽略的细节:
- 多核机器上,多线程进程的
%CPU可以超过100%。 - 整体 CPU 不高,不代表没有单核瓶颈;要检查每个 CPU 核心。
iowait是 CPU 时间统计,不是磁盘利用率,仍需结合磁盘延迟和队列判断。
2.3 运行队列和上下文切换
vmstat 是 CPU 初筛时非常好用的工具:
1 | vmstat 1 10 |
重点看:
1 | r b | in cs | us sy id wa st |
| 指标 | 含义 | 常见判断 |
|---|---|---|
r |
正在运行和等待 CPU 的任务数 | 持续大于 CPU 数,说明 CPU 竞争明显 |
b |
不可中断睡眠任务数 | 持续升高通常应转入 I/O 分析 |
in |
每秒中断次数 | 突增时检查设备和网络 |
cs |
每秒上下文切换次数 | 结合 sy、线程数和业务基线判断 |
上下文切换分为两类:
- 自愿切换:进程等待 I/O、锁、条件变量等资源,主动让出 CPU。
- 非自愿切换:时间片耗尽或被更高优先级任务抢占。
1 | pidstat -w -p <PID> 1 5 |
| 指标 | 含义 |
|---|---|
cswch/s |
每秒自愿上下文切换 |
nvcswch/s |
每秒非自愿上下文切换 |
cswch/s高:检查 I/O 等待、锁竞争、条件变量和频繁唤醒。nvcswch/s高:检查线程数、运行队列和 CPU 争抢。
上下文切换没有适用于所有机器的固定阈值,必须与正常基线、吞吐和 CPU 时间一起分析。
2.4 CPU 常用工具
top:先看全局,再找进程
1 | top |
常用交互:
1:展开每个 CPU。P:按 CPU 使用率排序。H:显示线程。M:按内存排序。
重点观察:
- Load Average 和任务状态。
%us、%sy、%wa、%si、%st。- 是否只有某个 CPU 核心打满。
- 高 CPU 进程是否处于
R、D或Z状态。
mpstat:检查单核热点和 CPU 时间分布
1 | mpstat -P ALL 1 5 |
它适合发现 CPU 使用是否均衡,以及用户态、内核态、I/O 等待、中断或 steal 是否集中在少数核心。
pidstat:定位进程和线程
1 | # 所有进程 |
重点看:
%usr:进程用户态 CPU。%system:进程内核态 CPU。%wait:任务等待 CPU 调度的时间。
ps 和 pstree:补充进程状态与父子关系
1 | ps -eo pid,ppid,tid,psr,stat,pcpu,pmem,comm --sort=-pcpu | head |
psr:线程当前或最近运行的 CPU。stat:重点关注R、D、Z。pstree:适合发现频繁创建的子进程和外部命令。
2.5 从进程继续定位到函数
找到高 CPU 进程后,下一步不是立即“优化代码”,而是先确认热点到底在哪里。
实时查看热点:
1 | perf top -p <PID> |
采样调用栈:
1 | perf record -F 99 -g -p <PID> -- sleep 15 |
分析时重点看:
- 热点位于应用、动态库还是内核。
- 哪个函数占用最多采样。
- 调用链上是谁反复调用热点函数。
如果大量显示未知符号,应检查:
- 二进制和动态库是否缺少符号。
- 调试构建是否包含
-g。 - 调用栈不完整时是否缺少 frame pointer 或 DWARF 支持。
系统态 CPU 较高时,还可以短时间检查系统调用:
1 | strace -c -p <PID> |
重点看是否存在高频或持续失败的 futex、read、write、poll 等调用。strace 会影响目标进程,生产环境中应谨慎使用。
2.6 按现象选择 CPU 排查路径
用户态 CPU 高
1 | top 找进程 |
常见原因:死循环、复杂计算、忙轮询、低效算法、流量增长。
系统态 CPU 高
1 | pidstat 查看进程内核态 CPU 和上下文切换 |
常见原因:高频系统调用、锁竞争、频繁调度、内存回收和内核线程繁忙。
Load 高,但 CPU 使用率不高
重点检查:
1 | vmstat 1 10 |
如果 b 和 D 状态任务增多,问题通常在磁盘、NFS、块设备或其他内核资源等待。
软中断或硬中断高
1 | cat /proc/interrupts |
如果 NET_RX、NET_TX 增长很快,应继续检查网络 PPS、丢包、小包和异常流量。还要观察中断是否过度集中在某个 CPU。
普通工具找不到高 CPU 进程
可能是:
- 短生命周期进程创建后迅速退出。
- 应用持续崩溃和重启。
- 父进程频繁执行外部命令。
- CPU 消耗在软中断或内核线程。
1 | pstree -ap |
环境支持 eBPF/BCC 时,可以使用 execsnoop 捕获短时进程。
容器 CPU 使用率不高,但请求变慢
容器可能被 CPU 配额限流。cgroup v2 可以检查:
1 | cat /sys/fs/cgroup/cpu.stat |
重点关注 nr_throttled 和 throttled_usec 是否持续增长。此时宿主机 CPU 可能仍有空闲,但容器已经用完自己的配额。容器运行时和 cgroup namespace 会影响实际挂载路径,应先从 /proc/self/cgroup 与挂载信息确认当前进程所属 cgroup,不要假定所有环境都把目标文件暴露在同一路径。
2.7 CPU 常见误区
- Load 高就是 CPU 高:错误,
D状态任务也会提高 Load。 - 整体 CPU 不高就没有问题:错误,可能只有单核打满。
- 进程 CPU 不会超过 100%:错误,多线程进程可以占用多个核心。
- 上下文切换高就一定异常:错误,应与业务基线和有效吞吐一起判断。
- iowait 高就等于磁盘 100%:错误,必须继续检查设备延迟和队列。
- 调低进程优先级就解决了问题:通常只是缓解竞争,没有消除根因。
三、内存问题分析
Linux 会主动使用空闲内存做缓存。因此,内存分析最重要的一步,是先区分“内存被有效利用”和“系统已经出现内存压力”。
排查内存时应依次回答:
- 系统是否真的缺内存?
- 内存主要去了哪里?
- 是合理工作集、缓存、泄漏、碎片,还是内核内存?
- 是否已经触发回收、Swap 或 OOM?
3.1 先理解虚拟内存和物理内存
进程访问的是虚拟地址空间,内核通过页表映射到物理内存。进程申请虚拟内存后,物理页通常在首次访问、触发缺页异常时才真正分配。
进程用户空间主要包括:
- 代码段和只读数据。
- 数据段:全局变量和静态变量。
- 堆:
malloc()、new等动态分配。 - 文件映射区:动态库、共享内存和
mmap()文件。 - 栈:局部变量和函数调用上下文。
内存紧张时,Linux 通常依次尝试:
- 回收页缓存和可回收 Slab。
- 将不活跃匿名页换出到 Swap。
- 内存仍然不足时触发 OOM。
3.2 系统内存指标
1 | free -h |
| 指标 | 含义 |
|---|---|
total |
物理内存总量 |
used |
已使用内存,计算方式会随工具版本略有差异 |
free |
完全空闲的物理内存 |
shared |
共享内存,主要包括 tmpfs |
buff/cache |
Buffer、页缓存和部分可回收 Slab |
available |
不发生明显 Swap 时,新进程可使用的估算内存 |
判断内存是否紧张时,优先看 available,不要只盯着 free。free 很低但 available 充足,通常只是 Linux 充分利用了内存做缓存。
更详细的组成可以查看:
1 | cat /proc/meminfo |
重点字段:
MemAvailableCached、BuffersAnonPages、Mapped、ShmemSlab、SReclaimable、SUnreclaimDirty、WritebackPageTablesSwapTotal、SwapFree
3.3 进程内存指标
| 指标 | 含义 | 注意事项 |
|---|---|---|
VIRT / VSZ |
虚拟地址空间 | 大不等于实际占用物理内存多 |
RES / RSS |
常驻物理内存 | 包含共享页,进程间可能重复统计 |
SHR |
可能共享的常驻页面 | 不代表所有页面都真正被共享 |
PSS |
共享页按进程数均摊后的物理内存 | 统计多进程总占用时更准确 |
USS |
进程独占的物理内存 | 进程退出后可立即释放的主要部分 |
VmSwap |
进程换出到 Swap 的内存 | 可从 /proc/<PID>/status 查看 |
先找高内存进程:
1 | top |
再查看指定进程:
1 | pidstat -r -p <PID> 1 10 |
smaps_rollup 能快速给出 PSS、匿名页、文件页和私有页等汇总,比直接阅读完整 smaps 更方便。
3.4 缺页异常
缺页异常分为:
- 次缺页
minor fault:不需要磁盘 I/O,例如分配新的匿名页或复用页缓存。 - 主缺页
major fault:需要从文件或 Swap 读取页面,开销明显更大。
1 | pidstat -r 1 10 |
重点关注:
minflt/s:每秒次缺页。majflt/s:每秒主缺页。
主缺页持续升高时,需要检查可用内存、Swap 活动、工作集大小和文件读取模式。
3.5 Page Cache、Buffer 和 Slab
- Page Cache:缓存文件数据。
- Buffer:缓存块设备相关数据。
- Slab:缓存内核对象。
SReclaimable:可回收 Slab。SUnreclaim:不可回收 Slab。
当进程 RSS 都不高,但系统可用内存持续下降时,应检查 Slab:
1 | slabtop |
常见增长对象包括 dentry、inode、网络对象和文件系统对象。如果 SUnreclaim 持续增长,需要进一步检查内核模块、驱动、容器和网络行为。
buff/cache 很高时,先看 available:
available充足且无 Swap 活动:通常是正常缓存。available持续下降,并伴随回收、主缺页或 Swap:缓存已经开始参与内存竞争。
3.6 Swap:看活动,不要只看占用
1 | vmstat 1 10 |
| 指标 | 含义 |
|---|---|
swpd |
已使用的 Swap |
si |
每秒从 Swap 换入内存 |
so |
每秒将内存换出到 Swap |
swpd 不为 0,但 si/so 长期为 0,不一定存在当前性能问题,可能只是历史上换出的冷页仍留在 Swap。
如果 si/so、主缺页和磁盘延迟同时升高,说明系统正在活跃换页,应用延迟通常会明显恶化。
查找使用 Swap 的进程:
1 | grep -H '^VmSwap:' /proc/[0-9]*/status 2>/dev/null |
3.7 如何判断内存泄漏
典型泄漏表现是:
- 在相似负载下,进程 RSS 长期单调增长。
- 请求结束或任务完成后,内存仍不回落。
- 未释放分配的数量和总字节数持续增长。
排查路径:
1 | free/vmstat 确认系统趋势 |
环境支持 BCC 时,可以追踪未释放分配:
1 | /usr/share/bcc/tools/memleak -p <PID> |
C/C++ 程序在测试环境还可以使用:
- AddressSanitizer:检查越界、Use After Free 和泄漏。
- LeakSanitizer:检测泄漏。
- Valgrind Memcheck:运行开销较大,但适合离线定位。
- Heap profiler:分析分配热点和堆增长。
需要注意,RSS 不下降不一定是泄漏,也可能是:
malloc分配器缓存了已释放内存。- 内存碎片导致页面无法归还内核。
- 应用缓存、对象池或队列没有上限。
- Page Cache 或文件映射增长。
3.8 OOM 与容器内存限制
查看系统 OOM 日志:
1 | dmesg -T | grep -Ei 'out of memory|oom|killed process' |
确认:
- 是整机 OOM 还是 cgroup OOM。
- 哪个进程被杀。
- 当时的内存、Swap 和进程占用。
- 容器内存上限是否过小。
进程的 OOM 倾向:
1 | cat /proc/<PID>/oom_score |
cgroup v2 中可以检查:
1 | cat /sys/fs/cgroup/memory.current |
memory.events 中的 oom、oom_kill 持续增长,说明容器或 cgroup 已经触发内存限制。
不要只通过降低 oom_score_adj 掩盖泄漏,否则可能导致其他进程被杀,甚至让整机失去响应。
3.9 内存快速排查路径
1 | free -h |
按内存去向继续分析:
1 | 进程 RSS 高 |
3.10 内存常见误区
free很低就是内存不足:错误,应优先看available。VIRT很大就是泄漏:错误,虚拟地址不等于物理内存。- 把所有进程 RSS 相加就是总内存:错误,共享页会重复统计。
- Swap 已使用就一定很慢:错误,关键看当前
si/so和主缺页。 - RSS 不下降就是没有调用
free():错误,分配器缓存和碎片也会造成这种现象。 - 缓存占内存应该立即清理:错误,缓存通常在提高性能,并可在压力下回收。
drop_caches是常规优化手段:错误,它会破坏缓存,只适合受控测试。- OOM 只会在整机内存耗尽时发生:错误,cgroup 达到内存上限也会触发 OOM。
四、文件系统与磁盘 I/O 分析
文件系统和磁盘经常被混为一谈,但它们处在不同层次:
1 | 应用程序 |
文件系统负责组织文件、目录和元数据;块设备负责真正持久化数据。分析 I/O 问题时,要先分清是容量、文件系统缓存、应用读写方式,还是底层设备出现瓶颈。
4.1 文件系统中几个重要概念
- inode:保存文件类型、权限、大小、时间和数据块位置等元数据。
- dentry:保存文件名和目录层级关系,主要存在于内存缓存中。
- Page Cache:缓存文件内容,减少直接访问慢速磁盘。
- VFS:为 ext4、XFS、NFS、OverlayFS 等文件系统提供统一接口。
文件数据空间充足,不代表文件系统一定还能创建新文件。inode 数量耗尽时,即使磁盘仍有剩余空间,也会出现 No space left on device。
4.2 I/O 模式会直接影响性能
常见分类:
- 缓冲 I/O 与非缓冲 I/O:是否使用语言或标准库缓冲。
- Buffered I/O 与 Direct I/O:是否经过操作系统 Page Cache。
- 阻塞 I/O 与非阻塞 I/O:当前线程是否因等待而停住。
- 同步 I/O 与异步 I/O:提交后是否需要等待整个操作完成。
数据库常使用 Direct I/O 自己管理缓存;日志和事务系统常关注 fsync() 延迟;普通文件读取通常受益于 Page Cache。脱离具体访问模式讨论“磁盘快不快”,结论很容易失真。
4.3 先检查容量、inode 和挂载关系
1 | df -h |
df -h:文件系统容量。df -i:inode 使用率。findmnt:挂载点、设备和文件系统类型。lsblk -f:块设备、文件系统和挂载关系。
查找大目录:
1 | du -xhd1 <目录> | sort -h |
-x 可以限制在当前文件系统内,避免跨挂载点统计。
4.4 df 和 du 为什么可能不一致
常见原因:
- 文件已经被删除,但仍被进程打开。
- 目录下存在其他挂载点,统计范围不同。
- 文件系统元数据、预留空间或快照占用。
- 容器 OverlayFS 让宿主机和容器看到的层次不同。
检查“已删除但仍被占用”的文件:
1 | lsof +L1 |
只要进程仍持有文件描述符,文件占用的磁盘空间就不会释放。通常应让应用重新打开日志文件或安全重启对应进程,而不是盲目删除更多文件。
4.5 磁盘 I/O 的关键指标
磁盘性能不能只看 %util。至少要同时观察 IOPS、吞吐、延迟、队列和请求大小。
| 指标 | 含义 | 关注点 |
|---|---|---|
r/s、w/s |
每秒读写请求数,即 IOPS | 随机小 I/O 场景尤其重要 |
rkB/s、wkB/s |
每秒读写吞吐量 | 顺序大块 I/O 场景更重要 |
r_await、w_await |
读写平均完成时间 | 包含排队和设备处理时间 |
aqu-sz |
平均 I/O 队列长度 | 持续增长说明请求正在积压 |
rareq-sz、wareq-sz |
平均读写请求大小 | 用来区分小随机 I/O 和大块 I/O |
%util |
设备处理 I/O 的忙碌时间比例 | 需结合设备类型和队列解释 |
不要机械套用“%util 超过某个值就是瓶颈”:
- 单队列机械盘上,接近 100% 往往表示设备持续忙碌。
- NVMe、RAID、云盘和多队列设备能并行处理请求,
%util的解释更复杂。 - 即使
%util不高,单次同步写或网络存储延迟也可能已经影响业务。
真正关键的是:设备延迟和队列是否明显高于正常基线,业务是否正在等待这些 I/O。
4.6 iostat:确认设备是否存在瓶颈
1 | iostat -xz 1 10 |
分析顺序:
- 看
r_await、w_await是否升高。 - 看
aqu-sz是否积压。 - 看 IOPS、吞吐和请求大小,判断负载类型。
- 再结合
%util和设备基准判断是否饱和。
不同 sysstat 版本的字段名可能略有差异,必要时使用:
1 | man iostat |
4.7 从设备定位到进程
1 | pidstat -d 1 10 |
pidstat -d 常见指标:
kB_rd/s:进程每秒读取量。kB_wr/s:进程每秒写入量。kB_ccwr/s:被取消的写入量。iodelay:块 I/O 和换页等待时间。
iotop 适合按实时读写量和 I/O 等待排序,快速找出最活跃的进程或线程。
如果设备很忙但看不到明显用户进程,还要考虑:
- Page Cache 正在异步回写。
- Swap 或内存回收。
- 文件系统日志和内核线程。
- RAID、LVM、设备映射或网络存储。
- 短生命周期进程。
4.8 从进程定位到文件和系统调用
查看进程打开的文件:
1 | lsof -p <PID> |
查看文件系统调用汇总:
1 | strace -c -p <PID> |
短时间跟踪文件操作:
1 | strace -tt -T -e trace=openat,read,write,fsync,fdatasync -p <PID> |
-tt:显示精确时间。-T:显示系统调用耗时。-e trace=...:只跟踪相关调用,降低输出量。
如果普通 strace 无法捕获短时操作,环境支持 eBPF/BCC 时可以使用:
opensnoop:追踪打开文件。filetop:按文件统计读写。biolatency:统计块设备延迟分布。biosnoop:追踪单次块 I/O。
4.9 文件系统缓存和写回
写入普通文件时,数据往往先进入 Page Cache,并被标记为 Dirty,随后由内核异步写回磁盘。
1 | grep -E 'Dirty|Writeback|Cached|SReclaimable' /proc/meminfo |
如果 Dirty 持续增长、Writeback 活跃并伴随磁盘延迟升高,说明写入速度可能超过设备持续处理能力。此时应用最初可能感觉写入很快,等脏页达到阈值后才突然出现明显阻塞。
同步写场景中,fsync() 或 fdatasync() 的延迟更能直接反映持久化成本。数据库和日志系统的性能问题,常常不是平均吞吐不足,而是同步写尾延迟过高。
4.10 I/O 常见问题的排查路径
磁盘空间不足
1 | df -h 确认文件系统 |
inode 耗尽
1 | df -i |
iowait 高、进程处于 D 状态
1 | vmstat 确认 b/wa |
磁盘延迟高
需要区分:
- 请求量是否超过设备能力。
- 是大量随机小 I/O,还是大块顺序 I/O。
- 写延迟是否远高于读延迟。
- 队列是在应用、文件系统、块层还是远端存储中形成。
- 是否同时发生内存回收或 Swap。
I/O 高,但找不到业务进程
重点检查:
1 | vmstat 1 10 |
可能是异步写回、文件系统日志、Swap、内核线程或设备层任务。
设备错误或文件系统突然只读
性能下降不一定只是负载过高,也可能是设备超时、链路重置或文件系统检测到错误:
1 | dmesg -T | grep -Ei 'I/O error|blk_update|nvme|reset|ext4|xfs|read-only' |
如果日志中出现介质错误、设备重置、文件系统错误或重新挂载为只读,应优先保护数据并检查磁盘、控制器、云盘和宿主机状态。这类问题不能只靠调 I/O 参数解决。
4.11 I/O 常见误区
df有空间就一定能创建文件:错误,inode 也可能耗尽。du统计小于df就是工具错误:错误,可能存在已删除但仍打开的文件。%util100% 就一定无法再处理请求:错误,多队列设备需要结合延迟和队列判断。- IOPS 越高越好:错误,必须结合请求大小、读写模式和延迟。
- 平均延迟正常就没有问题:错误,业务可能受 P99 同步写尾延迟影响。
- iowait 高就是 CPU 不够:错误,它通常提示应转入存储或 Swap 分析。
- 清理 Page Cache 能优化磁盘:通常错误,缓存正是用来减少磁盘访问的。
五、网络问题分析
网络问题最难的地方,是数据会经过很多层:
1 | 应用协议:HTTP、DNS、RPC |
因此,“请求慢”不一定是网络;“ping 正常”也不代表应用连接、TLS、DNS 和服务端处理都正常。
排查网络时,建议依次回答:
- 配置和路由是否正确?
- 网卡是否有错误、丢包或队列积压?
- TCP/UDP 协议栈是否出现重传、连接失败或队列溢出?
- 问题发生在 DNS、建连、传输,还是应用处理阶段?
- 抓包看到的真实收发过程是什么?
5.1 网络性能的关键指标
| 指标 | 含义 | 常见用途 |
|---|---|---|
| 带宽 | 链路理论最大传输速率 | 判断链路上限 |
| 吞吐量 | 单位时间成功传输的数据量 | 判断实际使用率 |
| PPS | 每秒处理的网络包数 | 分析小包和转发压力 |
| RTT | 数据包往返延迟 | 判断链路延迟 |
| 并发连接数 | 当前 TCP/UDP Socket 数量 | 分析连接压力 |
| 丢包率 | 未成功送达的数据包比例 | 定位链路、队列或主机丢包 |
| 重传率 | TCP 重发数据的比例 | 判断拥塞、丢包和超时 |
| 错误率 | 网卡或协议栈错误 | 定位硬件、驱动和协议问题 |
| Socket 队列 | 未处理或未确认的数据 | 判断应用或网络侧积压 |
同样的吞吐量下,小包的 PPS 更高,会消耗更多中断、协议栈和 CPU 资源。因此网络分析必须同时看字节数和包数。
5.2 先检查网卡、地址和路由
1 | ip -br addr |
重点确认:
- 网卡是否为
UP、LOWER_UP。 - IP、掩码、默认路由是否正确。
- MTU 是否与物理网络、VPN、VXLAN 或容器网络匹配。
- RX/TX 的
errors、dropped、overruns是否持续增长。 - ARP/邻居表是否出现
FAILED、INCOMPLETE。
网卡速率、双工和驱动信息:
1 | ethtool <网卡> |
ethtool -S 中的统计字段由驱动定义,名称并不统一,但可以帮助定位 Ring Buffer、CRC、队列和硬件丢包。
5.3 吞吐量、PPS 和网卡错误
1 | sar -n DEV 1 10 |
sar -n DEV 重点看:
rxpck/s、txpck/s:接收和发送 PPS。rxkB/s、txkB/s:接收和发送吞吐量。%ifutil:接口使用率,前提是工具能正确获得链路速率。
sar -n EDEV 用于查看接口错误和丢包。
如果吞吐量不高但 PPS 很高,通常意味着大量小包。此时可能出现:
%soft和ksoftirqdCPU 升高。- 网卡接收队列或协议栈处理不及时。
- SYN Flood、扫描、短连接风暴或高频 RPC。
5.4 Socket 和 TCP 连接状态
1 | ss -s |
已建立连接中:
Recv-Q:应用尚未读取的数据。Send-Q:已发送但尚未被对端确认的数据。
如果 Recv-Q 持续积压,通常说明应用消费不及时;如果 Send-Q 持续积压,可能是对端处理慢、链路拥塞、丢包或发送缓冲受限。
监听 Socket 中,队列表示等待应用 accept() 的连接情况和队列上限。监听队列溢出时,客户端可能出现连接超时或拒绝。
统计 TCP 状态:
1 | ss -ant | awk 'NR>1 {count[$1]++} END {for (state in count) print state, count[state]}' |
常见状态提示:
- 大量
SYN-RECV:握手未完成,可能是客户端异常、丢包或 SYN Flood。 - 大量
TIME-WAIT:短连接较多,应先检查连接复用和客户端行为。 - 大量
CLOSE-WAIT:本端应用收到对端关闭后,没有及时关闭 Socket。 - 大量
ESTAB:不一定异常,要与连接基线、内存和文件描述符一起判断。
高并发连接还要检查文件描述符和客户端临时端口:
1 | cat /proc/<PID>/limits | grep 'open files' |
- 文件描述符耗尽时,应用可能报
Too many open files,无法继续接受连接。 - 客户端大量创建到同一目标的短连接时,可能耗尽本地临时端口。
- 解决方向通常是连接复用、及时关闭 Socket、合理设置连接池和资源上限,而不是只扩大内核参数。
5.5 TCP 重传和协议栈错误
1 | sar -n TCP,ETCP 1 10 |
重点关注:
- TCP 主动和被动建连。
- 连接失败和 Reset。
- 重传分段。
- Listen 队列溢出。
- IP、TCP、UDP 丢包和错误。
这些指标很多是累计值,不能只看绝对数字。应观察故障期间的增长速度,并与请求量比较。
TCP 重传升高常见原因:
- 网络链路丢包或拥塞。
- 对端处理慢,ACK 返回不及时。
- 主机软中断或接收队列处理不过来。
- MTU 或路径 MTU 问题。
- 防火墙、负载均衡和中间设备异常。
5.6 连通性、路由和延迟
1 | ping -c 5 <IP> |
ping:测试 ICMP 连通性和 RTT。traceroute:查看路径中的路由跳点。mtr:连续观察各跳延迟和丢包。
需要注意:
- 对端或中间设备可能限制 ICMP,ping 不通不一定代表 TCP 服务不可用。
- 某一中间跳不回复 ICMP,不等于该跳真的丢弃业务流量。
- ping 正常只说明 ICMP 基本正常,不代表 DNS、TCP、TLS 和应用响应正常。
测试端口:
1 | nc -vz <主机> <端口> |
5.7 不要漏掉 DNS
“使用 IP 很快,使用域名很慢”时,应优先检查 DNS:
1 | dig <域名> |
重点看:
- DNS 服务器响应时间。
- 是否存在超时和重试。
/etc/resolv.conf中的服务器与搜索域。- IPv4、IPv6 查询和 fallback 行为。
- 应用是否每次请求都重新解析,缺少缓存。
许多网络工具默认会反向解析 IP 和端口名,导致命令本身变慢。排查时通常使用 -n 或 -nn 禁止名称解析。
5.8 把应用延迟拆开
HTTP 请求慢时,可以拆分 DNS、TCP、TLS、首字节和总耗时:
1 | curl -o /dev/null -sS \ |
判断思路:
time_namelookup高:DNS。time_connect高:TCP 建连、链路丢包或服务端监听队列。time_appconnect高:TLS 握手。time_starttransfer高:服务端处理或上游依赖。time_total高但前几项正常:响应体传输慢或吞吐不足。
这一步能避免把应用处理慢误判为网络慢。
5.9 抓包:确认网络中真实发生了什么
当统计指标只能告诉你“有问题”,但无法解释原因时,可以使用 tcpdump。
1 | # 指定网卡、主机和端口,不做名称解析 |
抓包时应尽量限制:
- 网卡。
- 主机和端口。
- 报文数量或采样时间。
- 保存文件大小。
通过抓包可以确认:
- 三次握手在哪一步超时。
- 是否存在重复 ACK、重传、乱序或 Reset。
- DNS 查询是否超时。
- 请求已经到达服务端,但应用迟迟没有响应。
- MTU、分片或中间设备是否异常。
高 PPS 生产环境不要无过滤地长时间抓包,否则抓包本身就可能增加 CPU、磁盘和丢包压力。
5.10 软中断与网络小包
网络包接收后,大部分协议栈工作在软中断中完成。出现以下组合时,应重点检查网络小包:
top或mpstat中%si高。ksoftirqd/<CPU>CPU 高。/proc/softirqs中NET_RX快速增长。sar -n DEV显示 PPS 高,但吞吐量并不高。
1 | cat /proc/softirqs |
还应检查中断是否集中在少数 CPU,以及网卡多队列、RSS/RPS 等配置是否与 CPU 分布匹配。参数调整应建立在确认瓶颈之后,不应一看到软中断高就直接修改内核配置。
5.11 网络快速排查路径
服务完全不可达
1 | ip addr/link 确认网卡和地址 |
请求延迟高
1 | curl 拆分 DNS/TCP/TLS/TTFB |
吞吐量上不去
需要同时检查:
- 网卡链路速率和实际吞吐。
- TCP 重传、窗口和 RTT。
- 单连接还是多连接测试。
- CPU、软中断和内存是否成为瓶颈。
- 对端、负载均衡和中间链路限制。
- 应用自身是否生成数据不够快。
基准测试可以使用 iperf3,但测试结果只代表对应协议和测试模型,不能直接代替真实业务性能。
丢包或重传高
1 | ip -s link / ethtool -S 定位网卡和驱动层 |
连接数高或队列溢出
检查:
- 各 TCP 状态数量。
- 监听队列和应用
accept()速度。 - 文件描述符上限。
- 短连接是否缺少连接池和 Keep-Alive。
- 应用事件模型、线程数和单连接内存。
5.12 网络常见误区
- ping 正常就代表网络正常:错误,它只覆盖 ICMP 和基本 RTT。
- 吞吐量低就是带宽不足:错误,也可能是丢包、窗口、CPU 或应用限制。
- TIME_WAIT 多就应该立即调内核参数:错误,应先检查短连接和连接复用。
- Recv-Q 高就是对端慢:通常相反,它更可能表示本端应用读取不及时。
- Send-Q 高就是本端发送太快:还可能是对端处理慢、链路丢包或拥塞。
- 抓包能看到重传就一定是发送端网络故障:重传可能由链路、接收端或中间设备引起。
- 一上来修改 TCP 缓冲区和 backlog:错误,应先确认真正的瓶颈和队列位置。
六、典型案例:从现象一步步走到根因
下面的案例重点不在具体数值,而在排查顺序。示例中的输出经过简化,实际环境应与正常基线、机器配置和业务负载对比。
案例 1:接口延迟升高,单个进程 CPU 接近 100%
现象
某个接口发布后,P99 延迟从几十毫秒升到数百毫秒。机器没有明显 I/O 和内存压力,但应用进程 CPU 持续升高。
第一步:确认 CPU 时间花在哪里
1 | top |
观察到:
%us很高,%sy、%wa不高。- 某个应用进程 CPU 使用率最高。
- 只有一个 CPU 核心接近 100%,其他核心仍有空闲。
这说明问题主要发生在应用用户态,并且很可能只有一个线程成为热点。
第二步:定位热点线程
1 | pidstat -u -t -p <PID> 1 5 |
假设结果显示某个 TID 长时间占用一个 CPU。可以把十进制线程 ID 转为十六进制,便于与部分调试工具中的线程编号对应:
1 | printf '%x\n' <TID> |
第三步:定位热点函数
1 | perf record -F 99 -g -p <PID> -- sleep 15 |
调用栈显示,大部分 CPU 时间都消耗在某个解析、压缩、正则匹配或数据转换函数中。
根因
新版本在请求路径中重复执行了高成本计算,例如:
- 对同一份数据反复解析或序列化。
- 在循环中重复编译正则表达式。
- 本可以复用的计算结果没有缓存。
- 单线程任务无法利用多核。
修复与验证
- 移除重复计算,把不变结果移出循环。
- 复用解析结果或已经编译的对象。
- 优化算法复杂度,而不是只增加机器。
- 如果任务可以安全并行,再考虑拆分工作。
修复后应同时验证:
1 | 接口 P99 延迟下降 |
仅看到 CPU 使用率下降还不够。如果吞吐也下降了,可能只是少处理了请求。
案例 2:Load 很高,但 CPU 使用率并不高
现象
系统 Load Average 持续高于 CPU 数,但 top 中 %us + %sy 不高,应用请求却明显变慢。
第一步:区分运行队列和不可中断任务
1 | vmstat 1 10 |
示意:
1 | r b us sy id wa |
r不高,说明等待 CPU 的任务不多。b和%wa很高,说明大量任务正在等待 I/O。
继续查看 D 状态进程:
1 | ps -eo pid,ppid,stat,wchan:32,comm | awk '$3 ~ /^D/' |
第二步:确认底层设备是否积压
1 | iostat -xz 1 10 |
重点发现:
r_await或w_await明显升高。aqu-sz持续积压。- 某块设备的请求量突然增加。
第三步:找出 I/O 来源
1 | pidstat -d 1 10 |
结果发现备份、日志归档或批处理进程正在进行大量随机读写,与在线服务共用同一块磁盘。
根因
Load 升高并不是因为 CPU 不够,而是大量任务进入 D 状态。后台任务耗尽磁盘处理能力,在线请求也被迫等待。
修复与验证
- 将批处理安排到低峰期。
- 将在线数据、日志和备份拆分到不同设备。
- 对后台任务设置合理的 I/O 优先级和速率限制。
- 优化随机 I/O,合并小请求。
验证时观察:
b和%wa是否下降。- 磁盘
await、队列是否恢复。 - Load 是否随
D状态任务减少而回落。 - 在线请求延迟是否恢复。
案例 3:进程 RSS 持续增长,最终触发 OOM
现象
服务刚启动时占用 500 MB,运行数小时后增长到数 GB,重启后又恢复。随着运行时间增加,系统开始 Swap,最后进程被 OOM Killer 终止。
第一步:确认是持续增长,而不是单次高峰
1 | free -h |
观察到:
available持续下降。- 目标进程 RSS 持续增长。
- 后期出现
si/so和主缺页。
第二步:判断增长的内存类型
1 | pmap -x <PID> |
如果 RssAnon、Private_Dirty 持续增长,通常说明增长主要来自堆或匿名映射,而不是文件缓存。
第三步:定位分配调用栈
环境支持 BCC 时:
1 | /usr/share/bcc/tools/memleak -p <PID> |
C/C++ 测试环境中也可以重新构建并运行:
1 | g++ -fsanitize=address,leak -fno-omit-frame-pointer -g app.cpp -o app |
最终发现某个错误处理分支分配内存后提前返回,没有执行释放;或者某个缓存、队列只增加不淘汰。
根因
常见根因有两类:
- 所有权错误:
malloc/new后遗漏free/delete,异常路径没有释放。 - 逻辑上的“泄漏”:对象仍然可达,但缓存、Map、队列没有容量和过期策略。
第二类不会被所有泄漏检测器识别,但同样会耗尽内存。
修复与验证
C++ 中优先使用 RAII:
- 使用
std::vector、std::string等容器管理内存。 - 使用
std::unique_ptr表达唯一所有权。 - 避免裸 owning pointer。
- 给缓存和队列设置容量、TTL 和淘汰策略。
验证时应在稳定负载下运行足够长时间,观察:
- RSS 是否进入稳定区间。
available是否不再持续下降。memleak或 Sanitizer 是否不再报告增长调用栈。- 是否不再发生 Swap 和 OOM。
案例 4:df 显示磁盘已满,du 却找不到大文件
现象
1 | df -h /var |
df 显示 /var 已使用 95%,但 du 汇总的可见文件远小于已用空间。
第一步:排除 inode 和挂载点问题
1 | df -i /var |
inode 没有耗尽,统计的也是同一个文件系统。
第二步:查找已删除但仍被占用的文件
1 | lsof +L1 |
示意:
1 | COMMAND PID FD TYPE SIZE/OFF NLINK NAME |
日志文件虽然已经从目录中删除,但应用仍持有打开的文件描述符,所以磁盘块没有释放。
根因
日志轮转或人工清理直接删除了正在写入的日志,应用没有重新打开文件。du 只能统计目录树中可见的文件,而 df 统计文件系统真实占用。
修复与验证
- 优先让应用重新打开日志文件,例如发送应用支持的 reopen 信号。
- 如果应用不支持重新打开日志,再安排安全重启。
- 为
logrotate配置正确的postrotate行为。 - 不要默认使用
copytruncate;复制与截断之间可能丢失少量日志。
处理后重新检查:
1 | lsof +L1 |
不要直接操作
/proc/<PID>/fd/<FD>截断生产文件,除非已经确认文件类型、应用行为和数据风险。
案例 5:应用吞吐不高,但磁盘写延迟很高
现象
业务请求量不大,CPU 和内存也有余量,但每个请求偶尔会卡住几十到几百毫秒。iostat 显示写延迟明显升高。
第一步:确认写延迟和队列
1 | iostat -xz 1 10 |
观察到:
w_await明显高于正常基线。aqu-sz周期性升高。- 写吞吐量并不大。
吞吐量不高但延迟高,说明问题不一定是“写得太多”,也可能是每次写都要求同步持久化。
第二步:定位写入进程
1 | pidstat -d 1 10 |
找到应用进程后,短时间查看写相关系统调用:
1 | strace -tt -T -e trace=write,fsync,fdatasync -p <PID> |
结果发现应用每处理一个请求,就执行一次:
1 | write(...) |
而单次 fsync() 经常耗时数十毫秒。
根因
应用把每条日志、事件或业务记录都单独同步写入磁盘。请求路径必须等待数据真正持久化,因此吞吐量不高也会被单次同步写延迟限制。
修复与验证
在不破坏数据一致性要求的前提下:
- 合并多个写请求,再批量执行
fsync()。 - 把非关键日志改为异步写入,并控制队列上限。
- 降低无用的 debug 日志量。
- 将日志与数据库数据拆分到不同设备。
- 对必须同步持久化的数据,评估更低延迟的存储。
修复后不仅要看平均延迟,还要比较:
- 请求 P95/P99。
fsync()延迟分布。- 磁盘
w_await和队列。 - 异步队列是否会在故障时丢数据或无限增长。
案例 6:使用 IP 访问正常,使用域名却时快时慢
现象
直接访问服务 IP 很快,但使用域名时偶尔需要几秒,应用日志中还出现连接超时。
第一步:拆分请求耗时
1 | curl -o /dev/null -sS \ |
结果显示 time_namelookup 偶尔达到数秒,而 TCP 建连和服务端首字节时间正常。
第二步:直接测试 DNS
1 | dig example.com |
多次测试发现,第一个 DNS 服务器不可达或响应很慢,等待超时后才回退到第二个 DNS 服务器。
继续确认网络路径:
1 | dig @<DNS服务器IP> example.com |
根因
主 DNS 配置错误、网络不通或服务不稳定。每次命中该服务器时,都要等待超时和重试,所以表现为“时快时慢”。
修复与验证
- 修复 DNS 服务器或网络路径。
- 移除无效的 DNS 地址。
- 为应用使用合理的 DNS 缓存和过期策略。
- 检查搜索域是否导致额外查询。
- 不要长期通过写死
/etc/hosts掩盖动态服务发现问题。
验证时同时检查:
dig响应时间和失败率。curl的 DNS 阶段耗时。- 应用解析超时和请求错误率。
案例 7:高并发时连接超时,但 CPU 和带宽都没满
现象
低流量下服务正常,高并发时客户端出现连接超时。服务器 CPU、内存和网卡带宽仍有余量。
第一步:检查监听队列
1 | ss -lntp 'sport = :8080' |
示意:
1 | State Recv-Q Send-Q Local Address:Port |
监听 Socket 的 Recv-Q 已接近队列上限,说明已经完成握手、等待应用 accept() 的连接正在积压。
第二步:检查队列溢出计数
1 | nstat -az | grep -E 'ListenOverflows|ListenDrops' |
连续观察发现 TcpExtListenOverflows、TcpExtListenDrops 持续增长。
第三步:继续追问应用为什么没有及时 accept
1 | pidstat -u -w -t -p <PID> 1 5 |
结果可能发现:
- 工作线程全部阻塞在数据库或下游 RPC。
- 单线程事件循环执行了耗时计算。
- 进程频繁发生长时间 GC 或锁等待。
- 应用的连接处理模型无法承受当前并发。
根因
连接超时看似是网络问题,实际根因是应用没有及时接受和处理新连接。监听队列只是最先暴露出积压的位置。
修复与验证
- 先解决阻塞应用线程的数据库、锁、计算或下游依赖问题。
- 为下游调用设置超时、并发限制和失败策略。
- 优化事件模型或合理增加工作线程。
- 确认应用处理能力后,再评估 backlog 上限。
只扩大 backlog 通常只能延后失败,并不能提高应用实际处理能力。
验证时观察:
ListenOverflows、ListenDrops是否停止增长。- 监听
Recv-Q是否快速回落。 - TCP 建连耗时和客户端超时率是否下降。
- 应用吞吐是否真正提高。
案例复盘模板
每次真实故障处理后,可以按下面的模板记录:
1 | 1. 业务现象: |
案例最有价值的部分,不是最后执行了哪条命令,而是为什么根据当前证据选择了下一步。
七、把 CPU、内存、I/O 和网络关联起来
性能问题常常跨越多个资源:
| 现象组合 | 可能方向 |
|---|---|
Load 高、CPU 不高、b 高 |
磁盘、NFS、Swap 或其他 D 状态等待 |
%sy 高、缺页高 |
频繁内存分配、回收或映射 |
%wa 高、si 高 |
Swap 换入导致磁盘等待 |
| RSS 持续增长、磁盘写入增加 | 无界缓存、持久化队列或日志积压 |
%si 高、PPS 高、吞吐不高 |
网络小包或异常流量 |
| Send-Q 高、TCP 重传高 | 对端处理慢、拥塞或链路丢包 |
| OOM 前整机卡顿 | 内存回收、Swap 和 I/O 抖动 |
| 磁盘很忙但进程 I/O 不高 | Page Cache 写回、Swap、内核线程 |
| 容器延迟高但宿主机资源空闲 | cgroup CPU throttling 或内存限制 |
所以,排查时不要把资源割裂。一个指标异常时,应继续问:它是根因,还是另一个瓶颈造成的结果?
八、现场排查最小命令集
下面这组命令不是要每次全部执行,而是用于快速保存现场。根据现象选择对应部分即可。
CPU
1 | uptime |
内存
1 | free -h |
文件系统与磁盘
1 | df -h |
网络
1 | ip -br addr |
定位到进程后
1 | pidstat -u -r -d -w -p <PID> 1 5 |
然后再根据证据选择 perf、strace、tcpdump、eBPF、ASan 或 Valgrind。不要无目的地把所有工具跑一遍。
九、最后总结:比命令更重要的是证据链
一次完整的性能分析,应该能讲清楚下面这条证据链:
1 | 业务为什么慢 |
真正值得沉淀的不是“某次用了哪些命令”,而是:
- 当时观察到了什么现象。
- 哪些指标互相印证。
- 哪些假设被排除。
- 根因位于哪一层。
- 优化前后的延迟、吞吐、错误率和资源消耗发生了什么变化。
当这条链路足够完整时,性能优化就不再是靠经验猜问题,而会变成一个可以重复、验证和复盘的工程过程。