检查负载平均值
许多 Linux 管理员和支持技术人员经常使用 top 实用程序来实时监控他们的系统状态。在某些公司中,当出现任何故障迹象时,首先检查 top 是非常典型的做法。 在这种情况下,top 成为衡量机器健康状况的事实上的关键指标。 如果 top 看起来不错,那么肯定没有任何系统问题。 top 包含丰富的信息——内存使用情况、内核状态、进程优先级、进程所有者等等,所有这些都可以从 top 中获得。 但是,这三个奇怪的负载平均值的目的是什么,它们到底想告诉我什么? 要回答这些问题,必须对这些值的形成方式有直观和详细的了解。 让我们从直觉开始。
top 输出的第一行中的三个负载平均值是 1 分钟、5 分钟和 15 分钟的平均值。(这些值也由其他命令显示,例如 uptime,而不仅仅是 top。) 这意味着,从左到右读取,可以检查特定系统状态的老化趋势和/或持续时间。 所讨论的状态是 CPU 负载——不要与 CPU 百分比混淆。 事实上,它精确地测量的是 CPU 负载,因为负载平均值不包括任何等待 I/O、网络、数据库或任何其他不需要 CPU 的进程或线程。 它狭隘地关注那些主动要求 CPU 时间的东西。 这与 CPU 百分比有很大不同。 CPU 百分比是一个时间间隔(即采样间隔)内,系统的进程被发现在 CPU 上处于活动状态的时间量。 如果 top 报告您的程序占用 45% 的 CPU,则 top 采样的 45% 发现您的进程在 CPU 上处于活动状态。 其余时间您的应用程序都在等待中。 (重要的是要记住,CPU 是一个离散状态机。它实际上只能处于 100%,执行指令,或者处于 0%,等待执行某些操作。 不存在使用 45% 的 CPU 这样的事情。 CPU 百分比是时间的函数。) 但是,您的应用程序的休息时间可能包括等待在 CPU 上而不是在外部设备上被调度。 那么,等待百分比的这部分与理解您的整体 CPU 使用模式非常相关。
负载平均值与 CPU 百分比在两个重要方面有所不同:1) 负载平均值测量 CPU 利用率的趋势,而不仅仅是像百分比那样的瞬时快照,以及 2) 负载平均值包括对 CPU 的所有需求,而不仅仅是在测量时有多少是活动的。
作者倾向于过度使用类比,有时会冒着侮辱读者智力或将主题过度简化到失去重要细节的风险。 然而,高速公路交通模式是这个主题的完美类比,因为这个模型概括了资源争用的本质,并且也是许多排队论书籍作者选择的隐喻。 毫不奇怪,CPU 争用是一个排队论问题,到达率、泊松理论和服务率的概念都适用。 四处理器机器可以被可视化为四车道高速公路。 每条车道都提供了指令可以执行的路径。 车辆可以代表这些指令。 此外,入口车道上还有准备驶入高速公路的车辆,这四条车道要么准备好容纳这种需求,要么没有。 如果所有高速公路车道都拥堵,则进入的汽车必须等待开口。 如果我们现在将 CPU 百分比和 CPU 负载平均值测量应用于这种情况,则百分比检查每辆车占用高速公路车道的时间的相对量,这固有地忽略了对高速公路的积压需求——即在入口处排队的汽车。 因此,例如,车牌 XYZ 123 的车辆在采样时间的 30% 被发现在高速公路上。 车牌 ABC 987 的车辆在采样时间的 14% 被发现在高速公路上。 这描绘了每辆车如何利用高速公路,但它并未表明对高速公路的需求。
此外,这些车辆在高速公路上被发现的时间百分比并没有告诉我们关于整体交通模式的任何信息,除了,也许,它们到达目的地的时间比它们希望的要长。 因此,我们可能会怀疑某种程度的拥堵,但 CPU 百分比不会确切地告诉我们。 另一方面,负载平均值会。
这就引出了重点。 是高速公路本身的整体交通模式为我们提供了交通状况的最佳图景,而不仅仅是汽车占用车道的频率。 负载平均值为我们提供了这种视图,因为它包括正在排队进入高速公路的汽车。 可能是非高峰时段,对高速公路的需求很少,但恰好路上有很多汽车。 CPU 百分比显示了汽车使用高速公路的程度,但负载平均值向我们展示了整个图景,包括积压的需求。 更有趣的是,积压的需求越近,负载平均值就越能反映出来。
将讨论带回到手头的机器,负载平均值通过增加持续时间告诉我们,我们的物理 CPU 是过度利用还是利用不足。 完美利用的点,意味着 CPU 始终处于忙碌状态,但没有进程等待 CPU,是平均值与 CPU 数量相匹配。 如果一台机器上有四个 CPU,并且报告的 1 分钟负载平均值为 4.00,则该机器在过去 60 秒内完美地利用了其处理器。 这种理解可以推断到 5 分钟和 15 分钟的平均值。
一般来说,负载平均值的直观概念是,它们越高,超过处理器数量,对 CPU 的需求就越大,它们越低,低于处理器数量,未开发的 CPU 容量就越多。 但一切并非看起来那样。
负载平均值计算最好被认为是 Linux 运行队列中标记为运行或不可中断的进程的移动平均值。 选择“被认为是”这个词是有原因的:这就是对这些测量的解释方式,但并非幕后发生的真实情况。 正是在我们旅程的这个关头,一切的现实,就像量子力学一样,似乎与它呈现的直观方式不符。
top 和 uptime 命令显示的负载平均值直接从 /proc 获取。 如果您运行的是 Linux 内核 2.4 或更高版本,则可以使用以下命令自行读取这些值cat /proc/loadavg。 然而,是 Linux 内核在 /proc 中生成这些值。 具体来说,timer.c 和 sched.h 协同工作进行计算。 要了解 timer.c 的作用,时间分片的概念和节拍计数器有助于完善整个图景。
在 Linux 内核中,每个可调度进程在每次调度时都会获得固定的 CPU 时间量。 默认情况下,此量为 10 毫秒,即 1/100 秒。 在这短暂的时间跨度内,该进程被分配一个物理 CPU 以在其上运行其指令,并被允许接管该处理器。 通常,进程会在 10 毫秒用完之前通过套接字调用、I/O 调用或回调内核的调用放弃控制。 (在 Intel 2.6GHz 处理器上,10 毫秒的时间足以执行大约 5000 万条指令。 这对于大多数应用程序周期来说已经足够多的处理时间了。) 如果进程使用了其全部 10 毫秒的 CPU 时间,则硬件会引发中断,并且内核重新获得对进程的控制权。 然后,内核会立即惩罚该进程,因为它太占用资源了。 正如您所看到的,时间分片是一个重要的设计概念,用于使您的系统在外部看起来运行流畅。 它也是生成负载平均值的工具。
10 毫秒的时间片是一个足够重要的概念,值得为其命名:量子值。 10 毫秒本身不一定有什么特别之处,但量子值总体上是特别的,因为无论将其设置为哪个值(它是可配置的,但 10 毫秒是默认值),它都控制着内核至少多久从应用程序手中夺回系统控制权。 内核在夺回控制权时执行的众多任务之一是增加其节拍计数器。 节拍计数器测量自系统启动以来发生的量子滴答数。 当量子定时器弹出时,timer.c 在内核中名为 timer.c:do_timer() 的函数处输入。 在这里,所有中断都被禁用,因此代码不会与移动目标一起工作。 节拍计数器递增 1,并检查负载平均值计算以查看是否应计算它。 实际上,负载平均值计算并非真正在每个量子滴答上计算,而是由基于 HZ 频率设置并在每个量子滴答上测试的可变值驱动的。 (HZ 不要与处理器的 MHz 额定值混淆。 此变量设置特定 Linux 内核活动的脉冲频率,默认情况下 1HZ 等于一个量子或 10 毫秒。) 尽管 HZ 值可以在某些版本的内核中配置,但通常设置为 100。 计算代码使用 HZ 值来确定计算频率。 具体来说,timer.c:calc_load() 函数将每 5 * HZ 或大约每五秒运行一次平均算法。 以下是该函数的完整内容
unsigned long avenrun[3]; static inline void calc_load(unsigned long ticks) { unsigned long active_tasks; /* fixed-point */ static int count = LOAD_FREQ; count -= ticks; if (count < 0) { count += LOAD_FREQ; active_tasks = count_active_tasks(); CALC_LOAD(avenrun[0], EXP_1, active_tasks); CALC_LOAD(avenrun[1], EXP_5, active_tasks); CALC_LOAD(avenrun[2], EXP_15, active_tasks); } }
avenrun 数组包含我们一直在讨论的三个平均值。 calc_load() 函数由 update_times() 调用,update_times() 也位于 timer.c 中,并且是负责为 calc_load() 函数提供 ticks 参数的代码。 不幸的是,此函数并未揭示其最有趣的方面:计算本身。 但是,这可以在 sched.h 中轻松找到,sched.h 是内核代码的大部分使用的头文件。 在那里,CALC_LOAD 宏及其关联的值可用
extern unsigned long avenrun[]; /* Load averages */ #define FSHIFT 11 /* nr of bits of precision */ #define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */ #define LOAD_FREQ (5*HZ) /* 5 sec intervals */ #define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #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;
这里是轮胎与路面相遇的地方。 现在应该很明显,现实似乎与幻觉不符。 至少,这肯定不是我们大多数人在小学学到的那种平均值。 但它毕竟是一个平均值。 从技术上讲,它是一个指数衰减函数,并且是大多数 UNIX 系统以及 Linux 的首选移动平均值。 让我们检查一下它的细节。
该宏采用三个参数:负载平均值桶(avenrun[] 中的三个元素之一)、一个常数指数和当前在运行队列中的运行/不可中断进程的数量。 上面列出了可能的指数常数:1 分钟平均值的 EXP_1、5 分钟平均值的 EXP_5 和 15 分钟平均值的 EXP_15。 需要注意的重点是,该值会随着时间的推移而降低。 这些常数是通过下面显示的数学函数计算出的幻数

当 x=1 时,y=1884;当 x=5 时,y=2014;当 x=15 时,y=2037。 幻数的目的是它允许 CALC_LOAD 宏使用分数的精确定点表示。 然后,幻数只不过是对运行负载平均值使用的乘数,使其成为移动平均值。 (定点表示的数学超出了本文的范围,因此我不会尝试解释。) 指数衰减函数的目的是它不仅通过保持有用的趋势线来平滑下降和峰值,而且还准确地降低了它测量为活动年龄的质量。 随着时间的推移,连续的 CPU 事件增加了它们对负载平均值的重要性。 这正是我们想要的,因为最近的 CPU 活动可能比古代事件对当前状态产生更大的影响。 最后,负载平均值给出了从 15 分钟到当前分钟的平滑趋势,并为我们提供了一个窗口,不仅可以了解 CPU 使用率,还可以了解对 CPU 的平均需求。 随着负载平均值高于物理 CPU 的数量,CPU 被使用的越多,对 CPU 的需求也越大。 并且,随着它消退,需求就越小。 通过这种理解,负载平均值可以与 CPU 百分比一起使用,以获得更准确的 CPU 活动视图。
我希望这不仅可以作为对 Linux 负载平均值的实用解释,还可以阐明它们背后一些黑暗的数学阴影。 有关更多信息,对指数衰减函数及其应用的研究将有助于更深入地了解该主题。 但对于更注重实践的人来说,绘制负载平均值与受控进程数(即,在受控循环中建模 CALC_LOAD 算法的效果)的关系图将使您对实际关系以及衰减滤波器如何应用有所了解。
Ray Walker 是一名专门研究 UNIX 内核级代码的顾问。 他是一位软件开发人员,拥有超过 25 年的经验,自 1995 年以来一直使用 Linux。 可以通过 ray.rwalk2730@gmail.com 与他联系。