资讯详情

资讯详情

loadavg高不代表CPU忙:解密Linux负载均值与排查方法

做后端和运维的人几乎每天都会敲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 个周期 active0然后跳变到 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(ft{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/loadavg3.1 谁在维护活跃任务数这个问题的核心是一个全局计数器每当一个任务进入可运行状态或者进入不可中断睡眠状态计数器加一每当它离开这两个状态计数器减一。你可以把它理解成一个“瞬时活跃任务数”的水位计。每个 CPU 在调度 tick 或者任务迁移时都会维护自己这一侧的统计增量。内核通过calc_load_fold_active这类函数把各个 CPU 的增量聚合到全局变量calc_load_tasks上。每 5 秒calc_global_load读取这个值作为本次采样的活跃任务数再做指数平均。这里有个小细节值得注意内核里定义的更新周期常数不是简单写死一个 5 秒而是类似LOAD_FREQ (5*HZ1)的形式。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 $1D {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列接近 0mpstat显示所有核都在 90% 以上。这种场景很明确CPU 就是瓶颈。处理思路也比较直接先用top按 CPU 排序找到最耗 CPU 的进程确认是业务高峰、代码死循环、还是正则贪婪匹配之类的经典问题。如果已经优化到位仍然过载再考虑扩容或增加核数。这个场景里加核通常是有效的因为等待 CPU 的任务确实多于可用的 CPU。5.4 案例 B负载高、CPU 使用率低先别急着加核另一台机器8 核负载到了 10但mpstat显示 CPU 平均使用率只有 15%vmstat里r只有 2b却到了 6。这时候加 CPU 一点用都没有因为任务不是没 CPU 可用而是被阻塞住了。下一步应该排查 D 状态进程ps -eo state,pid,comm | awk $1D {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 更复杂也更有意思后面有机会再单独拆开讲。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →