Skip to content

필사 모드: 为什么平均负载不是 CPU 使用率 — load average 24 而 CPU 只有 30% 的时候

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

开篇 — load average 24,可 CPU 只有 30%

凌晨收到告警。一台 8 核服务器的平均负载超过了 24。登录上去打开 top,CPU 空闲还剩 60% 以上。这时很容易滑向两个结论之一:“监控出问题了”,或者“得把 CPU 扩到三倍”。两个都是错的。

平均负载不是 CPU 使用率。连单位都不一样。使用率是比例 (0~100%),而负载是个数 (任务数量)。而且 Linux 在数这个个数时,多数了一样其他 Unix 不数的东西。就是这一样东西占了本文的一半篇幅。

uptime
#  03:41:52 up 12 days,  3:21,  2 users,  load average: 24.31, 22.08, 14.95

nproc
# 8

cat /proc/loadavg
# 24.31 22.08 14.95 2/1284 48210

看最后一行的 2/1284。前面的 2 是此刻可运行的线程数,1284 是线程总数。负载是 24,可可运行的线程只有 2 个。剩下的 22 在哪里呢?这个问题的答案就是诊断的起点。

三个数字的真身 — 5 秒采样与指数移动平均

内核按 LOAD_FREQ 周期,也就是每 5 秒一次 (准确地说是 5 秒加 1 个 tick,用意是避开与周期性中断的相位重合),从每个 CPU 的运行队列采样活跃任务数。活跃任务是这样计算的。

nr_active = rq->nr_running + rq->nr_uninterruptible

nr_running 是当前正在运行或可运行的任务,nr_uninterruptible 是处于不可中断睡眠状态的任务。把这个和在所有 CPU 上相加,就是那一瞬间的样本。

接下来的更新公式才是核心。内核用定点运算计算下面这些。

load = (load * EXP + active * (FIXED_1 - EXP)) >> FSHIFT

FSHIFT  = 11,  FIXED_1 = 2048
EXP_1   = 1884   (2048 * e^(-5/60))
EXP_5   = 2014   (2048 * e^(-5/300))
EXP_15  = 2037   (2048 * e^(-5/900))

把常数验算一遍,理解会快很多。每 5 秒更新一次而要得到 1 分钟的时间常数,衰减系数必须是 e 的负 5/60 次方,也就是 0.9200。0.9200 乘以 2048 等于 1884。5 分钟和 15 分钟按同样方式算出 2014 和 2037。

由此引出一个在实战中很重要的性质。这不是算术平均,而是指数衰减平均。负载像台阶一样跃升时,1 分钟平均恰好在 1 分钟后达到最终值的 63.2%,要达到 99% 则需要大约 5 分钟。按同样的逻辑,15 分钟平均会被拖上一个多小时。

经过时间1 分钟平均反映率5 分钟平均反映率15 分钟平均反映率
1 分钟63.2%18.1%6.5%
5 分钟99.3%63.2%28.3%
15 分钟100.0%95.0%63.2%
45 分钟100.0%100.0%95.0%

所以“1 分钟平均大于 5 分钟和 15 分钟就说明负载正在上升”这个惯用解读是成立的。反过来,故障刚结束时只有 15 分钟平均还很高,属于正常现象。用那个数字设告警,恢复之后还会响 20 分钟。

Linux 独有的关键差异 — 连 D 状态也算进去

传统 Unix 把平均负载只定义为运行队列的长度,也就是等待 CPU 的任务数。Linux 在 1993 年往里面加上了 TASK_UNINTERRUPTIBLE 状态。意图很明确:不只是衡量 CPU 需求,而是要衡量对整个系统资源的需求

TASK_UNINTERRUPTIBLE 就是在 ps 中显示为 D 的状态。它是连信号都唤不醒的内核内部等待,典型的有这些。

  • 等待块设备 I/O 完成 (io_schedule)
  • 等待页缓存锁 (folio_wait_bit_common)
  • 等待硬挂载的 NFS 服务器响应
  • 等待某些内核互斥量和信号量
  • 等待设备驱动的固件响应

这份清单里没有 CPU。也就是说,即便 CPU 完全闲着,平均负载照样可以涨到任意高度。只要有一台 NFS 服务器不响应,访问过那个挂载点的进程就会全部堆积在 D 状态,8 核服务器的负载冲到 200,而 CPU 曲线一片平坦。这是正常行为。

有一个例外值得记住。从内核 4.2 开始引入了 TASK_IDLE (TASK_UNINTERRUPTIBLE | TASK_NOLOAD),无事可做而等待的内核线程不再计入负载。在 ps 中显示为 I 的那些就属于这一类。

ps -eo state,pid,comm --no-headers | awk '{c[$1]++} END {for (s in c) print c[s], s}' | sort -rn
#    1183 S
#      68 I
#      22 D
#       2 R
#       9 Z

D 是 22,R 是 2。和前面看到的负载 24 严丝合缝。到这一步结论只有一个。这台服务器不是 CPU 不够,而是在某个地方等待 I/O

这里顺带点一下常见的错误说法。“平均负载就是 CPU 等待队列的长度”这句话在其他 Unix 上成立,在 Linux 上是错的。而“负载低于核心数就安全”这条规则,只要那个数值里混着 D 状态,就同样保证不了安全。

“按核心数相除”在哪里失效

把负载除以 nproc,小于 1.0 算宽裕、大于 1.0 算饱和,这个习惯作为起点还算好用。但在下面五种情况下会失效。

第一,D 状态污染。和上一节完全一样。24/8 = 3.0,可这和 CPU 饱和毫无关系。

第二,平均值的滞后。30 秒的尖峰在 1 分钟平均里连一半都体现不出来。反过来,早已结束的故障却会一直留在 15 分钟平均里。

第三,SMT(超线程)。nproc 数的是逻辑 CPU。8 个物理核心开启 SMT 会显示 16,但实际处理能力并不是 16 核。负载 12 不等于“16 核里用了 75%”。

lscpu | grep -E '^CPU\(s\)|Thread|Core|Socket'
# CPU(s):                          16
# Thread(s) per core:              2
# Core(s) per socket:              8
# Socket(s):                       1

第四,cgroup 配额。容器上设了 CPU limit 而被限流的任务会彻底从运行队列里退出,因此根本不会被平均负载统计到。在 64 核的宿主机上,一个被限制到 0.5 CPU 的 Pod 慢得离谱,宿主机负载却安安静静。这个组合后面还会再讲。

第五,归属信息的缺失。负载是全系统范围内的一个标量。它不会告诉你是哪个容器、哪个服务、哪种资源导致的。你可以拿它设告警,但那个告警能让你做的事只有“进去看看”。

负载高而 CPU 空闲的情况 — 诊断路径

这里是有顺序的。从上到下三分钟就能走完。

# 第 1 步: 确认负载的构成 — R 少而 D 多就是 I/O 路径
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 22      0 512344  84120 6231044    0    0     0     0 2103 4102  4  3 21 72  0
#  1 23      0 511208  84120 6231180    0    0 132096   412 2288 4390  3  4 19 74  0
#  2 22      0 510992  84120 6231212    0    0 128512   380 2251 4301  4  3 20 73  0

b 列是 22 到 23。这一列正是处于不可中断睡眠的任务数。wa (iowait) 达到 72% 说的也是同一件事。iowait 是“CPU 空闲但该 CPU 上存在 I/O 等待任务的时间”,所以它其实是空闲时间的一种。数值高本身并不等于坏,但和 b 列一起看,方向就很明确了。

# 第 2 步: 看看是哪个设备饱和了
iostat -x 1 3
# Device      r/s     rkB/s  rrqm/s %rrqm r_await rareq-sz    w/s   wkB/s  w_await  aqu-sz  %util
# nvme0n1   1842.0   58944.0    0.0  0.00   12.85    32.00  210.0  3360.0     3.02   24.30  99.60
# nvme1n1      2.0      64.0    0.0  0.00    0.31    32.00    1.0    12.0     0.22    0.00   1.20

要读的有三样。%util 是不是贴在了 100%(设备一刻不得闲)、r_await/w_await 是平时的多少倍 (每个请求的延迟)、aqu-sz 有多深 (队列里积压的平均请求数)。在 NVMe 上,%util 100% 可能因为并行处理而并不意味着饱和,所以判断要靠 await 和队列深度。

# 第 3 步: 逐个查看 D 状态的进程
ps -eo state,pid,ppid,comm --no-headers | awk '$1 == "D"' | head
# D  4821  4102 postgres
# D  4822  4102 postgres
# D  7710     1 kworker/u33:2
# 第 4 步: 看那个进程停在内核的什么位置 (需要 root 权限)
sudo cat /proc/4821/stack
# [<0>] io_schedule+0x46/0x80
# [<0>] folio_wait_bit_common+0x131/0x330
# [<0>] filemap_fault+0x5f0/0xa50
# [<0>] __do_fault+0x39/0x120
# [<0>] handle_mm_fault+0xd4e/0x1090

栈顶是 io_schedule 就是块 I/O,看到 rpc_wait_bit_killablenfs_wait_on_request 就是 NFS,属于 __lock_page 一类的则是页缓存争用。如果 /proc/PID/stack 是空的或者报权限错误,那要么是内核没有 CONFIG_STACKTRACE,要么是权限不足。

怀疑 NFS 时,直接看挂载统计。

# 查看每台服务器的 RTT 与重传
nfsstat -c | head -20
mountstats --nfs /mnt/shared 2>/dev/null | grep -A3 'READ:'

# 找出没有响应的服务器 (硬挂载会无限等待)
grep nfs /proc/mounts
# 10.0.9.4:/export/data /mnt/shared nfs4 rw,hard,proto=tcp,timeo=600,retrans=2 0 0

hard 挂载会把进程按在 D 状态,直到服务器回来为止。这种状态下连 kill -9 都不管用。从运维角度可以考虑 softerr 或更短的 timeo,但要先确认数据一致性方面的要求。

负载低却依然很慢的情况 — 负载看不见的东西

反方向也很常见。负载是 0.8,p99 却是 3 秒。这时把负载在结构上根本看不见的东西列出来,候选范围就缩小了。

  • 可中断等待(S 状态)不计入负载。等待套接字响应的时间,也就是下游 API 或数据库的延迟,全都属于这里。应用变慢的原因大多在这一侧。
  • 用户态锁争用也大多处于 S 状态。futex 等待是可中断的,所以再严重负载也不会上升。
  • cgroup CPU 限流如前所述,因为把任务从运行队列里摘掉了,所以看不见。
  • 单线程瓶颈按定义就是负载 1。哪怕有 64 个核心,负载也不会超过 1。
  • 虚拟机监控器的窃取时间意味着 CPU 被抢走了,而不是队列变长了。

区分这五种情况的命令如下。

# S 状态下在等什么 (套接字等待在这里看不到,所以要看栈)
sudo cat /proc/9931/stack | head -5

# 锁争用: 上下文切换是否暴涨
vmstat 1 3        # cs 列是平时的多少倍
pidstat -w -p 9931 1 3
# UID  PID   cswch/s nvcswch/s  Command
# 1000 9931  18422.0   34110.0  java

# 限流 (cgroup v2)
cat /sys/fs/cgroup/cpu.stat
# nr_periods 86400
# nr_throttled 41287
# throttled_usec 2914837261

# steal
mpstat -P ALL 1 3 | tail -3

nvcswch/s (非自愿上下文切换) 大就是 CPU 争用,cswch/s (自愿) 大则是因为锁或 I/O 等待而睡下又醒来的模式。

PSI — 回答平均负载答不上来的那个问题的指标

平均负载的根本问题在于,它只说有多少个在等待,却不说停顿了多久。从内核 4.20 进来的 PSI 量的正是后者。

cat /proc/pressure/io
# some avg10=78.23 avg60=71.04 avg300=48.92 total=8172635241
# full avg10=61.17 avg60=55.83 avg300=37.10 total=6019283746

cat /proc/pressure/cpu
# some avg10=2.41 avg60=1.88 avg300=1.02 total=91827364
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=142
# full avg10=0.00 avg60=0.00 avg300=0.00 total=88

读法是这样的。

  • some 是至少有一个任务因为该资源而停顿的时间占比。
  • full 是所有可运行任务同时停顿的时间占比。也就是说,在那段时间里系统没能做成任何有用的事。
  • avg10avg60avg300 分别是 10 秒、60 秒、300 秒窗口的百分比,total 是累计的微秒数。
  • 全系统 cpufull 按定义没有意义,所以恒为 0 (从内核 5.13 开始这一行本身会被输出)。

上面的输出说明的事情很清楚。I/O 的 full 是 61%,意思是最近 10 秒里有 6 秒以上整个系统都因为 I/O 而停顿。这比负载 24 这个数字直接得多,更重要的是它可以直接换算成损失掉的吞吐量

PSI 的第二个优点是它按 cgroup 粒度存在。这正是平均负载所没有的归属信息。

# 哪个 cgroup 因为 I/O 停顿得最久
for f in /sys/fs/cgroup/**/io.pressure; do
  v=$(awk '/^full/ {print $2}' "$f" | cut -d= -f2)
  printf '%6s  %s\n' "$v" "${f%/io.pressure}"
done 2>/dev/null | sort -rn | head -5
#  58.12  /sys/fs/cgroup/system.slice/postgresql.service
#   3.04  /sys/fs/cgroup/system.slice/containerd.service
#   0.00  /sys/fs/cgroup/user.slice

如果没有 /proc/pressure,那要么是 CONFIG_PSI 被关掉了,要么是内核以 CONFIG_PSI_DEFAULT_DISABLED=y 构建,需要启动参数 psi=1

指标量的是什么是否反映 D 状态时间窗口cgroup 粒度
load average正在等待的任务个数包含1/5/15 分钟 EMA不支持
CPU 使用率真正用在 CPU 上的时间比例不包含任意支持
iowait空闲 CPU 中存在 I/O 等待的部分间接任意不支持
PSI cpu.some因为 CPU 而停顿的时间不包含10/60/300 秒支持
PSI io.full因为 I/O 整体停顿的时间事实上就是它10/60/300 秒支持

结语 — 平均负载是队列长度,不是使用率

只记住一件事的话,就记这个。Linux 的平均负载是把等待 CPU 的任务和等待 I/O 的任务不加区分地合并计数后的指数移动平均。所以光凭这一个数字,回答不了“要不要加 CPU”。

整理成实战规则就是下面这些。

  1. 负载高的时候,先看 /proc/loadavg 的第四个字段和 vmstat 1b 列。R 与 D 的比例决定方向。
  2. 如果 D 很多,就往 iostat -x 的 await 和 /proc/PID/stack 下钻。加 CPU 不是答案。
  3. 告警设在 PSI 上,而不是负载上。cpu.some avg60io.full avg60 越过阈值,与真实延迟的相关性更强,误报也少得多。
  4. 负载可以留在仪表盘上,但只当趋势指标用。1 分钟值大于 5 分钟和 15 分钟就说明在上升,读到这里为止,不多读。
  5. 如果是容器环境,宿主机负载解释不了 Pod 的性能。必须去看 cgroup 的 cpu.stat*.pressure

현재 단락 (1/99)

凌晨收到告警。一台 8 核服务器的平均负载超过了 24。登录上去打开 `top`,CPU 空闲还剩 60% 以上。这时很容易滑向两个结论之一:“监控出问题了”,或者“得把 CPU 扩到三倍”。两个都是...

작성 글자: 0원문 글자: 7,016작성 단락: 0/99