服务器负载过高的系统性排查方法

描述

问题背景

2025 年 6 月,某电商平台在年中促销活动期间,线上 Web 服务器(配置 24 核 CPU / 32GB 内存)于 14:30 左右触发 CPU 负载告警。监控数据显示 load average 从正常值 5~8 飙升至 42+,同时业务监控显示接口 P99 响应时间从 200ms 升高到 3800ms,用户侧开始出现超时和 502 错误。

本文以这次故障的完整排查过程为线索,展示服务器负载过高的系统性排查方法。文章以第一人称叙事展开,每步都给出实际命令输出和决策思路。

一、接到告警:第一反应

收到告警后,登录服务器执行第一个命令:

 

$ uptime
 1445 up 15 days,  6:12,  3 users,  load average: 42.35, 28.17, 15.42

 

load average 的三个值分别是 1 分钟 / 5 分钟 / 15 分钟的平均负载。42.35 对于 24 核的服务器意味着平均每个 CPU 核上有 1.76 个任务在竞争——CPU 已经饱和。

但负载高不等于 CPU 使用率高,需要进一步区分瓶颈类型。

二、全局扫描:定性瓶颈类型

2.1 top —— 看 CPU 时间分布

 

$ top
top - 1450 up 15 days,  6:12,  3 users,  load average: 42.35, 28.17, 15.42
Tasks: 325 total,   4 running, 321 sleeping,   0 stopped,   0 zombie
%Cpu(s):  8.5 us,  5.2 sy,  0.0 ni, 32.1 id, 53.8 wa,  0.0 hi,  0.4 si,  0.0 st
MiB Mem :  31967.6 total,   4284.5 free,  14234.2 used,  13448.9 buff/cache
MiB Swap:   4096.0 total,    945.2 free,   3150.8 used.  13233.4 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 5678 www       20   0  0.356t 0.042t  32768 S   2.2   0.6  12:34.56 java
 2345 root      20   0  175240   5300   4212 S   1.8   0.1   2:15.30 nginx

 

最关键的发现是 **wa(I/O 等待)= 53.8%**,说明大量 CPU 时间在等待 I/O 完成。结合 id(空闲)= 32.1%,说明 CPU 并没有满载(us + sy = 13.7%),但系统负载高的原因是 I/O 阻塞导致进程排队。

这是典型的"负载高因为 I/O 瓶颈"场景——CPU 有空闲时间,但进程在等 I/O 完成,所以 load average 中的"等待进程"数量很高。

2.2 vmstat —— 确认阻塞情况

 

$ 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
 3 38 204800 42845    ...

 

b 列(不可中断睡眠进程数)为 38,说明有 38 个进程阻塞在 I/O 上。r 列(运行队列)为 3,说明 CPU 调度并没有明显积压。

判断:系统瓶颈在 I/O 子系统,不是 CPU。

2.3 free —— 检查内存和 swap

 

$ free -h -w
              total        used        available     buffers      cache
Mem:           31G          14G           13G          2.2G         11G
Swap:         4.0G         3.0G          1.0G

 

available 内存 13G,不是内存瓶颈。但 swap 使用了 3GB,说明存在一定程度的内存压力,进程被换出到磁盘,可能加重 I/O 负担。

2.4 第一阶段小结

全局扫描结论:

瓶颈类型:I/O 瓶颈(wa=53.8%,b=38)

资源情况:CPU 有余量(id=32.1%),内存充足,swap 有少量使用

方向:下一步定位哪个进程在产生大量 I/O

三、定位 I/O 来源

3.1 iostat —— 看磁盘到底有多忙

 

$ iostat -xdm 1 3
Linux 5.15.0-91-generic ... 06/15/2025  _x86_64_ (24 CPU)

Device      r/s     w/s     rkB/s     wkB/s  r_await  w_await  aqu-sz  %util
vda      120.5  4500.3   3840.0  360000.0     2.1   112.3   248.5   98.7

 

关键指标解读:

w/s = 4500:每秒 4500 次写请求,非常高

w_await = 112.3ms:每次写请求平均等待 112ms,远高于 SSD 的正常范围(<2ms)

aqu-sz = 248.5:平均队列长度 248,说明 IO 请求大量积压

**%util = 98.7%**:磁盘接近饱和

这是一块通用型 SSD 云盘(极速型 SSD 的 IOPS 上限一般在 2 万左右,但 4500 w/s 不至于打到上限)。112ms 的写延迟说明单次写入遇到了抖动——可能是磁盘层的资源争抢或日志落盘的大量小 IO。

 

# 查看磁盘队列深度(内核侧)
$ cat /sys/block/vda/queue/nr_requests
256

# 查看调度器
$ cat /sys/block/vda/queue/scheduler
[mq-deadline] none

 

3.2 iotop —— 找到 I/O 最多的进程

 

$ iotop -o
Total DISK READ: 3.84 M/s | Total DISK WRITE: 360.00 M/s
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN      IO>    COMMAND
 3456 be/4  root       0.00 B/s  320.00 M/s  0.00 %  95.20 %  java
 5678 be/4  www        3.84 M/s    0.00 B/s  0.00 %   2.10 %  nginx

 

Java 进程以 320MB/s 的速度在写入磁盘,几乎占了全部 IO。IO> 列显示该进程 95.2% 的时间在等待 I/O。

3.3 pidstat -d —— 按进程确认

 

$ pidstat -d 1 3
Linux ... 06/15/2025  _x86_64_ (24 CPU)

1422      PID   kB_rd/s   kB_wr/s  kB_ccwr/s  Command
1423     3456      0.00  320000.00    0.00      java

 

3.4 确认文件级写入

用 lsof 看 Java 进程打开了哪些文件在写:

 

$ ls -la /proc/3456/fd/ | grep -E 'REG.*W' | sort -k7 -rn | head -5
lrwx------ 1 root root 64 Jun 15 14:33 23 -> /var/log/app/app.log
lrwx------ 1 root root 64 Jun 15 14:33 24 -> /var/log/app/app.log.1
lrwx------ 1 root root 64 Jun 15 14:33 25 -> /var/log/app/error.log

 

关键发现:进程打开了 app.log.1(轮转后的日志文件)—— 这意味着日志轮转后旧文件被重命名,但 Java 进程仍在往旧文件中写入。

 

# 检查这些 fd 是否已被删除
$ ls -la /proc/3456/fd/23
... /var/log/app/app.log (deleted)
$ ls -la /proc/3456/fd/24
... /var/log/app/app.log.1

 

确认第 23 号 fd 对应的文件已被删除(logrotate create 方式导致),而第 24 号 fd 对应的 app.log.1 文件仍在写入。

四、根因确认

4.1 排查发现

完整排查后,根因链路如下:

应用日志配置为 DEBUG 级别:促销活动期间开发团队在线开启了 debug 日志,日志写入量从平时的 20MB/min 暴增到 20GB/min。

logrotate 使用 create 而非 copytruncate:默认的 logrotate 配置是 create,轮转时执行 mv app.log app.log.1 + create new app.log。Java 进程持有的旧文件句柄仍然指向 app.log.1(即被重命名后的文件),所以日志继续向 app.log.1 写入。

写放大效应:日志写入量激增 + 双文件写入(app.log 和 app.log.1)→ 磁盘写 IO 打满 → 所有需要磁盘 I/O 的进程排队(包括数据库持久化、session 持久化)→ load average 飙升 → 业务响应变慢。

 

# 确认日志文件大小
$ ls -lh /var/log/app/
-rw-r--r-- 1 root root 5.2G Jun 15 14:34 app.log
-rw-r--r-- 1 root root 4.8G Jun 15 14:34 app.log.1

# 确认 logrotate 配置
$ cat /etc/logrotate.d/app
/var/log/app/*.log {
    daily
    rotate 7
    create      # <- 问题在这里,应该用 copytruncate
    compress
    delaycompress
    missingok
    notifempty
}

 

4.2 立即恢复操作

第一步:让 Java 进程释放旧文件句柄,释放 deleted 文件的磁盘空间

 

# 查找 Java 进程 PID
$ ps aux | grep java
# 发送 USR1 信号让 log4j2 重新加载配置(大多数日志框架支持)
$ kill -USR1 

# 验证空间是否释放
$ df -h /

 

如果 kill -USR1 无效,可以用 lsof 确认哪个 fd 是 deleted 文件,然后清空:

 

# 找到 deleted 文件的 fd 编号
$ sudo lsof -p  | grep '(deleted)'
$ : > /proc//fd/

 

第二步:关闭 DEBUG 日志

修改 log4j2.xml 或 logback.xml,将日志级别恢复为 WARN 或 INFO,然后重新加载配置。

第三步:确认 IO 恢复正常

 

$ iostat -xdm 1 3
$ uptime

 

15 分钟后负载恢复到正常水平。

4.3 根本解决方案

修改 logrotate 配置,使用 copytruncate:

 

/var/log/app/*.log {
    daily
    rotate 30
    copytruncate
    compress
    delaycompress
    missingok
    notifempty
}

 

配置异步日志写入(log4j2 AsyncAppender / logback AsyncAppender),避免日志 I/O 阻塞业务线程。

将 /var/log 独立分区,避免日志写满根分区。

日志级别变更流程化:生产环境日志级别变更需审批,变更完成后自动恢复。

五、附加生产故障案例

上述案例是 I/O 阻塞型负载过高的典型场景。以下是另外三种在线上环境中反复出现的负载过高案例,供对比参考。

案例 A:数据库双 1 配置拖死磁盘

场景:某 MySQL 实例在业务高峰时 load 飙高到 60+,wa 持续在 70~80%。

 

$ iostat -xdm 1
sda   w/s=12000  w_await=92ms  %util=99.5%

 

根因:innodb_flush_log_at_trx_commit=1(每次事务提交都刷盘)+ sync_binlog=1(每次提交同步 binlog),在高并发事务下,磁盘 fsync 成为瓶颈。

 

-- 临时降低持久化级别(风险:丢失 1 秒内的事务)
SET GLOBAL innodb_flush_log_at_trx_commit=2;
SET GLOBAL sync_binlog=1000;

 

长期方案:

升级更高 IOPS 的云盘(如 ESSD PL3)

开启组提交(group commit),MySQL 5.7+ 默认支持

考虑 semi-sync 复制代替强同步

案例 B:TIME_WAIT 堆积导致连接失败

场景:高并发短连接服务,load 上升到 9.2(16 核),业务开始报 Connection refused。

 

$ ss -s
Total: 65200 (kernel 65321)
TCP:   62134 (estab 12, closed 62000, orphaned 8, synrecv 0, timewait 62000)

$ netstat -s | grep "listen"
    18432 times the listen queue of a socket overflowed

 

根因:短连接场景下 TIME_WAIT 堆积到 62000,占满了系统连接表,新的连接无法建立。

 

# 立即缓解
$ sysctl -w net.ipv4.tcp_tw_reuse=1
$ sysctl -w net.ipv4.tcp_fin_timeout=15

 

长期方案:客户端改用长连接池或连接复用。

案例 C:apt-check 后台进程 IO 打满(小内存服务器)

场景:低配云服务器(2 核 4GB)运行中突然 load 飙高到 15+,wa > 90%。

 

$ iotop -o
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN      IO>    COMMAND
 1234 be/4  root      84.00 M/s    0.00 B/s  0.00 %  98.5 %  apt-check

 

根因:Ubuntu 的 unattended-upgrades 自动更新检查服务触发,导致大量磁盘读操作。小内存服务器上磁盘 I/O 更容易成为瓶颈。

 

# 临时停止
$ systemctl stop apt-daily.timer

# 永久关闭(如果不需要自动更新)
$ systemctl disable apt-daily.timer
$ systemctl disable apt-daily-upgrade.timer

 

六、Netflix 60 秒法则在实战中的应用

本次排查过程中使用的正是 Netflix 性能工程团队总结的"60 秒法则"——登录服务器后的前 60 秒内执行一组标准命令,快速建立整体认知。以下是针对负载过高场景的命令序列:

 

# 第 1~5 秒
uptime                                  # load average
dmesg -T | tail -10                     # kernel errors

# 第 6~15 秒
vmstat 1 3                              # procs/memory/io/system/cpu
mpstat -P ALL 1 3                       # per-CPU breakdown

# 第 16~25 秒
pidstat -u 1 3                          # per-process CPU
pidstat -d 1 3                          # per-process IO

# 第 26~35 秒
iostat -xzm 1 3                         # disk IOPS & latency
free -h -w                              # memory pressure

# 第 36~45 秒
sar -n DEV 1 3                          # network throughput
sar -n TCP,ETCP 1 3                     # TCP retransmits, listen drops

# 第 46~60 秒
top -bn1                                # snapshot of top processes
ss -s                                   # connection summary

 

这套命令序列能在 1 分钟内完成系统的全面体检,定位出瓶颈是 CPU、内存、IO 还是网络。

七、生产环境排查注意事项

不要在负载高时盲目重启——重启清除现场数据,根因可能永远丢失。除非系统完全无响应,否则优先排查而非重启。

先采集现场数据再操作:在执行 kill / rm / restart 之前,先把关键指标保存下来:

 

$ mkdir -p /tmp/debug_$(date +%s)
$ uptime > /tmp/debug_*/uptime
$ top -bn1 > /tmp/debug_*/top
$ vmstat 1 5 > /tmp/debug_*/vmstat
$ iostat -xdm 1 5 > /tmp/debug_*/iostat
$ ss -s > /tmp/debug_*/ss

 

判断依据要有数据支撑:不要说"我觉得是 IO 问题",而是"wa=53.8%、b=38、w_await=112ms,所以 IO 是瓶颈"。

灰度执行操作:如果有多个节点,先在一个节点上验证恢复方案,确认有效再推全量。

回滚方案要提前准备:修改配置前备份(cp /etc/logrotate.d/app /etc/logrotate.d/app.$(date +%F)),确保改错了能还原。

变更记录要完整:谁在什么时间执行了什么操作,影响范围是什么,要记录到故障复盘报告中。

八、总结

服务器负载过高的排查本质是找到瓶颈资源的过程。负载高 ≠ CPU 高,负载高可能来自:

I/O 瓶颈(最常见):wa 高、b 列高、iostat 显示磁盘饱和

CPU 瓶颈:us 高、r 列高、各核负载均衡

内存瓶颈:available 低、swap 活跃

网络瓶颈:重传率高、listen drops

排查流程可以固化为一个标准 SOP:

 

告警触发 → uptime 确认 → top 看 CPU 分布 → vmstat 看调度和阻塞 →
iostat/iotop 定位 IO → pidstat 确认进程 → lsof/straace 深挖根因 →
制定恢复方案 → 灰度执行 → 验证效果 → 根因整改

 

本次案例的核心教训是:日志轮转配置不当 + 日志级别失控 是最容易被忽视的生产隐患。建议将 lsof 检查 deleted 文件作为新服务器上线检查清单中的一项,同时规范日志级别变更流程。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分