说到 CPU 负载均衡,很多人的第一反应都是top或uptime输出里的 load average。这个数字简单直观,但也特别容易被误解。曾经一台 8 核机器 loadavg 冲到 40,我第一反应是 CPU 被打满了,结果一看us只有 15%,wa却到了 35%,大量进程卡在磁盘 IO 上。后来把 loadavg 的计算逻辑仔细过了一遍才发现,这个数字背后藏着一整套和内核调度器紧密相关的设计:它统计什么、用什么模型平滑、多久采一次样、为什么和 CPU 使用率对不上。
这篇文章就把这块讲透。适合服务端开发、SRE、性能优化工程师,以及刚接触 Linux 内核但对“负载”这个概念有点懵的读者。看完之后,你不仅知道 loadavg 怎么算出来的,还能用它去定位真实瓶颈,而不是被一个数字带偏方向。
1. loadavg 的真面目:CPU 负载均衡里的“水位计”到底量了什么
1.1 它统计的是任务队列水位,不是 CPU 占用率
先看一个最常见的输出:
$ uptime 09:32:11 up 21 days, 3:05, 2 users, load average: 0.68, 0.52, 0.41三个数值分别代表最近 1 分钟、5 分钟、15 分钟的平均负载。/proc/loadavg里的信息更完整:
$ cat /proc/loadavg 0.68 0.52 0.41 1/245 3789这里第四段1/245表示当前可运行任务数 / 总任务数,第五段是最近创建的 PID,它们不是负载均衡那个 load average。真正需要关心的前三个数,统计口径是:当前处于 TASK_RUNNING 和 TASK_UNINTERRUPTIBLE 状态的任务数。
TASK_RUNNING包含正在 CPU 上执行的任务和已经在运行队列里等待 CPU 的任务。TASK_UNINTERRUPTIBLE是那些不可中断睡眠状态的任务,常见的D状态进程,比如正在等磁盘 IO、NFS、内存回收。这两种状态加在一起,才是内核计算 loadavg 的active计数。
所以 loadavg 根本不是一个百分比,它没有上限。一个进程占满一个核,CPU 使用率是 100%,loadavg 却可能只有 1;反过来,有 10 个任务都在等磁盘 IO,CPU 使用率可能很低,loadavg 却能到 10。本质上,它更像是一个“任务排队水位”,而不是“CPU 忙不忙”。
1.2 1 分钟、5 分钟、15 分钟,是三档不同记忆长度的镜头
为什么不是只看一个值?因为一个瞬间的值说明不了趋势,长时间的平均值又掩盖了突发抖动。三档平均就是三档不同“记忆长度”的镜头。
如果 loadavg 的 1 分钟值突然从 1 跳到 8,而 15 分钟值还是 2 左右,说明这是刚刚发生的流量尖峰,系统还没来得及消化。如果 15 分钟值也已经很高,说明压力已经持续了很久,不是偶发现象。
需要注意的是,这三个值不是简单的“最近一分钟内任务数的算术平均”,而是指数加权移动平均。旧的负载会以一定的比例留在新平均值里,越早的数据权重越小,但不会像滑动窗口那样被一刀切掉。这也是为什么一个持续 10 分钟的高峰,即使现在已经安静了,15 分钟均值依然要花很长时间才能掉下来。
2. loadavg 计算全过程:5 秒采样、定点数、指数衰减
2.1 每 5 秒采样一次,而不是每个时钟中断都算
内核每 5 秒更新一次全局 loadavg,这个间隔在源码里叫LOAD_FREQ。为什么不每个 tick(通常 1 毫秒到 10 毫秒)都算?因为没必要,而且成本不低。所有 CPU 都需要汇总任务状态,如果每个 tick 都去读所有运行队列,反而会给高负载系统增加额外开销。
这 5 秒里,每个 CPU 会不断把运行队列上可运行任务数和不可中断任务数的变化量累加到一个全局计数上。采样点到来时,再把这个累计值取出来,参与下一轮 loadavg 计算。这么做还有一个好处:如果某个瞬时高峰只持续了几百毫秒,内核算出来的 loadavg 也能“看到”它在 5 秒窗口内发生过,而不是恰好被采样点漏掉。简单说,内核不是每隔 5 秒对着运行队列拍一张照片,而是把 5 秒内的变化量都记账了。
2.2 核心公式:老值衰减,新值递增
loadavg 的更新逻辑可以简化成下面这个公式:
new_load = old_load * exp + active * (1 - exp)其中active是当前累计得到的目标状态任务数,old_load是上一轮的平均值,exp是小于 1 的衰减系数。系数越接近 1,旧值保留得越多,曲线越平滑,对新变化的反应也越慢。
内核实际是用定点数计算的,避免浮点开销。FSHIFT取 11,也就是FIXED_1 = 2048。三个档位分别对应三组系数,简化后的效果如下:
| 平均档位 | 5 秒采样周期对应的衰减系数 exp | 直观感受 |
|---|---|---|
| 1 分钟 | 约 0.92 | 反应快,能跟上短时波动 |
| 5 分钟 | 约 0.983 | 中间档,兼顾敏感和稳定 |
| 15 分钟 | 约 0.995 | 很平滑,适合看长期趋势 |
以 1 分钟均值为例,如果之前 loadavg 是 0,某一刻突然同时来了 4 个可运行任务,第一次采样算出来大约只有4 * (1-0.92) = 0.32。即使接下来一分钟一直保持 4 个任务,1 分钟均值也只能爬到 2.5 左右,要到接近 4 还需要更长的时间。这就是指数加权平均的特性:它永远不会瞬间跳到一个值,只会渐进逼近。
2.3 全局累计与 tickless 时代的补偿
现代内核支持 NOHZ,也就是 CPU 在没有任务时可能长时间不打 tick。如果每个 CPU 都各自维护一套负载统计,进入 tickless 状态后,采样频率就会失真。内核为此做了两件事:一是在 CPU 进出 idle 时计算并折叠这段时间的负载变化,二是在全局负载更新时把 tickless 期间缺失的那部分补偿回来。
这个细节日常排障中用得不多,但理解它对排查“为什么负载突然跳了 0.5”这种问题有帮助。loadavg 不是一个纯实时的瞬时值,它带了很多历史记忆和采样策略,看到它变化后,不要急着说“刚才 CPU 爆了”,应该先看任务状态。
3. 调度器负载均衡用的 load,和 loadavg 为什么不是同一个东西
3.1 两个都叫 load,但用途完全不同
很多做性能调优的人会有一个疑惑:CPU 负载均衡不是应该用 loadavg 吗?它的“负荷”不就在这里?其实不是。Linux 内核调度器内部确实有“负载”概念,但它不是/proc/loadavg里那个三个数。
loadavg 是全局的、平滑后的、面向系统管理员的观测指标。而调度器做负载均衡时,需要的是每一个 CPU 运行队列实时或者近实时可迁移的压力评估。用 loadavg 做调度依据会有几个致命问题:它太滞后、太全局化、无法区分单个 CPU 的忙闲,也没有任务优先级权重信息。
3.2 调度器用的是 per-CPU / per-entity 的负载跟踪
现代 Linux 调度器用的是 PELT(per-entity load tracking)思路。每个调度实体(可以理解为一个线程或一个任务组)都有一个基于运行时间衰减的负载贡献,运行越多,它对所在 CPU 的负载贡献越大;长期睡眠,贡献就衰减。CPU 上所有实体的负载加总,就是对应运行队列的 load。
调度器在触发负载均衡时,会遍历调度域里的各个 CPU 组,找出最忙的组和最忙的 CPU,然后比较它和空闲 CPU 之间的负载差。只有当差值超过一定阈值时,才迁移任务。这个过程中使用的指标,是经过任务权重和时间窗口计算出来的“调度负载”,而不是 uptime 里那个 global loadavg。
这样做的好处很明显:它能看到单核失衡。比如 16 核机器一半空闲一半忙,整体 loadavg 可能并不高,但某些 CPU 已经排队严重,另一些却在偷懒。只看 loadavg 完全发现不了这种问题,而调度器的 per-CPU 负载能够及时触发迁移。
3.3 两者的关系:一个对内一个对外
如果硬要打个比方,loadavg 是公司门口的电子屏,显示“当前园区内有多少人员工”;调度器内部的 load 是每个工区的实时工位占用率。电子屏用来做宏观管理、判断园区挤不挤;工位占用率则用来决定要不要把人调到空闲工区。你不能拿园区总人数去指挥具体某个人坐哪个工位,调度器也不能拿 loadavg 去做单核迁移决策。
所以,当有人说“CPU 负载均衡”时,要分清楚他是在说内核的 task scheduler load balance,还是系统级 loadavg。这两个概念经常被混着聊,但它们的数据来源、计算周期、用途都完全不同。
4. 实操:loadavg 飙高时如何一步步定位真凶
4.1 先给机器做一次“分诊”
loadavg 高不等于 CPU 忙,所以我拿到一台负载高的机器,第一件事不是看 loadavg 本身,而是先把它和 CPU 使用率、阻塞任务数放在一起看。
| 观测组合 | 初步判断 |
|---|---|
loadavg 持续大于核数,us+sy也很高 | CPU 真饱和,任务量超过处理能力 |
loadavg 高,CPU idle 高,wa高 | IO 阻塞,大概率有大量 D 状态任务 |
loadavg 高,CPU idle 高,wa也不高 | 锁竞争、内核路径卡顿、网络存储异常等 |
loadavg 低于核数,但 CPUus+sy高 | 少数任务吃满 CPU,但还没形成明显排队 |
这套组合判断看起来很简单,实际排障时特别有用。很多问题一开始就是靠这个“分诊”缩小范围的。
4.2 用命令把证据固定下来
建议按顺序执行下面几组命令,每一条都有它要看的东西。
uptime vmstat 1 5 mpstat -P ALL 1 3 ps -eo state,pid,comm | awk '$1 ~ /D/ {print}' iostat -x 1 3vmstat的r列是当前运行队列上的任务数,b列是阻塞在不可中断状态的任务数,wa列是 CPU 等待 IO 的时间占比。如果b高而wa不高,说明阻塞可能不在块设备上,而在别的内核路径上。
ps那一条是快速找出 D 状态进程。D 状态任务无法用信号杀死,如果它们一直不消失,就说明是某个 IO 或内核等待迟迟没有完成。iostat -x则会给出每个磁盘的avgqu-sz(平均队列长度)和%util,用来确认存储层是不是已经堵死。
4.3 一个现场还原:CPU 不忙,负载却近 40
有次线上机器 8 核,loadavg 到了 38,top里总体 CPU 使用率还不到 25%。刚开始我以为是采集工具出了问题,但vmstat一看,r只有 12,b却有 28,wa达到 35。ps过滤 D 状态任务,发现几十个线程都卡在同一个日志落盘路径上。再用iostat -x看,某块磁盘avgqu-sz已经 50 多,%util接近 100。
这个案例里 loadavg 高不是因为 CPU 不够,而是因为大量任务被不可中断地等在一块慢磁盘上。后来把日志写入改成缓存批量刷、降低同步落盘频率,D 状态任务慢慢消失,loadavg 才跟着降下来。耗时比想象中久,因为 15 分钟均值带记忆,即使阻塞已消除,它也要数分钟才能把旧压力“忘掉”。
4.4 容器和云主机里 loadavg 的坑
容器里的/proc/loadavg在很多场景下其实是宿主机级别的负载。也就是说,你容器只用了 0.5 核,但宿主机上其他容器把负载拉高,你在容器里看到的 loadavg 照样会很高。这时候不能拿它判断自己的容器是否健康。
更靠谱的是看 cgroup v2 的 CPU 统计或 PSI(Pressure Stall Information)指标,比如cpu.pressure里的some和full,它们直接反映任务因为等待 CPU 而被延迟的时间比例。云主机还要注意超分和多核关系,nproc返回的是逻辑 CPU 数量,超分严重时,实际算力比核数打折扣,看阈值也要更保守。
5. 高频问题速查:loadavg 使用中常见的坑
5.1 为什么 loadavg 高,CPU 使用率却很低?
最典型的原因是 D 状态任务。它们被算在 loadavg 的 active 计数里,但并没有在 CPU 上执行,所以 CPU 使用率上不去。常见场景包括磁盘 IO 拥塞、网络文件系统挂起、内存回收风暴等。看到这种组合,先去找 D 状态进程,再顺着进程的内核栈去看它在等什么。
5.2 loadavg 超过核数就代表系统崩溃吗?
不一定。loadavg 它衡量的是“排队+阻塞”的任务水位,超过核数只能说明已经出现了排队,不一定是不可用。比如 4 核机器 loadavg 5,可能是短暂的高峰,也可能是 1 个任务阻塞 IO 加上 4 个任务在排队。需要结合持续时间、响应时间、CPU 使用率一起判断。
但如果长期超过核数 2 倍以上,问题基本是确定的:系统在相当长一段时间内都处于过载或拥堵状态。不要用“loadavg 可以超过核数”来安慰自己,那只是说明有这个现象,不代表可以无视。
5.3 为什么高峰过后 loadavg 降得那么慢?
因为指数加权平均值里的旧值不会消失,只是按比例衰减。比如 15 分钟均值的衰减系数约 0.995,即使活跃任务数已经变成 0,每一轮 5 秒采样也只会把旧值乘以 0.995。5 分钟之后它还能剩下大约 70% 的旧水平,15 分钟之后才降到 40% 左右。这个设计是为了平滑长周期趋势,代价就是对“突然安静下来”这种事反应迟钝。
5.4 loadavg 能不能用内核参数调低?
不行。loadavg 是观测结果,不是可以直接设置的参数。你能做的只有减少可运行任务数和不可中断任务数,比如优化代码、减少锁竞争、换更快的磁盘、调整线程池大小、降低 IO 阻塞。把进程 nice 值调低也不会改变 loadavg 的统计口径,它同样能被算进活跃任务数。
线程也会被当作任务计数。一个进程起 200 个线程并且大部分处于 Runnable 或 D 状态,它一样会让 loadavg 变得很高。排查时不要只看进程数,要看/proc下的线程状态。
我个人现在的习惯是:看 loadavg 一定会搭配vmstat的r、b和top的 D 状态一起看。只看一个数字,永远没法还原真实场景;把计算口径想清楚了,再去解释现象,才不会一看到 loadavg 高就急着加机器,结果加了机器之后发现 CPU 需求根本没变。