2. 常见瓶颈类型:CPU、内存、IO、网络

做性能压测这些年,我最常被问的一句话就是:“系统到底慢在哪?” 很多人一上来就怀疑代码写得烂,其实大部分时候,根子在资源层面。CPU、内存、IO、网络——这四大件,说白了就是系统的四条腿。哪条腿瘸了,吞吐量就上不去。

我个人习惯,拿到一个压测结果,先不看火焰图,而是看一眼 系统资源使用率。哪个资源先到 100%,大概率就是它。下面我把这四类瓶颈拆开讲,每个都配上我在项目中踩过的坑。

核心原则:瓶颈定位不是猜,而是用数据说话。CPU 跑满未必是 CPU 问题,可能是 IO 等待导致上下文切换飙升。记住这一点,后面很多坑就能避开。

2.1 CPU 瓶颈

CPU 是系统的算力中心。它吃紧的时候,用户请求排队,延迟飙升。但 CPU 满不一定是坏事情——如果业务逻辑本身就需要大量计算,那满负荷反而是正常状态。关键在于:是用户态用满了,还是内核态用满了?

我在项目中遇到过这样一个案例:一个 Redis 集群突然性能下降,监控显示 CPU 打满。我一开始以为是慢查询,结果发现是网卡中断不均匀,所有中断都打在一个 CPU 核上。用 irqbalance 重新分配后,问题解决。嗯,有时瓶颈不在业务代码,而在系统调度。

怎么识别 CPU 瓶颈?

  • 用户态高(%user > 70%):通常是业务代码本身的算力消耗大,比如频繁的正则、加解密、序列化。
  • 内核态高(%sys > 30%):系统调用频繁、锁竞争、内存分配回收、中断处理。
  • 上下文切换暴增(cs > 10 万/秒):线程太多,或者锁粒度太细。
  • 运行队列过长(load average > 2 倍 CPU 核数):CPU 无法消化任务。

我常用的排查命令:

# 查看每个核的使用率
mpstat -P ALL 1
# 查看上下文切换、运行队列
vmstat 1
# 追踪进程热调用
perf top -p <pid>

调优方向

  • 减少不必要的计算:缓存结果、使用更轻量的算法。
  • 降低锁竞争:读写分离、无锁数据结构、缩小临界区。
  • 调整线程池大小:不是线程越多越快,I/O 密集型可以多,CPU 密集型建议不超过核数。
  • 绑定 CPU 亲和性:某些场景下把特定线程固定到指定核。

2.2 内存瓶颈

内存的坑往往比 CPU 更隐蔽。CPU 跑满你一眼就能看到,内存泄漏却可能潜伏几天。我记得有一次做压测,前 10 分钟吞吐量很好看,20 分钟后突然断崖式下跌——Full GC 来了。你想想看,内存不够,JVM 疯狂 GC,CPU 也跟着遭殃。

怎么识别内存瓶颈?

  • 物理内存使用率持续 > 90%,并且不断发生 swap(检查 si/so 字段)。一旦开始 swap,系统基本就废了。
  • OOM Killer 频繁出现:进程被强制杀掉。
  • 垃圾回收频率异常:对于 Java 应用,检查 jstat -gcutil,如果 Young GC 每秒超过 5 次,或者 Full GC 几分钟一次,就要警惕。
  • 页错误(major fault):进程需要从磁盘换入内存页,延迟暴增。

我曾经犯过的错: 压测环境内存故意配小,想着节省成本,结果压出来的数据一顿一卡。后来才知道,压测环境的内存应尽量模拟生产,否则定位到的瓶颈全是 swap,根本看不出业务本身的性能。

调优方向

  • 堆内存与元空间分开调:别一股脑给 Xmx 设很大,要根据对象存活周期来。
  • 使用堆外内存(Direct Buffer)时务必控制上限,否则容易触发 Native 内存泄漏。
  • 减少大对象分配:比如一次性读入整个文件,改成流式处理。
  • 调整 swap 策略:vm.swappiness 设置到 10 以下,避免系统过早使用 swap。

2.3 IO 瓶颈

IO 是个大筐,磁盘读写、数据库查询、文件系统操作都算。我个人最头疼的就是磁盘 IO——它不像 CPU 那样可以弹性伸缩,磁盘满了就是满了,服务就卡住了。

记得有一次线上 Kafka 集群写入延迟极高,看了磁盘指标:await 达到 200ms,%util 接近 100%。我一开始以为磁盘坏了,排查后发现是同一个磁盘上既有日志写入,又有数据盘操作,IO 混杂导致寻道开销暴增。解决方案很简单:把日志单独放一块盘上。

怎么识别 IO 瓶颈?

  • %util 接近 100%:磁盘忙不过来了,但注意这是服务时间占比,不是利用率。SSD 下 %util 到 100% 不一定代表饱和,要看 await。
  • await > 50ms:对于 SSD 来说,这个值太高了。可能是队列太长,或者是磁盘故障。
  • iowait 高(%iowait > 30%):CPU 在等待 IO 完成,空转。
  • 请求队列长度(avgqu-sz)> 磁盘能并行处理的请求数:请求在排队。

我常用的排查命令:

# 查看每个磁盘的 IO 数据
iostat -x 1
# 定位进程的 IO 行为
iotop
# 文件系统级别的延迟
strace -e trace=file -p <pid>

调优方向

  • 使用异步 IO(如 io_uring、AIO)替代同步阻塞。
  • 合并小的随机写,变成顺序写(比如使用 LSM-Tree 结构的数据库)。
  • 正确配置块设备调度器:NVMe 用 none,SATA SSD 用 mq-deadline,机械盘用 bfq。
  • 增加缓存层:用 Redis 或本地缓存挡住热点数据,减少磁盘穿透。

2.4 网络瓶颈

网络瓶颈最容易被忽视,因为大多数压测都在同机房内网进行,延迟低、带宽足。但一旦跨机房、跨公网,或者并发连接数过高,问题就出来了。

我遇到过最离谱的一次:压测结果不稳定,时好时坏。查了半天,发现是网卡队列溢出——RX 丢包率高达 0.5%。原来网卡只有一个队列,中断全部打在一个核上,核忙不过来。调整 ethtool -L 增加队列后,问题消失。

怎么识别网络瓶颈?

  • 网卡饱和:RX/TX 带宽接近网卡上限(比如千兆网口跑满 100MB/s),此时丢包率上升。
  • 连接数暴增:`ss -s` 看到 TIME_WAIT 连接数上万,端口资源耗尽。
  • TCP 重传率 > 2%:可能是丢包、拥塞、缓冲区不足。
  • 延迟抖动:使用 ping -f 或 mtr 看中间节点是否丢包。

避坑提示: 很多人发现网络慢就加带宽,但带宽不是唯一因素。TCP 窗口、拥塞控制算法、Nagle 算法、NIC 缓冲区大小,每个都可能成为瓶颈。我曾经通过调大 net.core.rmem_max 把吞吐量提高了 30%,一分钱没花。

调优方向

  • 调整内核网络参数:net.ipv4.tcp_tw_reuse、net.core.somaxconn、net.ipv4.tcp_rmem 等。
  • 使用多队列网卡 + RSS(Receive Side Scaling),把中断分散到多核。
  • 考虑使用 DPDK 或 XDP 绕过内核协议栈(极端场景)。
  • 应用层做连接复用(HTTP/2、gRPC 长连接),减少三次握手开销。

瓶颈类型对照表

瓶颈类型 典型现象 关键指标 核心排查工具
CPU 负载高、请求排队、延迟锯齿 %user、%sys、上下文切换数 mpstat, vmstat, perf top
内存 频繁 GC、OOM、swap 活动 内存使用率、swap si/so、GC 频率 free -m, jstat, sar -r
IO 磁盘繁忙、请求超时、iowait 高 %util、await、avgqu-sz iostat -x, iotop, strace
网络 丢包、重传、连接堆积、带宽打满 带宽使用率、TCP 重传率、连接数 iftop, ss, tcpdump, ethtool

小结:四类瓶颈的关系图

下面这张图展示了四种瓶颈之间的关联。注意,它们不是孤立的——CPU 瓶颈可能导致网络堆积,内存不足会加剧 IO 压力。压测时,我的习惯是盯住最先到达 90% 的那个资源。

常见瓶颈类型及关联 瓶颈根因 CPU 内存 IO 网络 用户态/内核态 上下文切换 GC / swap 堆外内存 磁盘 await iowait TCP 重传 丢包 依赖与联动关系 CPU 瓶颈 → 网络请求排队 → 连接超时or重传 内存不足 → 频繁 swap → IO 压力增大 → iowait 升高 → CPU 空转 IO 瓶颈 → 业务线程阻塞 → 上下文切换飙升 → CPU 内核态满载

快速定位口诀: CPU 看负载,内存看 GC,IO 看 await,网络看重传。先抓最明显的,再追连锁反应。