当前位置:首页>Linux>Linux load高负载排查 & SOP

Linux load高负载排查 & SOP

  • 2026-10-11 06:24:59
Linux load高负载排查 & SOP

线上 IoT 边缘设备出现异常,监控显示 load average 飙到 89,持续数小时未恢复。

设备配置是 8 核 ARM 架构,跑着多个容器化服务。


一、矛盾的现象

load average: 48.45, 64.62, 89.64%Cpu(s):  5.3 us, 16.8 sy,  0.0 ni, 77.6 id,  0.0 waTasks: 243 total,   1 running, 242 sleeping

load 最高 89,但 CPU idle 是 77%,I/O wait 是 0,就 1 个进程在跑。

CPU 没饱和,磁盘也没压力,这么高的负载哪来的?


二、一步步排查

排除 D 状态进程

load average 统计的是 R(运行中)和 D(不可中断睡眠)状态的线程数。CPU 明明空着,会不会是大量进程卡在内核里出不来?

ps aux | awk '$8 ~ /D/ {print $0}'

没输出。当前没有 D 状态进程,可能之前有,随着负载下降已经恢复了。

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 2  0      0 3257096 152380 1814252    0    0     0    16 9197 14344 10 24 67  0  0 2  0      0 3251544 152380 1814268    0    0     0   164 8511 13274  5 14 81  0  0 1  0      0 3248808 152380 1814300    0    0     0     4 9757 14384 14 12 74  0  0 0  0      0 3250724 152380 1814304    0    0     0   160 8286 11653  8 24 67  0  0

抓到几个异常点:

  • b = 0:没有阻塞进程,D 状态确实清了
  • cs = 11k~14k/s:上下文切换很高
  • in = 8.5k~9.7k/s:中断也不低
  • sy = 11~24%:内核态 CPU 占比偏高

内存还有 3GB 多,swap 是 0,I/O 基本没动(bi/bo 都接近 0)。看起来是线程在内核层面频繁切换导致的。

找谁在频繁切换

pidstat -w 1 5 | sort -k6 -rn | head -20
09:39:34 AM     0   1794325      1.00    277.00  pidstat09:39:36 AM     0   1794325      1.00    167.00  pidstat09:39:36 AM  1000   1794618      0.00     28.00  java09:39:34 AM     0      2019      8.00     18.00  checknet.sh09:39:33 AM     0      2019     34.00     12.00  checknet.sh09:39:35 AM  1000   1794580      1.00      7.00  ping09:39:36 AM     0      2019     26.00      6.00  checknet.sh

java 进程上下文切换 28 次/秒,不算特别夸张。倒是 checknet.sh 在反复 fork 子进程。

先看看 java 有多少线程:

ps -eo pid,nlwp,comm --sort=-nlwp | head -10
    PID NLWP COMMAND3969687 7148 java1786510   37 nanomq   2506   21 containerd2894914   21 MediaServer   3117   18 vedgecore1786445   15 containerd-shim

7148 个线程。8 核机器跑 7000 多线程,调度器能不累吗。


三、进容器看 Java 线程

进容器抓个 thread dump:

jstack 99 | grep 'java.lang.Thread.State' | sort | uniq -c | sort -rn
4638    TIMED_WAITING (on object monitor)2321    RUNNABLE 123    WAITING (parking)  43    TIMED_WAITING (parking)   5    WAITING (on object monitor)   2    TIMED_WAITING (sleeping)

4638 个线程在 TIMED_WAITING,2321 个是 RUNNABLE。但 CPU 才用了 20~30%,这些 RUNNABLE 线程其实没在真跑,应该是阻塞在 native 调用里了,Java 这边看着状态还是 RUNNABLE。

看看线程数还在不在涨:

whiletrue; do cat /proc/3969687/status | grep Threads; sleep 2; done
Threads: 7157Threads: 7161Threads: 7161Threads: 7157Threads: 7157Threads: 7157

稳定在 7157~7161,不涨了,说明是之前积累下来的。

关键问题来了:这 7000 多线程都是哪来的?

jstack 99 | grep '"' | awk -F'"''{print $2}' \  | sed 's/-[0-9]*$//' | sort | uniq -c | sort -rn | head -20
1153  SocketListener(22-0-0-1.local.)1153  JmDNS(22-0-0-1.local.).Timer1153  JmDNS(22-0-0-1.local.).State.Timer1152  SocketListener(192-168-3-69.local.)1152  JmDNS(192-168-3-69.local.).Timer1152  JmDNS(192-168-3-69.local.).State.Timer  20  SchedulerTaskService  20  pool-5-thread  20  pool-17-thread  11  Timer  10  http-nio-8081-exec  10  edge

6915 个线程,97%,全是 JmDNS。其他业务线程加起来都不到 200 个。


四、JmDNS 实例泄漏

JmDNS 是 Java 写的 mDNS/DNS-SD 库,做零配置网络服务发现用的。

每次调用 JmDNS.create() 都会创建 3 个线程:

JmDNS.create(iface)  ├── SocketListener    → 阻塞在 UDP multicast receive() 上  ├── Timer             → 维护 DNS 记录的定时器  └── State.Timer       → 维护实例状态机的定时器

应用里每次要做服务发现就调一次 JmDNS.create(),但**从来不调 jmdns.close()**。实例越积越多,两个网络接口各积了 1152~1153 个实例,合计 6915 个线程。

这也解释了为啥 2321 个 RUNNABLE 线程不吃 CPU——SocketListener 阻塞在 PlainDatagramSocketImpl.receive0()(native UDP recv),属于 I/O 等待,Java 层面状态显示 RUNNABLE,实际不占 CPU。


五、处理和修复

临时处理

重启应用,load 立马下来了:

15min: 82.69  →  5min: 52.10  →  1min: 4.16

内存也跟着释放了 1.5 GB(都是线程栈占的):

指标
重启前
重启后
load (1min)
48.45
4.16
CPU idle
77%
94%
内存已用
2864 MB
1323 MB

代码怎么改

错的写法(每次 create,从不 close):

JmDNS jmdns = JmDNS.create(iface);jmdns.registerService(info);// 没有 close,3 个线程永远泄漏

对的写法(用完就关):

JmDNS jmdns = JmDNS.create(iface);try {    jmdns.registerService(info);// ... 用完了} finally {    jmdns.close();  // 释放 3 个线程 + socket fd}

更好的写法(单例,全局复用):

// 应用启动时创建,关闭时销毁,中间一直复用这一个实例@Bean(destroyMethod = "close")public JmDNS jmDNS()throws IOException {return JmDNS.create(InetAddress.getLocalHost());}

六、高负载排查通用流程

这个案例走完,把通用的排查流程也整理一下。

如何读 top

负载与趋势

load average: 48.45, 64.62, 89.64              ↑1min  ↑5min  ↑15min
  • 1min > 15min:负载上升中,问题仍在恶化
  • 1min < 15min:负载下降中,峰值已过
  • 三者接近:负载处于稳定状态

CPU 各字段含义

%Cpu(s): 5.3 us, 16.8 sy, 0.0 ni, 77.6 id, 0.0 wa, 0.0 hi, 0.4 si
字段
含义
偏高时说明
us
用户态 CPU
应用代码在大量计算
sy
内核态 CPU
系统调用多、线程切换频繁、驱动繁忙
id
空闲
CPU 没饱和,问题不在计算
wa
等待 I/O
磁盘/存储是瓶颈,进程在 D 状态阻塞
si
软中断
网络包处理压力大(高带宽或高 PPS)
hi
硬中断
硬件设备中断频繁

Tasks 行

Tasks: 243 total,  1 running, 242 sleeping,  0 stopped,  0 zombie
  • running:当前在 CPU 上的线程数,超过核心数说明在排队
  • zombie:僵尸进程,父进程没有回收子进程,通常是代码 bug

排查决策树

第一步:load / nproc 是否 > 1?│└─ 是 → 系统过载,继续判断   │   ├─ Swap used > 0 且 si/so 不为 0   │    └─→ 内存压力,正在换页(见下文)   │   ├─ wa > 10%   │    └─→ I/O 瓶颈(见下文)   │   ├─ us > 60%   │    └─→ CPU 计算饱和(见下文)   │   ├─ sy > 20%(且 us 不高)   │    └─→ 内核态开销高(见下文)   │         通常是线程数爆炸或频繁系统调用   │   └─ 以上均不明显,但 id 仍偏高        └─→ D 状态进程(见下文)             CPU 没事干,但进程卡在内核操作出不来             常见:NFS 超时、内核锁、异常驱动

遇到 id 高但 load 高的情况最容易误判。CPU 空闲不代表系统正常,要重点排查 D 状态进程和线程数。

各类瓶颈的排查方法

内存压力(Swap 被使用)

特征:top 底部 Swap used > 0,vmstat 中 si/so 不为 0

# 查看内存使用free -h# 观察换页速率(si=换入, so=换出)vmstat 1 5# 找内存占用最多的进程ps -eo pid,rss,comm --sort=-rss | head -20# Java 应用查看堆内存jmap -heap <pid>

si/so 持续不为 0 说明系统在频繁换页,性能会严重下降,需要找内存泄漏或扩容。

I/O 瓶颈(wa 高)

特征:wa > 10%,进程大量处于 D 状态

# 确认哪块磁盘繁忙iostat -x 1 5# 关注 %util(接近100%说明磁盘饱和)、await(I/O 平均等待时间)# 找出哪个进程在读写iotop -o# 确认 D 状态进程ps aux | awk '$8 ~ /D/ {print $0}'

常见原因:日志疯狂写入、数据库大量读写、磁盘故障导致 I/O 超时。

CPU 计算饱和(us 高)

特征:us > 60%,top 中有进程长期占满 CPU

# top 中按 P 键按 CPU 排序,找高占用进程# 定位 CPU 热点函数(需要安装 perf)perf top -p <pid># Java 应用找热点线程jstack <pid> | grep -A10 'RUNNABLE' | grep -v 'java.lang\|sun\.\|jdk\.' | head -50

常见原因:死循环、正则回溯、GC 频繁(Full GC)、加密/压缩运算。

内核态开销高(sy 高)

特征:sy > 20%,us 不高,vmstat 中 cs 偏高

# 查看上下文切换频率vmstat 1 5# cs 列:正常服务器通常 < 10000/s,超过 50000/s 需关注# 找上下文切换最多的进程pidstat -w 1 5 | sort -k6 -rn | head -20# 查看线程数异常的进程ps -eo pid,nlwp,comm --sort=-nlwp | head -20

常见原因:

  • 线程/进程数量过多(如线程池无上限、线程泄漏)
  • 大量短生命周期进程频繁 fork(shell 脚本循环)
  • 锁竞争激烈(多线程争同一把锁)

D 状态进程(以上均不明显,但 load 仍高)

特征:CPU idle 高,但 load 居高不下,ps 看到 D 状态进程

# 找 D 状态进程ps aux | awk '$8 ~ /D/ {print $0}'# 或ps -eo pid,ppid,stat,wchan:30,comm | grep '^.*D'# 确认进程卡在哪个内核函数cat /proc/<pid>/wchan# 内核日志排查dmesg -T | grep -E 'hung|blocked|timeout|error' | tail -30

常见 wchan 与含义:

wchan
说明
nfs_...
NFS 文件系统操作超时
mutex_lock
内核互斥锁等待
blk_...
块设备 I/O 等待
xfs_...
XFS 文件系统操作

各类问题的指标指纹

问题类型
load
wa
us
sy
id
首选工具
内存不足换页
中高
中
低
低
中
vmstat si/so
CPU 计算饱和
高
低
高
低
低
top
 / perf top
磁盘 I/O 瓶颈
高
高
低
低
低
iostat
 / iotop
线程/进程爆炸
高
低
低
高
高
ps nlwp
 / jstack
NFS/内核锁阻塞
高
低
低
低
高ps D状态
 / wchan
网络高 PPS
中高
低
低
中
中
sar -n DEV
 / ss

工具速查

# 全局概览top                          # 实时概览,按 1 展开每个 CPU 核# CPU + 内存 + I/O + 上下文切换vmstat 1 5                   # 每秒采样,看趋势# 磁盘 I/Oiostat -x 1 5                # 磁盘利用率、等待时间iotop -o                     # 哪个进程在读写磁盘# 进程/线程ps aux | awk '$8~/D/'# D 状态进程ps -eo pid,nlwp,comm --sort=-nlwp | head -20   # 线程数排行pidstat -w 1 5               # 进程上下文切换# 网络ss -s                        # 连接状态概览sar -n DEV 1 5               # 网卡流量# 内核日志dmesg -T | tail -50          # 内核错误/告警# Java 专项jstack <pid>                 # 线程 dumpjstack <pid> | grep 'Thread.State' | sort | uniq -c | sort -rn   # 线程状态统计jstack <pid> | grep '"' | awk -F'"''{print $2}' | sed 's/-[0-9]*$//' | sort | uniq -c | sort -rn | head -20  # 线程名聚合

七、几个容易搞错的点

1. load 高不等于 CPU 高

load 统计的是 R+D 状态线程数,D 状态进程卡在内核里不吃 CPU,但照样贡献 load。看到高负载别急着找高 CPU 进程,先看 id 和 wa。

2. Java RUNNABLE 不等于真在跑

大量 RUNNABLE 线程阻塞在 native I/O(比如 UDP recv)时,CPU 利用率可以很低,但这些线程照样占线程栈内存和调度器资源。

3. 线程数稳定不代表没泄漏

这个案例里线程数稳定在 7157 左右,是因为触发创建的代码路径不活跃了,不是代码修好了——存量一直在,只是不再新增。

4. 有 create 的地方必须有 close

持有线程、socket、文件句柄的对象,不关就是慢性泄漏,在高频路径上会快速放大。

5. vmstat 很好用

top 看完方向,接着 vmstat 1 5 一条命令可以查 CPU/内存/I/O/上下文切换,快速缩小范围。

最新文章

随机文章