首页 / 资讯中心 / 文章详情

Linux loadavg全解:内核计算原理、高负载排查与容器陷阱

Linux loadavg全解:内核计算原理、高负载排查与容器陷阱 ★ FEATURED ARTICLE
服务器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核老机器loadavg3已经明显过载而一台96核机器loadavg30可能还非常健康。这一点会在后面的章节反复出现因为它关系到几乎所有loadavg相关的误判。2. 内核计算机制拆解从runqueue到指数滑动平均2.1 活进程统计谁会被算进loadavg内核计算loadavg用的核心采样值是“活跃任务数”由两部分组成nr_running加上nr_uninterruptible。nr_running是各CPU运行队列runqueue里可运行任务的总数包括当前正在CPU上运行的任务和已经就绪、正在排队等待CPU的任务。注意Linux的loadavg统计是所有优先级、所有nice值一视同仁的普通进程、实时进程、nice19的进程只要处于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秒采样一次活跃任务数然后用下面这个公式更新三个loadavgnew_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.08015分钟EXP_5 20142014/2048 ≈ 0.983434/2048 ≈ 0.016615分钟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/Osar -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核机器loadavg30压力系数0.31看着数字吓人其实是轻载4核机器loadavg6压力系数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 $2D结果发现十几个进程卡在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 $2D # 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限额的容器里看到loadavg8这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在容器里不可靠该用什么我的经验是优先看两组数据第一组是PSIPressure Stall Information在cgroup v2里可以通过/sys/fs/cgroup/cpu.pressure读取它统计的是因为这个cgroup内的任务排队而导致的“时间压力占比”输出类似cat /sys/fs/cgroup/cpu.pressure some avg1012.34 avg608.56 avg3003.21 total12345678some表示至少有一个任务在等待资源的时间占比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变成01分钟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侧这两个问题回答清楚了问题基本解决了一半。这套判断方法帮我避免了很多次半夜上线扩容的尴尬也希望对你排查下一轮故障有帮助。
阅读完成 · 觉得有帮助?
咨询建站