☰
Linux loadavg全解:内核计算原理、高负载排查与容器陷阱
2026/10/16 7:35:09 网站建设 项目流程

服务器CPU告警了,我登录上去按惯例敲了下uptime,看到load average: 8.23 7.15 6.08,紧接着打开top按P排了个序——结果CPU占用最高的进程也才40%,整机CPU使用率30%出头。这个场景我见过太多次了:指标爆了,资源却很闲,到底是机器在说谎还是监控在说谎?

答案是两边都没说谎,只是loadavg这个指标经常被读错。它是Linux上曝光率最高、误读率也最高的性能指标之一,既不是CPU使用率,也不是简单的“排队任务数”。这篇文章我想把loadavg的来龙去脉一次讲透:内核到底怎么取样、怎么用指数滑动平均把瞬时值变成一个带“惯性”的负载曲线、/proc/loadavg里每个字段该怎么读,以及高负载排查和容器环境下最容易踩的坑。适合所有用过top/uptime但没细究过loadavg原理的开发、运维和SRE,也适合想搞懂调度器为什么“负载均衡”却经常报出不平衡数字的内核爱好者。

1. 先搞清楚:loadavg到底在衡量什么

1.1 不要把loadavg当成CPU使用率

很多人一看到load average: 1.5就以为CPU用了150%。这是最常见的误解。CPU使用率表示的是“时间占比”,比如一秒内有0.7秒在干活,使用率就是70%。而loadavg表示的是“任务数量”,准确说是:在一段统计窗口内,处于可运行状态或在不可中断睡眠状态的任务数的滑动平均。

用生活化的例子理解:假设一家奶茶店只有一个柜台(1个CPU核)。CPU使用率是“柜台小哥一分钟内有几秒在忙”,而loadavg是“柜台前排队等待制作+正在制作中的奶茶总数”。这两者虽然相关,但根本不是一回事。

再深入一点:一群任务排队等CPU,和一群任务排队等磁盘I/O,都会让loadavg上升,但CPU使用率可能一个在高位、一个在低位。所以loadavg本质上是“系统里有多少活儿在等着资源干”,它衡量的是一种竞争压力,不是资源本身的忙碌程度。

1.2 1分钟/5分钟/15分钟三个数字各看各的门道

uptime输出里三个数字分别代表1分钟、5分钟、15分钟的loadavg。但这里有个容易忽略的细节:这三个数不是“最近1分钟的平均任务数”“最近5分钟的平均任务数”这种简单窗口平均,而是三个不同衰减速度的指数加权移动平均(EWMA)。

EWMA的特点是:越靠近当前时刻的采样权重越大,越久远的采样权重指数级衰减。这意味着1分钟load对突发负载反应最快,5分钟居中,15分钟最迟钝。

实际使用中这三者的关系非常有信息量:

  • 1分钟 > 5分钟 > 15分钟:负载正在快速上升,系统刚经历或正在经历一次突增。
  • 1分钟 < 5分钟 < 15分钟:负载在回落,之前的峰值已经过去,系统在缓慢恢复。
  • 三者基本持平且都偏高:系统已经处于持续高位运行,不是瞬时毛刺。

举个例子,一台8核机器平时loadavg在1左右,突然某条业务链路出问题,1分钟load瞬间飙到12,但15分钟load可能还纹丝不动地停在1.2。如果你只看15分钟值,就会错过这次急性故障;反过来,如果你只看1分钟值,又会把很多正常的小抖动误判成事故。三个数字一起看,才能真正区分“趋势”和“毛刺”。

1.3 用队列模型建立判断直觉

把loadavg当作一个排队系统来理解会直观得多。假设系统有N个CPU核,同时最多有N个任务真正在“被服务”。loadavg等于N,意味着任务到达速率等于服务能力,排队长度趋于稳定但CPU已经没有余量;loadavg远小于N,说明大多数时候CPU都在歇着;loadavg显著大于N,说明任务到达速度超过了处理速度,队列正在堆积,任务的等待时间会明显变长。

所以判断系统是否过载,永远要拿loadavg和CPU核数对比,而不是看绝对值。一台2核老机器loadavg=3已经明显过载,而一台96核机器loadavg=30可能还非常健康。这一点会在后面的章节反复出现,因为它关系到几乎所有loadavg相关的误判。

2. 内核计算机制拆解:从runqueue到指数滑动平均

2.1 活进程统计:谁会被算进loadavg

内核计算loadavg用的核心采样值是“活跃任务数”,由两部分组成:nr_running加上nr_uninterruptible。

nr_running是各CPU运行队列(runqueue)里可运行任务的总数,包括当前正在CPU上运行的任务和已经就绪、正在排队等待CPU的任务。注意,Linux的loadavg统计是所有优先级、所有nice值一视同仁的,普通进程、实时进程、nice+19的进程只要处于R状态都算数,这点和某些商业Unix只在部分条件下计入有差异。

nr_uninterruptible是处于不可中断睡眠状态(D状态)的任务数。这个设计很关键,也是很多loadavg玄学的来源:一个任务正在等磁盘I/O、等NFS响应、或者在等某些内核锁时,它虽然不占CPU,但它依然是“活跃”的,因为它占着资源、堵着流程。把它计入loadavg,才能反映出“系统整体是否顺畅”,而不只是CPU是否吃饱。

那么什么不算?S(可中断睡眠)、T(停止)、Z(僵尸)状态的进程都不算。也就是说,你起了10000个线程但都在sleep等网络请求,loadavg不会因为这些线程而上涨;反过来,哪怕只有10个线程卡在D状态,loadavg也可能飙升。

2.2 指数滑动平均机理:为什么loadavg有“惯性”

内核每5秒采样一次活跃任务数,然后用下面这个公式更新三个loadavg:

new_load = old_load × 衰减系数 + 当前活跃数 × (1 - 衰减系数)

三个窗口的衰减系数不是拍脑袋定的,它对应着指数函数exp(-采样间隔/窗口长度)。采样间隔是5秒,窗口长度分别是60秒、300秒、900秒,于是得到三组理论衰减系数:exp(-1/12)≈0.9200、exp(-1/60)≈0.9835、exp(-1/180)≈0.9945。

内核把这些系数转成定点数存在代码里:

窗口内核常量衰减系数每次新采样的贡献比例
1分钟EXP_1 = 18841884/2048 ≈ 0.9199164/2048 ≈ 0.0801
5分钟EXP_5 = 20142014/2048 ≈ 0.983434/2048 ≈ 0.0166
15分钟EXP_15 = 20372037/2048 ≈ 0.994611/2048 ≈ 0.0054

每次更新时,旧值乘以一个接近1的系数被“打折”,新采样值只按很小的权重加进来。这就是loadavg“惯性”的来源:它永远在追赶真实值,但永远不可能瞬间到位。

我算过一个具体例子方便你建立感觉:假设一台机器的活跃任务数从0瞬间变成16,此后保持不变。1分钟loadavg大约需要25到30次采样(也就是2分多钟)才能爬到16的九成左右;5分钟loadavg要爬十几个采样才开始明显动;15分钟loadavg前几分钟几乎不动。所以刚发生故障的头一两分钟,15分钟loadavg完全“没反应”是正常的,不是监控坏了。

2.3 FIXED_1与FSHIFT:内核里的定点数玩法

内核代码里看不到浮点数,因为浮点运算在核心路径上开销大、不同架构行为还不一致。loadavg用的是定点数技巧:一个整数被拆成“整数部分+小数部分”,用固定位数保存小数。

相关的一组宏在kernel/sched/loadavg.c里,简化后长这样:

#define FSHIFT 11 /* 小数部分占11位 */ #define FIXED_1 (1 << FSHIFT) /* 即2048,代表1.0 */ #define LOAD_FREQ (5 * HZ + 1) /* 5秒+1个tick采样一次 */ #define EXP_1 1884 /* 1/exp(5sec/1min) 定点表示 */ #define EXP_5 2014 /* 1/exp(5sec/5min) 定点表示 */ #define EXP_15 2037 /* 1/exp(5sec/15min) 定点表示 */ #define CALC_LOAD(load, exp, n) \ load *= exp; \ load += n * (FIXED_1 - exp); \ load >>= FSHIFT;

FIXED_1 = 2048表示定点数中的1.0,也就是整个数值被放大了2048倍。衰减系数1884实际就是0.9199 × 2048取整。每次计算先乘衰减系数,再加上新采样贡献的部分,最后右移11位把放大的倍数除回来。

这里的核心窍门是:所有中间运算都发生在整数域,高位数保存整数部分,低11位保存小数精度。内核里全局的avenrun[0/1/2]就是从这套计算里产出的,用户态看到的/proc/loadavg读的就是这个数组。

提示:CALC_LOAD宏里的load和n相乘时可能溢出,所以内核在调用时用的变量类型是64位的。你如果在用户态模仿这套算法,一定要用long long或更高精度类型,别用32位int硬算。

2.4 为什么偏偏是5秒一采样

LOAD_FREQ = (5 * HZ + 1),HZ是内核时钟频率(通常是250或1000),所以大约每5秒多一点点采样一次。这里有两个细节值得说:

第一,5秒的周期是和指数衰减公式匹配的。1分钟窗口除以5秒采样间隔正好12次,5分钟窗口60次,15分钟窗口180次。采样间隔太长,短时负载会被抹平;采样间隔太短,单次采样的噪声权重又太高。5秒是一个在“响应性”和“稳定性”之间调出来的折中。

第二,那个+1不是笔误。它是为了防止内核的定时采样和其他同样以5秒为周期的系统事件(比如进程记账、各种定时器)锁在同一时刻触发,从而避免采样值周期性“撞车”产生偏差。多等1个tick,把采样点错开,是内核里常见的小技巧。

在实际代码路径里,每个时钟tick都会把活跃任务数的变化累加到一个全局计数器上,等到满5秒周期,再从各CPU运行队列“折叠”汇总活跃数,统一更新avenrun[]三个值。整个过程的意图是:让统计周期稳定、开销可控,同时不让瞬时波动过多污染三根曲线。

3. 读数据与实操观测:从/proc/loadavg到性能工具

3.1 /proc/loadavg每个字段逐个拆

Linux下/proc/loadavg是loadavg的直接出口,五行中只有一行,字段却比大多数人以为的多。先看输出:

cat /proc/loadavg 1.52 1.01 0.93 3/421 88432

五个部分分别是什么:

  • 前三个数:1分钟、5分钟、15分钟loadavg,即上面的avenrun[0..2]换算成小数后的值。
  • 第四个字段:3/421,分子是“当前这一刻”处于可运行状态的任务数(瞬时值,对应nr_running的当前读数),分母是系统里总的任务/线程数。这个分子跟前面三个loadavg没有直接换算关系,一个是被采样后再做指数平滑的结果,一个是按下瞬间的原始快照。
  • 第五个数:系统最近创建的一个进程的PID,有时候可以当“进程创建活跃度”的参考。

很多人把第四个字段的分子当成“现在有几个进程在跑”,这是不对的。它只统计R状态任务,D状态任务不计入分子,但会通过nr_uninterruptible影响前面的loadavg。所以会出现一个很反直觉的现象:系统里一堆D状态任务卡着,3/421里分子可能还是3,前面的loadavg却很高。

3.2 top、uptime、sar、vmstat各自的“load”都不一样

搞清楚一个易混淆点:top、uptime、w、sar -q显示的loadavg都来自同一个/proc/loadavg,数值一定一样,只是排版不同。真正的差异出现在另一类工具上:

vmstat 1输出的第一列r表示的是“当前可运行任务数”的瞬时值,也就是上面3/421里的分子,它每秒刷新,完全不经过指数平滑。当你看到r列突然冲到很高、但loadavg还慢慢爬时,说明这是刚发生的突发;反过来,loadavg高但r列正常,说明高位loadavg是历史惯性残留,或者大量任务处于D状态。

mpstat -P ALL看的是每核CPU使用率,iostat和iotop看的是I/O,sar -q则是把loadavg和运行队列长度一起归档做历史趋势。排查时不要只盯一个工具,vmstat的r列负责“即时现场”,loadavg三值负责“趋势方向”,CPU使用率负责“是否CPU瓶颈”,三者各司其职。

3.3 用“相对基线法”替代拍脑袋阈值

监控告警里最常见的错误做法是给loadavg设一个绝对阈值,比如“大于4就告警”。这套规则在4核机器上可能合理,换到64核机器上就变成永远不告警,再换到2核小机器上就变成天天告警。

我的建议是建立相对基线:

  • 第一步,用nproc确认机器核数,loadavg所有的解读都必须基于这个数。
  • 第二步,把loadavg除以核数,得到“压力系数”。系数低于0.7算舒适区,接近1说明满载,超过1说明已经在排队。
  • 第三步,结合1分钟和15分钟的差值判断性质:1分钟远高于15分钟是急性突发,持续高于15分钟是慢性过载。

举个例子,96核机器loadavg=30,压力系数0.31,看着数字吓人,其实是轻载;4核机器loadavg=6,压力系数1.5,这才是真过载。不看核数聊loadavg,基本等于裸奔。

4. 高负载排查实录:从数字到根因的完整路径

4.1 高loadavg但CPU很闲:D状态进程的典型现场

这是loadavg最著名的“灵异事件”。我遇到过一次典型的线上故障:机器loadavg飙到20,但top里CPU使用率不到20%,CPU空闲一大片,页面卡顿、接口超时。

当时我用常规思路先查了CPU占用最高的进程,一无所获。后来换成查D状态进程:

ps -eo pid,ppid,stat,wchan:32,comm | awk '$2=="D"'

结果发现十几个进程卡在D状态,wchan列指向的等待点都集中在块设备I/O相关路径上。再配合iostat -x 1一看,某块盘的%util接近100%,await高达几百毫秒。根因是那块盘在高峰期扛不住写压力,大量进程堵在I/O等待上。CPU很闲,因为活儿全卡在磁盘队列里,而loadavg因为把D状态任务计入活跃数,忠实地把“系统不顺畅”反映了出来。

这个案例的教训是:看到loadavg高,先别急着假设是CPU不够,先花十秒钟确认一下D状态进程有多少。

4.2 CPU和I/O双高:运行队列过深的处理思路

与上一种情况对称的场景是:loadavg高、CPU使用率也高、vmstat的r列长期大于核数。这说明任务确实在抢CPU,运行队列一直在堆积。常见根因有几类:

  • 线程数远超CPU核数,大量线程同时进入R状态,典型如连接池、线程池配置过大而业务逻辑又是纯CPU密集型的。
  • 频繁上下文切换和锁竞争,CPU时间大量消耗在调度和自旋上,表现为vmstat的cs列高得离谱。
  • 单机混合部署,多个应用互相抢占CPU,高峰期互相拖累。

处理顺序是先看vmstat 1 10整体判断,再pidstat -u 1按进程拆CPU消耗,必要时perf top看内核热点是否在调度器或锁上。如果是线程池过大,适当压缩并行度反而吞吐更高;如果是锁竞争,解决方向是拆分锁、减少共享,而不是加CPU。

4.3 一套可复用的排查命令清单

把前面几次实战的命令整理成一套固定动作,每次loadavg告警我基本按这个顺序过:

# 1. 先看趋势和核数 uptime && nproc # 2. 看即时运行队列与CPU分布 vmstat 1 10 # 3. 按核观察CPU是否均匀 mpstat -P ALL 1 3 # 4. 找CPU消耗大户 top -b -n 1 | head -30 # 5. 抓D状态进程 ps -eo pid,ppid,stat,wchan:32,comm | awk '$2=="D"' # 6. 有D状态再看IO iostat -x 1 3 # 7. 看CPU内部时间和锁竞争 pidstat -u 1 5

这套步骤的核心逻辑是先分“大类”:是CPU忙、D状态多、还是两者都高。大类确定后再往进程、锁、I/O设备逐层下钻,基本不会跑偏。

注意:wchan列有时候显示为-,说明进程当前没有处在明确的内核等待函数上,这时候要看D状态是间歇出现还是持续出现,间歇出现往往是I/O抖动,持续出现要重点怀疑磁盘故障或网络文件系统。

5. 容器化环境下的loadavg陷阱与避坑经验

5.1 容器看到的是宿主机的loadavg

容器技术普及之后,loadavg的误读又增加了一个重灾区:在容器里执行uptime,看到的loadavg其实是宿主机的全局loadavg,不是这个容器自己的负载。

原因很简单,/proc/loadavg在内核侧是全局的,avenrun[]统计的是整个系统所有CPU运行队列和所有D状态任务,它没有按cgroup或容器做隔离。你在一个只分配了1个CPU限额的容器里,看到loadavg=8,这8可能绝大部分来自宿主机上其他邻居容器的业务。

所以千万别用容器里的loadavg去做弹性伸缩或自动扩缩容判断,那是拿别人的负载惩罚自己。很多容器平台后来提供了替代指标,比如cgroup v2的cpu.stat和PSI,它们才是按cgroup维度统计的。

5.2 CPU绑核与亲和性对loadavg的影响

就算容器只绑定了2个物理核,宿主机是32核,那么loadavg的“参考核数”依然是32,而不是2。因为loadavg的活跃任务数来自全系统调度域的所有运行队列,不存在“只统计某几个CPU”的模式。

这就造成一个奇特现象:容器内业务完全跑满绑定的2个核,但容器里看loadavg可能只有0.5,因为宿主机的其他30个核都在闲着,把平均值拉低了。反过来也成立,宿主机上其他容器打满CPU,你的容器里loadavg也会跟着涨。综上,容器场景下loadavg既不是“你的负载”,也不是“你的容量”,它只是一个倒霉的全局背景噪声。

5.3 cgroup层面的替代指标:PSI与配额

既然loadavg在容器里不可靠,该用什么?我的经验是优先看两组数据:

第一组是PSI(Pressure Stall Information),在cgroup v2里可以通过/sys/fs/cgroup/cpu.pressure读取,它统计的是因为这个cgroup内的任务排队而导致的“时间压力占比”,输出类似:

cat /sys/fs/cgroup/cpu.pressure some avg10=12.34 avg60=8.56 avg300=3.21 total=12345678

some表示至少有一个任务在等待资源的时间占比,avg10/60/300就是10秒、60秒、300秒窗口的平滑值。它是百分比语义,天生适合做阈值告警,而且是per-cgroup的,比loadavg可靠得多。

第二组是CPU配额统计,cpu.stat里的nr_periods和nr_throttled可以算出CPU被限流(throttle)的周期比例。如果容器频繁被限流,说明配额设置不合理,这时候loadavg即使不高,业务延迟也会直线上升。这两个指标配合起来,才能真正回答“容器够不够用”的问题。

6. 常见问题速查与个人心得

6.1 loadavg到底等于几才算正常

这个问题没法脱离核数回答,但可以给一张速查表:

压力系数(loadavg/核数)含义建议动作
< 0.5明显轻载无需处理
0.5 ~ 0.7舒适区常规监控
0.7 ~ 1.0接近满载关注趋势,排查是否有突发
1.0 ~ 1.5已经排队结合延迟决定是否扩容
> 1.5明显过载立即定位根因

注意这只是经验值,不适用于所有业务。批处理任务短时间冲到1.5倍核数可能无所谓,线上低延迟接口0.8倍核数就可能已经不可接受了。所以正确的做法是:先按这个表建立量级概念,再结合自己业务的延迟指标校准告警阈值。

6.2 为什么kill掉高CPU进程后loadavg还是降得很慢

这是新手最常问的问题之一。因为loadavg是指数滑动平均,它带着历史记忆。哪怕活跃任务数瞬间从100变成0,1分钟loadavg也要经历一个多2分钟的衰减过程才能降下来,5分钟和15分钟就更慢。具体衰减速度可以参考前面的系数:1分钟load每一次5秒采样大约保留92%旧值,5分钟保留98%,15分钟保留99.5%。所以看到loadavg“滞后”很久,不是没生效,是数学上就该这样。

另外一个相关问题是:为什么重启后loadavg不是0?新内核启动后avenrun初始确实是0,但如果你看到的不是0,八成是因为用的工具本身缓存了旧数据,或者你的监控图时间轴还没刷新。内核“冷启动就带负载”这种直觉在Linux里是不成立的。

6.3 几个值得写进监控体系的经验

最后分享几个我踩过坑之后沉淀下来的习惯,不一定都写在文档里,但很实用:

第一,告警一定要做成“相对核数”的。哪怕脚本里写一行loadavg阈值 = 核数 × 1.2,也比拍脑袋定死值强得多。第二,loadavg高时先花30秒看vmstat的r和b两列,r高是CPU侧问题,b高是I/O侧问题,这一步能少走一半弯路。第三,容器监控永远不要直接用loadavg,换成PSI和限流统计,否则扩缩容决策一定会被宿主机邻居干扰。第四,loadavg只是“CPU竞争压力”的代理指标,它不代表业务健康度,锁等待、网络延迟、内存换页导致的问题loadavg经常反映不出来,必须配合延迟、错误率一起看。

我自己现在的习惯是:每次看到loadavg升高,先问两个问题——它是趋势还是毛刺?它是CPU侧还是I/O侧?这两个问题回答清楚了,问题基本解决了一半。这套判断方法帮我避免了很多次半夜上线扩容的尴尬,也希望对你排查下一轮故障有帮助。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询