load average 飙到 229 但 CPU 不满:atop 回放定位无 swap 机器上 go build 引发的 iowait 读盘风暴
现象一句话:一台 8 核测试机 load average 冲到 229,但没有 OOM、服务没挂,十分钟自己恢复了。最后用 atop 回放把根因钉到分钟级——不是 CPU 不够,是内存被编译榨干、又没有 swap,把 page cache 砸穿引发了读盘风暴。
这篇记录排查方法和那条容易被忽略的因果链:load 高 ≠ CPU 忙,以及为什么一台没有 swap 的机器,一次 go build 就能把 load 顶到 200+。所有命令可照抄,结尾有速查清单。

环境
- OS:Ubuntu 22.04(kernel 5.15)
- 规格:8 vCPU / 15G 内存,系统盘 ext4
- 关键:无 swap(
SwapTotal: 0) - 角色:测试环境业务机,单机挤了约 30 个 Go 微服务 + MySQL/搜索引擎/缓存等常驻进程,内存长期吃紧
- 已装 atop(
LOGINTERVAL=30,日志/var/log/atop/atop_<YYYYMMDD>)——这是事后能回放的前提
没装 atop 的机器,过去那一秒的逐进程现场是找不回的。生产/测试机建议常备 atop 或 sysstat。
一、现象:load 飙到 229,CPU 却几乎空闲
先两次取样看 load 是不是已经回落(避免对着一个已经过去的尖峰瞎查):
uptime
# load average: 11.10, 67.21, 69.87 ← 1分钟已回落到11,但5/15分钟还67~70,说明刚发生过尖峰
free -h
# total used free shared buff/cache available
# Mem: 14Gi 11Gi 275Mi 57Mi 2.9Gi 2.7Gi
# Swap: 0B 0B 0B ← 没有 swap
内存只剩 275M,且没有 swap。再看当下谁在吃 CPU:
ps -eo pid,user,%cpu,%mem,rss,etime,comm --sort=-%cpu | head
# 最高的进程才 3.4% CPU —— 没有任何进程在打满 CPU
到这里第一个反直觉点出现了:load 飙到几十上百,但没有任何进程在烧 CPU。这通常意味着负载来自 D 状态(不可中断睡眠,基本是在等磁盘 I/O),而不是 CPU 算力。
Linux 的 load average 统计的是「处于 R(可运行)+ D(不可中断睡眠)状态的任务数」,不是 CPU 利用率。一堆进程同时卡在等磁盘,load 一样会爆。
二、用 atop 回放尖峰窗口
load 已经回落,现场没了——但 atop 把历史每 30 秒存了一帧。先用解析模式拉出 load 和 CPU 利用率的时间线,定位尖峰在哪一段:
F=/var/log/atop/atop_$(date +%Y%m%d)
# CPU 各状态时间线,重点看 wait(iowait)
LC_ALL=C atop -r $F -b 10:50 -P CPU | grep '^CPU' | awk '{print $5, "sys="$8, "usr="$7, "wait="$12}'
输出里 wait(iowait)那一列在尖峰段直接拉满:
... wait=18
... wait=13720 ← 开始飙
... wait=19101
... wait=23403 ← 持续两万多
确认是 iowait 主导。再用 -P CPL 找出 load 的精确峰值时刻,然后把那一帧完整打出来。
三、峰值帧:所有线索都在这一帧里
LC_ALL=C atop -r $F -b 11:02 -e 11:04
峰值那一帧(已脱敏)信息量极大:
ATOP - <testbox> 11:03:02 ---------------- 30s elapsed
PRC | sys 9.97s | user 79.92s | #proc 381 |
CPU | sys 37% | user 258% | irq 0% | idle 1% | wait 504% | ← 8核里约5核在等磁盘,2.5核在跑计算
CPL | avg1 229.47 | avg5 116.83 | avg15 66.32 | ← load 峰值 229
MEM | tot 14.7G | free 288.2M | buff 4.3M | slab 500.1M | ← page cache 只剩 4MB(!)
SWP | tot 0.0M | free 0.0M | vmcom 28.8G | vmlim 7.4G | ← 无swap,已承诺内存是上限的~4倍
PSI | cpusome 2% | memsome 78% | memfull 57% | iosome 97% | iofull 62% | ← 内存与IO双重停顿
DSK | vda | busy 102% | read 122660 | write 282 | avio 0.24 ms | ← 磁盘被读打满,几乎没有写
逐行读这帧:
CPU wait 504%(总共 800%):8 个核里约 5 个卡在磁盘等待,sys 37%、user 258%——CPU 不是瓶颈,磁盘才是。DSK vda busy 102%,read 122660 / write 282:30 秒读了 12 万次、几乎不写——是疯狂读盘。MEM free 288M,buff 4.3M:page cache 被挤到只剩 4MB(正常应有几个 G)。SWP 0,vmcom 28.8G vs vmlim 7.4G:没有 swap,且内存严重超分。PSI iosome 97% / memfull 57%:内存和 I/O 两头都在制造停顿,进程被卡住等资源。
四、谁在读盘:不是一个进程,是全体一起重读
按磁盘读排序看进程级(atop -d):
LC_ALL=C atop -r $F -b 11:03 -e 11:04 -d
PID RDDSK WRDSK CMD
... 318.2M 76K app_server_a
... 239.2M 12K link ← Go 链接器
... 225.6M 4K app_server_b
... 213.5M 48K app_server_c
... 204.2M 4K app_server_d
...(十几个服务各读 160~320M)
关键现象:读盘量几乎均摊在十几个服务上,没有谁独大。这正是 page cache 塌缩的特征——所有服务的代码页都被驱逐了,于是它们同时从磁盘重读自己的二进制。
再按 CPU 排序(atop -c),抓到了点火的那个进程:
PID CPU COMMAND-LINE
... 47% .../go/pkg/.../tool/linux_amd64/link -o /tmp/go-build.../a.out -buildmode=exe ← Go 链接器,D态
... 0% go build --ldflags -X .../GitVersion=<commit> -X .../GitBranchName=origin/develop
峰值帧里有一个 go build 父进程 + 一个 Go link(链接器)子进程在 D 态读盘。 这是直接证据:编译就在尖峰那一刻正在跑。
五、用三个旁证把"是 go build"钉死
光看到进程还不够,再用三类时间戳交叉验证(不依赖 atop):
# ① 编出来的二进制 mtime = 编译完成时刻,正好落在尖峰窗口
ls -lt --time-style='+%H:%M' /path/to/built/binaries/ | head
# 11:10 app_server_a ← 最大的二进制,编完时刻=load 229 那波结束点
# 10:39 app_server_b ← 对应更早一波尖峰
# ② go build 缓存逐分钟落盘直方图,爆发点与尖峰对齐
find ~/.cache/go-build -newermt '10:00' ! -newermt '11:15' -printf '%TH:%TM\n' | sort | uniq -c
# 1490 10:34 ← 尖峰起点
# 3090 11:10 ← 最大一波,落盘文件最多=load最高
# ③ 两天对比:昨天没人编译 → 昨天全天 0 次 load>40
LC_ALL=C atop -r /var/log/atop/atop_<昨天> -P CPL | grep '^CPL' | awk '$8>40{print $5,$8}'
# (空) ← 同样的定时任务天天在跑,昨天却一点不尖,排除"定时任务"嫌疑
证据链闭环:源码改动 → atop 抓到 go build/link 在尖峰运行 → 编译缓存落盘直方图对齐 → 二进制 mtime 对齐 → 两天对比排除其它因素。
六、根因:满载内存 + 无 swap,被一次编译压崩
把现象串成因果:
- 这台机常驻服务把 15G 内存常年压到
free < 300M,几乎没有空闲余量; - 一次
go build大体积 Go 服务,编译器/链接器要吃掉一块内存,这台机没有这块余量给它; - 关键放大器——没有 swap:内存一紧,内核无法把匿名页换出(没地方换),就只能反复驱逐文件页(代码/库)再立刻重读;
- 于是 page cache 当场塌到 4MB,十几个服务同时从磁盘冷读自己的二进制 →
vda100% busy、30 秒读 12 万次; - 大批进程卡进 D 状态等磁盘 → load = R+D 任务数,雪球一样堆到 229;
- 编译在 11:10 落盘、读压解除,队列瞬间泄掉,load 直接跌回 40 以下,全程十分钟、无 OOM。

为什么没 OOM 反而抖 load?因为没有 swap 时,内核倾向于反复回收+重读文件页(page cache),而不是触发 OOM killer——表现就是"不崩,但磁盘和 load 爆炸"。
一句话:问题不在
go build本身,在这台机"满载 + 无 swap"的底子。同样的编译,放在有充足内存、或哪怕只配了 swap 的机器上,顶多内存紧一点,绝不会爆出 229 的 load。
七、处置:加 swap + 调 swappiness
1. 加一个 8G swapfile(立竿见影)
fallocate -l 8G /swapfile # ext4 可用;某些文件系统(如 btrfs)要改用 dd
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
swapon --show # 确认已启用
# 持久化(重启自动挂载)
grep -q '^/swapfile ' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
内存见底时有 swap 兜底,内核可以把闲置匿名页换出,而不是把 page cache 砸穿、被迫反复读盘。
2. 调 swappiness——并避开一个覆盖坑
光加 swap 还不够。这台机 vm.swappiness=10 偏低,意味着内核仍倾向于先丢文件缓存、而不是用 swap 换出匿名页——恰恰是引发读盘风暴的那个行为。对这个故障,应调高 swappiness(用默认 60),让编译时优先换匿名页、保住 page cache:
sysctl -w vm.swappiness=60 # 运行时立即生效,无需重启
坑:别只写 drop-in 文件。 我一开始把 vm.swappiness=60 写进 /etc/sysctl.d/99-swappiness.conf,结果 sysctl --system 跑完最终值还是 10:
* Applying /etc/sysctl.d/99-swappiness.conf ...
vm.swappiness = 60
vm.swappiness = 10 ← /etc/sysctl.conf 在 .d/ 之后加载,把 60 覆盖回去了
原因:sysctl --system 的加载顺序里,/etc/sysctl.conf 在 /etc/sysctl.d/* 之后,会覆盖 drop-in。而且这台机的 /etc/sysctl.conf 里重复写了两行且互相矛盾(=0 和 =10)。正确做法是直接改源头:
cp -a /etc/sysctl.conf /etc/sysctl.conf.bak.$(date +%Y%m%d) # 先备份
sed -i -E 's/^vm\.swappiness[[:space:]]*=[[:space:]]*[0-9]+/vm.swappiness = 60/' /etc/sysctl.conf
sysctl --system 2>/dev/null | grep -i swappiness # 复核最终值
sysctl -n vm.swappiness # = 60 才算 reboot-safe
3. 根治:别在跑着服务的机器上编译
swap 只是缓解。干净的做法是 CICD 把"编译"和"运行"分离:在专门的 builder/CI 里 go build 出二进制,只把产物投递到目标机重启,目标机不承担任何编译负载。当前是"拉码→本机编译→重启"全压在一台跑满服务的机器上,才会一编就崩。
快速参考
判断 load 高是 CPU 型还是 I/O 型
uptime # 看 load 趋势(1/5/15 分钟)
vmstat 1 5 # r 列高=CPU型;b 列高 + wa 高=I/O型
# 或直接看 atop 峰值帧的 CPU 行:user 高=算力;wait 高=等盘
atop 回放历史尖峰(无现场也能查)
F=/var/log/atop/atop_$(date +%Y%m%d)
atop -r $F -P CPL | grep '^CPL' | awk '$8>40{print $5,$8}' # 全天 load>40 的时刻
atop -r $F -b HH:MM -e HH:MM # 打印某窗口全帧
atop -r $F -b HH:MM -e HH:MM -d # 按磁盘读排序进程
atop -r $F -b HH:MM -e HH:MM -c # 看进程完整命令行
内存型 iowait 风暴的识别特征(峰值帧)
CPU wait很高 +idle接近 0,但user/sys不满MEM buff/cache异常小(被挤塌)、free见底、SWP为 0DSK busy~100%,read巨大、write很小(是重读不是写)PSI iosome/memfull高
加 swap 速查
fallocate -l 8G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab # 持久化
swapoff /swapfile && rm /swapfile # 回退(并删 fstab 行)
swappiness 易错点
- 运行时改:
sysctl -w vm.swappiness=60(立即生效,无需重启) - 持久化要改
/etc/sysctl.conf(它在/etc/sysctl.d/*之后加载、会覆盖 drop-in) - 改完用
sysctl --system && sysctl -n vm.swappiness复核最终值
一句话铁律
没有 swap 的机器,内存一紧不是"慢慢满",而是"page cache 瞬间塌缩→读盘风暴→load 爆炸"。给吃紧的机器配 swap、别在跑满服务的机器上做大编译。

浙公网安备 33010602011771号