做后端和运维的人,几乎每天都会敲uptime或top,然后看到一行load average: 2.50, 2.30, 2.10之类的东西。但你要是追问一句:这个 2.50 到底是怎么算出来的?能答清楚的人其实不多。更常见的说法是“服务器 CPU 有点高”,可 loadavg 从来就不是 CPU 使用率。它描述的是整个系统里,处于可运行状态和不可中断睡眠状态的任务排队长度。这个话题和 CPU 负载均衡有关系,但关系又没有你以为的那么直接。这篇文章就从 CPU 负载均衡这个大背景出发,把 loadavg 的统计口径、采样方式、指数衰减算法、内核计算链路全部摊开讲一遍,最后再给出一套实际排查时可以照着用的方法。适合正被负载告警困扰的运维同学,也想帮后端开发者补上调度器相关的底层认知。
1. 先纠正一个误区:loadavg 量的是排队,不是占用率
1.1 CPU使用率是“采样占比”,loadavg是“平均队列长度”
很多人把 loadavg 当成 CPU 使用率来看,这是最容易踩的坑。CPU 使用率是一个时间占比,比如 100 个采样周期里,CPU 有 70 个周期在忙,那使用率就是 70%。它回答的问题是“CPU 这秒干了多少活”,但它不回答“有多少活正在等着干”。
loadavg 回答的恰恰是后者。它是一个队列长度,或者说是一个并发等待数量。假设你有一台 4 核机器,loadavg 是 4.0,通俗地理解就是:平均每个 CPU 核心上有 1 个任务在跑,队列没有积压。如果 loadavg 是 8.0,那就意味着平均每个核心上有 1 个任务在跑、还有 1 个任务在排队等着。
可以用餐厅出餐来类比。CPU 使用率是“厨师手上忙活的比例”,他可能一刻不停地在炒菜,那是 100% 使用率;loadavg 是“餐厅里等着取餐的顾客数”。有时候厨师很忙,但出餐效率高,排队的人不多;有时候厨师看起来没怎么忙,出餐口却堆了一大堆单子,因为后厨在等食材、等清洗、等某个慢工位的设备。系统负载也是这个道理。
loadavg 之所以重要,是因为它捕捉到了“等待中但还没被服务”的工作量。这部分工作量是潜在的吞吐压力,对容量规划的意义比 CPU 使用率更敏感。两个 CPU 使用率相同的系统,负载可能完全不一样,因为它们内部积压的任务量不同。
1.2 只有两种状态的进程会被统计进去
Linux 里进程的状态有好几种,但 loadavg 并不是所有的都算。严格来说,它统计的是两类:
| 进程状态 | 是否计入 loadavg | 典型场景 |
|---|---|---|
| TASK_RUNNING(可运行) | 计入 | 正在 CPU 上执行,或者在 runqueue 里排队等待调度 |
| TASK_UNINTERRUPTIBLE(不可中断睡眠) | 计入 | 等待磁盘 I/O、网络文件系统、内核锁等不可被打断的路径 |
| TASK_INTERRUPTIBLE(可中断睡眠) | 不计入 | 等 socket、等锁、sleep 等可以被信号唤醒的等待 |
| TASK_STOPPED / TASK_TRACED | 不计入 | 进程被暂停或调试跟踪 |
| TASK_ZOMBIE | 不计入 | 已退出但未回收的僵尸进程 |
这里最关键的是 TASK_UNINTERRUPTIBLE,也就是我们常说的 D 状态。为什么这个状态要计入负载?因为它表示一个任务卡在内核路径里出不来,可能是磁盘 I/O 没返回,可能是网络文件系统的 RPC 一直等不到结果。这种等待会阻塞依赖它的线程,并且持有内核资源,对整个系统的吞吐量是实打实的消耗。所以内核把这个状态算进负载,是为了避免“CPU 没事干但系统其实已经堵死”的情况被掩盖掉。
TASK_INTERRUPTIBLE 不计入,是因为这种睡眠原则上可以被信号唤醒,等待期间不占用系统资源,对吞吐量没有直接压力。一个进程在sleep(100),你可以把它看成“请了假的人”,不应该算进排队的队伍里。
1.3 多核机器上负载数字的正确读法
负载值是一个绝对值,但判断它是否异常必须结合 CPU 核数,不然容易闹笑话。同样的load average: 2.00,在双核机器上已经是满载,在 16 核机器上还非常空闲。
- 4 核机器,loadavg 1.0:平均每个核只占了 25%,系统余量很大。
- 4 核机器,loadavg 4.0:理论上正好满载,所有核都在忙且队列不长。
- 4 核机器,loadavg 8.0:平均每个核有 1 个任务在跑,同时还有 1 个任务在等,属于明显过载。
但这里要加一个前提:要看持续趋势,而不是瞬时数字。负载值是平滑后的结果,如果只是几秒钟的突发,一分钟负载冲到 6,只要 5 分钟和 15 分钟负载不高,一般不用太紧张。真正要处理的是那种 1、5、15 分钟三个值全部高于核数,或者持续走高不回落的情况。
2. 为什么 loadavg 是1/5/15三个窗口,而不是实时值
2.1 瞬时值太容易骗人
你可能也好奇过:为什么 loadavg 不直接显示“当前这一刻的 runqueue 长度”?原因是瞬时值抖动太厉害。如果你用脚本每 10 毫秒读一次运行队列长度,会看到一条疯狂跳动的曲线:一个任务刚被唤醒时队列可能是 0,下一秒所有核上同时有任务到期,队列瞬间变成几十。
这种锯齿状的数据拿来监控,会给告警系统制造海量噪音。负载的意义在于趋势判断,不在于捕捉每一个瞬时尖峰。所以内核选择了平滑算法,把历史值加权混合进来,让数字更有“惯性”。
2.2 指数衰减平均是这里的正统解法
loadavg 用的不是简单移动平均。简单移动平均是把过去一段时间的数据做等权平均,比如算最近 60 秒的平均。它的问题在于:60 秒前的数据点,对现在的参考意义其实已经很小,等权平均会显得很钝。
内核用的是指数衰减平均,核心公式是:
load_new = load_old * exp(-Δt / T) + active * (1 - exp(-Δt / T))拆开看就两部分:旧负载乘以一个小于 1 的衰减系数,新采样到的活跃任务数乘以一个权重,两者相加得到新的负载值。T是时间窗口,60 秒、300 秒、900 秒分别对应 1 分钟、5 分钟、15 分钟。Δt是内核更新的间隔,通常是 5 秒。
这个公式的生活化类比是汽车速度表:显示的不是瞬时速度,而是带有阻尼的平均速度。你踩油门时数字不会瞬间跳到 120,松开油门时数字也不会立刻掉到 0,它会按一个指数曲线过渡。系统负载也是一样,它用指数曲线逼近真实的活跃任务数。
2.3 三个窗口的真实衰减节奏
我用一个具体表格展示三个窗口的差异,这样你对“为什么 1 分钟负载和 15 分钟负载差很多”会有直观感受。假设每 5 秒内核更新一次:
| 窗口 | 每步衰减系数 | 从 0 上升到目标值的 63% 所需时间 | 用途 |
|---|---|---|---|
| 1 分钟 | 0.9200 | 约 70 秒 | 捕捉近期突发,适合告警 |
| 5 分钟 | 0.9835 | 约 350 秒 | 反映最近几分钟的持续状态 |
| 15 分钟 | 0.9945 | 约 1050 秒 | 过滤掉短期噪声,适合观察长期趋势 |
所谓 63%,是指数衰减的典型特征:经过一个时间常数,系统能完成约 63% 的变化;经过 2 个时间常数,完成约 87%;经过 3 个时间常数,完成约 95%。所以一个 1 分钟的负载读数,其实在 70 秒前就已经开始提前反映数据变化了,并不是真的把最近 60 秒做一次算术平均,而是“过去 70 秒左右的数据权重特别大”。
下面我用一段简单的 Python 模拟,让你直观看到同样的活跃任务数变化,在三个窗口上的表现差异:
import math def ema(initial, active, dt, window): decay = math.exp(-dt / window) return initial * decay + active * (1 - decay) load1 = load5 = load15 = 0.0 dt = 5.0 # 前 20 个周期 active=0,然后跳变到 8,持续克隆 for i in range(80): active = 8 if i >= 20 else 0 load1 = ema(load1, active, dt, 60) load5 = ema(load5, active, dt, 300) load15 = ema(load15, active, dt, 900) if i % 10 == 0: print(f"t={i*5:4d}s load1={load1:.2f} load5={load5:.2f} load15={load15:.2f} active={active}")跑一下你就会发现,active 跳到 8 之后,1 分钟负载很快冲到 6 以上,5 分钟负载还很慢,15 分钟负载更是几乎还在原地。这就是不同窗口的意义:1 分钟负责“快”、15 分钟负责“稳”。
3. 内核里 loadavg 的计算主链路:从 tick 到 /proc/loadavg
3.1 谁在维护活跃任务数
这个问题的核心是一个全局计数器:每当一个任务进入可运行状态或者进入不可中断睡眠状态,计数器加一;每当它离开这两个状态,计数器减一。你可以把它理解成一个“瞬时活跃任务数”的水位计。
每个 CPU 在调度 tick 或者任务迁移时,都会维护自己这一侧的统计增量。内核通过calc_load_fold_active这类函数把各个 CPU 的增量聚合到全局变量calc_load_tasks上。每 5 秒,calc_global_load读取这个值,作为本次采样的活跃任务数,再做指数平均。
这里有个小细节值得注意:内核里定义的更新周期常数不是简单写死一个 5 秒,而是类似LOAD_FREQ (5*HZ+1)的形式。5*HZ是 5 秒,后面的+1是为了让采样时刻不会和某些固定频率的时钟事件完全同步。如果采样每次都卡在同一个相位上,可能会稳定地错过某些周期性任务的变化,导致负载值系统性偏低或偏高。这个+1是典型的工程细节,很多人不会注意到。
3.2 指数平均在内核里是怎么落地的
看一下上一节的公式,里面涉及exp(-Δt/T)这种浮点指数计算。但内核不会在每 5 秒的更新路径里真的去调exp函数,一是性能开销不划算,二是在某些内核路径上做浮点运算有额外成本。所以内核用了定点数加查表的方式。
简单理解就是:三个时间窗口对应的衰减系数,在编译期就按定点数形式算好,存成一个固定数组。每次更新时只需做几次整数乘法和移位,就能得到新的负载值。这也是为什么loadavg能显示三位小数——它不是系统随便拍出来的精度,而是定点数换算成十进制的自然结果。
计算逻辑的核心可以概括为:
old_load = avenrun[i] new_active = calc_load_tasks delta = 1 - exp(-LOAD_FREQ / window) avenrun[i] = new_active * delta + old_load * (1 - delta)注意avenrun是三个元素的数组,分别对应 1 分钟、5 分钟、15 分钟。每次采样得到同一个calc_load_tasks,但三个窗口的delta不同,所以三个值走势不同。
3.3 不可中断睡眠任务为什么一定要参与计算
这可能是 loadavg 设计里最反直觉,但也最精髓的一点。如果一个任务卡在D状态,CPU 此刻确实没有在跑它,但它牢牢占用着内核资源,可能是等不到响应的磁盘命令、可能是挂在某个网络文件系统的重试逻辑上。如果 loadavg 只统计正在 CPU 上跑的任务,那系统一旦发生大规模 I/O 阻塞,负载值反而会显得很低,这会严重误导运维判断。
内核把TASK_UNINTERRUPTIBLE放进统计口径,本质上是在说:只要一个任务退出了正常的用户态执行流程,不论它是在排队用 CPU,还是在排队等 I/O,都应该算作系统负载的一部分。所以你会看到一种经典现象:CPU 使用率只有 10%,loadavg 却能到 5 甚至更高。原因就是有大量 D 状态任务在队列里堆积。
这一点在做性能分析时极其重要。拿到一个高负载,先不要默认是 CPU 过载,要拆解负载的组成成分。
3.4 /proc/loadavg 输出的到底都是什么
当我们执行cat /proc/loadavg时,典型输出是:
$ cat /proc/loadavg 0.52 0.43 0.36 2/5688 12345逐个字段拆开:
- 前三位分别是 1 分钟、5 分钟、15 分钟的负载均值。
2/5688:分子是当前处于可运行和不可中断睡眠状态的任务总数,分母是系统当前的总任务数。- 最后那串数字是系统最近一次分配的 PID,和负载没有关系,不用管它。
更细的兴趣,可以再瞄一眼/proc/stat里的procs_running和procs_blocked。它们分别表示当前可运行任务数和被阻塞在 I/O 上的任务数。这个值相当于实时快照,而/proc/loadavg是平滑后的历史值,两者对照着看,能判断负载是在上行还是在下行。
4. loadavg 和 CPU 负载均衡到底是什么关系
4.1 调度器的均衡是“现在时”,loadavg 是“过去时”
很多文章把 CPU 负载均衡和 loadavg 混在一起讲,弄得人以为它们是同一套机制,其实这是两套不同的东西,虽然名字里都有“load”。
内核调度器里的负载均衡,指的是多核环境下,各个 CPU 各自维护自己的运行队列。有的核任务多到爆,有的核闲到打盹,调度器会通过周期性均衡和空闲核拉取等方式,把任务从忙的 runqueue 迁移到闲的 runqueue。它使用的是“实时”的负载判断,喂给它的不是一个 5 秒采样一次的历史均值,而是更细粒度的调度实体负载。
loadavg 则完全不同:它是一个给人看的系统级统计信息,5 秒更新一次,做的是指数平滑,体现的是历史趋势。调度器在决定是否迁移任务时,根本不会去看/proc/loadavg。所以你可以把 loadavg 理解成调度器所有动作产生的“结果”,而不是调度器做决策的“依据”。
4.2 调度器真正使用的负载指标:per-entity load tracking
现代调度器在负载均衡上已经不简单地做“数任务个数”这种初级判断。它有一套更精细的机制,通常称为 per-entity load tracking,也就是 PELT。
这套机制会跟踪每个调度实体在一段时间内的负载贡献。注意这里的负载不是“任务数”,而是“任务的历史利用率 + 任务权重”综合出来的数值。它同样用了衰减平均的思路,但是时间尺度更短,更新频率更高,并且是和 CPU 容量、利用率估算结合在一起的。
所以你可以这么理解:调度器内部维护着每个 CPU 的“实时负载画像”,负载均衡器根据这些画像做任务迁移;而 loadavg 是内核在另一个维度维护的“系统整体排队情况”的统计。两者的采样频率、算法精度、服务对象都不同。
4.3 任务迁移是如何间接影响 loadavg 的
虽然 loadavg 不从调度器的 PELT 数据里取数,但它统计的calc_load_tasks会真实地受到任务迁移影响。当一个任务从 CPU A 迁到 CPU B,它在某个瞬间还是会处于可运行状态,全局任务数不变,所以 loadavg 不会因为这个迁移本身产生变化。
但迁移带来的后续效果会体现在负载上:任务到了更空闲的 CPU,执行速度更快,处理完就退出,runqueue 清空速度加快,全局活跃任务数下降,loadavg 自然跟着下降。反过来,如果 CPU 固定绑核,某个核上的任务排队越长,这个核处理越慢,活跃任务迟迟退不下去,loadavg 就会持续走高。
这意味着,当 loadavg 高时,我们不仅要看总负载,还要看任务在核上的分布。一个极端场景是:8 核机器 loadavg 只有 1.5,看起来不高,但第 3 核已经 100% 跑满,其他核都在摸鱼。这种就是调度器没有把任务分散开,或者进程绑核导致。看ps -eo pid,psr,comm里的 PSR 列能确认任务落在哪个核上。
4.4 一个很常见的误判:loadavg 爆表但 CPU 使用率不高
这个现象值得单独拿出来说,因为它几乎每个运维都会遇到。
假设 4 核机器,loadavg 在 6 左右,你打开 top 一看,CPU 使用率只有 20%。第一反应是“监控是不是坏了”,其实没有。问题多半出在两个地方:
一个是大量进程处于 D 状态,卡在不可中断的等待上。这时候它们不计入 CPU 使用率,但会计入 loadavg,你是真的“有负载”,只是负载不在 CPU 上,而在 I/O 路径上。
另一个是任务在锁上自旋或者频繁唤醒。它们可能每个瞬间都在可运行状态,但分配不到 CPU,或者在多个 CPU 之间来回踢皮球,闹钟一样地抢一个锁。这时候 CPU 使用率可能不高,但 runqueue 已经乱成一锅粥。
所以 loadavg 是一个“综合压力计”,它不负责告诉你压力来自 CPU、磁盘还是锁,只负责告诉你压力有多大。要判断来源,需要结合其他工具拆解。
5. 实战:拿到一个异常的 loadavg 后,完整的排查思路
5.1 先把“负载除以核数”换算成队列富余程度
不要死记“loadavg 超过 1 就是有问题”,那是在单核时代留下的习惯。现在的机器动辄几十核,正确的第一步是把负载值和核数做对比。我这里给一个经验区间:
| 核数 | 1 分钟负载 | 初步判断 |
|---|---|---|
| 4 核 | 0 ~ 2 | 健康,余量充足 |
| 4 核 | 2 ~ 4 | 偏忙,需要关注趋势 |
| 4 核 | 持续 > 4 | 满载甚至过载,需要定位 |
| 8 核 | 0 ~ 4 | 健康 |
| 8 核 | 4 ~ 8 | 偏忙 |
| 8 核 | 持续 > 8 | 满载甚至过载 |
这只是经验值,具体业务要结合响应时间来判断。核心原则是:负载持续高于核数,说明有任务在排队;低于核数,说明 CPU 资源基本够用。
5.2 用一组命令快速拆解负载来源
我习惯按下面的顺序跑:
uptime # 看 1/5/15 分钟负载趋势 vmstat 1 5 # 看 r 列和 b 列 mpstat -P ALL 1 # 看单核 CPU 使用率和是否均衡 ps -eo state,pid,comm | awk '$1=="D" {print}' # 抓 D 状态进程 top -b -n 1 | sort -k9 -r | head -20 # 看 CPU 占用排序有几个关键点需要解释。vmstat的r列表示当前可运行任务数,b列表示被阻塞在 I/O 等资源上的任务数。理论上,r + b应该接近 loadavg 当前采样的活跃任务数。如果你看到r很大,说明任务主要在抢 CPU;如果b很大,说明瓶颈在 I/O。
mpstat -P ALL 1则是看各核的分布。如果某个核被顶满而其他核很闲,就要怀疑绑核或调度配置问题。
5.3 案例 A:负载高、CPU 使用率也高,这是真的过载
一台 4 核机器,负载持续在 8 左右,vmstat里r列在 6 到 8 之间,b列接近 0,mpstat显示所有核都在 90% 以上。这种场景很明确:CPU 就是瓶颈。
处理思路也比较直接:先用top按 CPU 排序找到最耗 CPU 的进程,确认是业务高峰、代码死循环、还是正则贪婪匹配之类的经典问题。如果已经优化到位仍然过载,再考虑扩容或增加核数。这个场景里加核通常是有效的,因为等待 CPU 的任务确实多于可用的 CPU。
5.4 案例 B:负载高、CPU 使用率低,先别急着加核
另一台机器,8 核,负载到了 10,但mpstat显示 CPU 平均使用率只有 15%,vmstat里r只有 2,b却到了 6。这时候加 CPU 一点用都没有,因为任务不是没 CPU 可用,而是被阻塞住了。
下一步应该排查 D 状态进程:
ps -eo state,pid,comm | awk '$1=="D" {print}'看到具体进程后,再深入看它在等什么。可能是在等网络文件系统的响应,可能是磁盘控制器卡住,也可能是某一个内核路径在长时间持锁。如果是访问远程存储超时,通常负载会在网络抖动时同步飙升;如果是本地磁盘 I/O 饱和,iostat -x 1能看到%util已经接近 100%。
这种问题加核、加内存都无效,要把 I/O 链路和阻塞点找出来才有意义。
5.5 自己写个小实验,验证 loadavg 的衰减行为
前面讲了很多理论,最后留一个可以自己复现的实验。在机器上启动 N 个 CPU 密集的进程,让它们跑满一段时间,再用脚本记录 loadavg 的变化:
for i in $(seq 1 8); do yes >/dev/null & done然后每 2 秒抓一次/proc/loadavg的第一列:
while true; do cat /proc/loadavg | awk '{print strftime("%H:%M:%S"), $1}' sleep 2 done你会看到 1 分钟负载先快速爬升,5 分钟和 15 分钟负载慢慢跟上;执行kill %1停掉这些进程后,1 分钟负载下降也明显快于 5 分钟和 15 分钟。这就是指数衰减平滑带来的视觉差异。自己亲手复现一遍,比读十篇文章印象都深。
这套 loadavg 的计算链路,说起来并不复杂:一个 5 秒采样,一组指数衰减系数,一个把不可中断睡眠任务算进去的统计口径。但它背后能延伸出 CPU 调度、负载均衡、I/O 阻塞等多个问题,也是它在告警里高频出现的原因。我自己处理过的几次诡异负载,十次里有七八次都不是 CPU 真的过载,而是统计口径里那部分 D 状态任务在作怪。所以拿到一个异常负载值的时候,别急着给 CPU 加核,先确认这个负载到底由什么组成。调度器内部的 PELT 和 CFS 均衡逻辑,比 loadavg 更复杂也更有意思,后面有机会再单独拆开讲。