ietf.org 上的这个版本链接到 1973 年 7 月手绘负载平均图的 PDF 扫描,显示这已经被监控了几十年:
source: https://tools.ietf.org/html/rfc546
如今,旧操作系统的源代码也可以在网上找到。这是来自 TENEX(1970 年代初期)SCHED.MAC 的 DEC 宏程序集的一个例外:
NRJAVS==3 ;NUMBER OF LOAD AVERAGES WE MAINTAIN
GS RJAV,NRJAVS ;EXPONENTIAL AVERAGES OF NUMBER OF ACTIVE PROCESSES
[...]
;UPDATE RUNNABLE JOB AVERAGES
DORJAV: MOVEI 2,^D5000
MOVEM 2,RJATIM ;SET TIME OF NEXT UPDATE
MOVE 4,RJTSUM ;CURRENT INTEGRAL OF NBPROC+NGPROC
SUBM 4,RJAVS1 ;DIFFERENCE FROM LAST UPDATE
EXCH 4,RJAVS1
FSC 4,233 ;FLOAT IT
FDVR 4,[5000.0] ;AVERAGE OVER LAST 5000 MS
[...]
;TABLE OF EXP(-T/C) FOR T = 5 SEC.
EXPFF: EXP 0.920043902 ;C = 1 MIN
EXP 0.983471344 ;C = 5 MIN
EXP 0.994459811 ;C = 15 MIN
如果您使用一个空闲系统,然后开始一个单线程 CPU 密集型工作负载(一个循环中的一个线程),那么 60 秒后一分钟的平均负载是多少?如果它是一个普通的平均值,它将是 1.0。这是那个实验,图表:
负载平均实验以可视化指数阻尼
所谓的“一分钟平均值”,到一分钟大关才达到0.62左右。有关方程和类似实验的更多信息,Neil Gunther 博士写了一篇关于平均负载的文章:How It Works,此外 loadavg.c 中有许多 Linux 源代码块注释。
Linux Uninterruptible Tasks
当负载平均值首次出现在 Linux 中时,它们反映了 CPU 需求,与其他操作系统一样。但后来在 Linux 上将它们更改为不仅包括可运行(runnable)任务,还包括处于不可中断状态的任务(TASK_UNINTERRUPTIBLE 或 nr_uninterruptible)。此状态由希望避免信号中断的代码路径使用,其中包括在磁盘 I/O 上阻塞的任务和一些锁。您之前可能已经看到过这种状态:它在输出 ps 和 top 中显示为“D”状态。 ps(1) 手册页称其为“不间断睡眠(通常为 IO)”。
添加不间断状态意味着 Linux 平均负载可能会因磁盘(或 NFS)I/O 工作负载而增加,而不仅仅是 CPU 需求。对于熟悉其他操作系统及其 CPU 平均负载的每个人来说,包括这种状态,起初都令人深感困惑。
From: Matthias Urlichs <urlichs@smurf.sub.org>
Subject: Load average broken ?
Date: Fri, 29 Oct 1993 11:37:23 +0200
The kernel only counts "runnable" processes when computing the load average.
I don't like that; the problem is that processes which are swapping or
waiting on "fast", i.e. noninterruptible, I/O, also consume resources.
It seems somewhat nonintuitive that the load average goes down when you
replace your fast swap disk with a slow swap disk...
Anyway, the following patch seems to make the load average much more
consistent WRT the subjective speed of the system. And, most important, the
load is still zero when nobody is doing anything. ;-)
--- kernel/sched.c.orig Fri Oct 29 10:31:11 1993
+++ kernel/sched.c Fri Oct 29 10:32:51 1993
@@ -414,7 +414,9 @@
unsigned long nr = 0;
for(p = &LAST_TASK; p > &FIRST_TASK; --p)
- if (*p && (*p)->state == TASK_RUNNING)
+ if (*p && ((*p)->state == TASK_RUNNING) ||
+ (*p)->state == TASK_UNINTERRUPTIBLE) ||
+ (*p)->state == TASK_SWAPPING))
nr += FIXED_1;
return nr;
}
--
Matthias Urlichs \ XLink-POP N|rnberg | EMail: urlichs@smurf.sub.org
Schleiermacherstra_e 12 \ Unix+Linux+Mac | Phone: ...please use email.
90491 N|rnberg (Germany) \ Consulting+Networking+Programming+etc'ing 42
我是在平均负载意味着 CPU 平均负载的操作系统中长大的,所以 Linux 版本一直困扰着我。也许真正的问题一直是“平均负载”这个词和“I/O”一样模棱两可。哪种类型的 I/O?磁盘 I/O?文件系统 I/O?网络输入/输出? ...同样,哪个负载平均值? CPU平均负载?系统负载平均值?以这种方式澄清它让我这样理解它:
在 Linux 上,负载平均值是(或试图成为)“系统负载平均值”,对于整个系统来说,测量正在工作和等待工作(CPU、磁盘、uninterruptible 锁)的线程数。换句话说,它测量不完全空闲的线程数。优势:包括对不同资源的需求
在其他操作系统上,负载平均值是“CPU 负载平均值”,测量 CPU 正在运行的数量 + CPU 可运行线程数。优点:可以更容易理解和推理(仅适用于 CPU)。
请注意,还有另一种可能的类型:“物理资源负载平均值”,它仅包括物理资源的负载(CPU + 磁盘)。
也许有一天我们会为 Linux 添加额外的负载平均值,让用户选择他们想要使用的内容:单独的“CPU 负载平均值”、“磁盘负载平均值”、“网络负载平均值”等。或者只是使用不同的指标。
什么是“好”或“坏”的平均负载?
有些人发现似乎适用于他们的系统和工作负载的值:他们知道当负载超过 X 时,应用程序延迟很高并且客户开始抱怨。但这并没有真正的规则。
对于 CPU 负载平均值,可以将该值除以 CPU 数量,然后说如果该比率超过 1.0,则您正在以饱和状态运行,这可能会导致性能问题。这有点模棱两可,因为它是一个可以隐藏变化的长期平均值(至少一分钟)。一个比率为 1.5 的系统可能运行良好,而另一个比率为 1.5 的系统在一分钟内突然爆发可能表现不佳。
我曾经管理过一个双 CPU 的电子邮件服务器,它在白天运行的 CPU 平均负载在 11 到 16 之间(比率在 5.5 到 8 之间)。延迟是可以接受的,没有人抱怨。这是一个极端的例子:大多数系统的负载/CPU 比率仅为 2。
至于 Linux 的系统负载平均值:这些更加模糊,因为它们涵盖了不同的资源类型,所以你不能只除以 CPU 数量。它对于相对比较更有用:如果您知道系统在负载为 20 时运行良好,而现在是 40,那么是时候深入研究其他指标以查看发生了什么。
更好的指标
当 Linux 平均负载增加时,您知道您对资源(CPU、磁盘和一些锁)有更高的需求,但您不确定是哪一个。您可以使用其他指标进行说明。例如,对于 CPU:
CPU 运行队列延迟:例如,在 /proc/schedstat、perf sched、我的 runqlatbcc 工具。
CPU 运行队列长度:例如,使用 vmstat 1 和 'r' 列,或者我的 runqlen bcc 工具。
前两个是利用率指标,后三个是饱和度指标。利用率指标可用于工作负载表征,而饱和指标可用于识别性能问题。最佳 CPU 饱和度指标是运行队列(或调度程序)延迟的度量:任务/线程处于可运行状态但必须等待轮到它的时间。这些允许您计算性能问题的严重程度,例如,线程花费在调度程序延迟上的时间百分比。相反,测量运行队列长度可能表明存在问题,但更难以估计量级。
由于 linux kernel 不支持浮点数计算,因此需要把浮点数计算转换成定点数计算。linux 把这些浮点数乘以 来保留精度,此处使用二进制的好处是只需要移位即可。得到:
这就是下面 linux 代码中宏 EXP_1,EXP_5,EXP_15 的由来,LOAD_FREQ 代表每 5s 采样一次数据。
// include/linux/sched/loadavg.h
/*
* These are the constant used to fake the fixed-point load-average
* counting. Some notes:
* - 11 bit fractions expand to 22 bits by the multiplies: this gives
* a load-average precision of 10 bits integer + 11 bits fractional
* - if you want to count load-averages more often, you need more
* precision, or rounding will get you. With 2-second counting freq,
* the EXP_n values would be 1981, 2034 and 2043 if still using only
* 11 bit fractions.
*/
extern unsigned long avenrun[]; /* Load averages */
extern void get_avenrun(unsigned long *loads, unsigned long offset, int shift);
#define FSHIFT 11 /* nr of bits of precision */
#define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */
#define LOAD_FREQ (5*HZ+1) /* 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 LOAD_INT(x) ((x) >> FSHIFT)
#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100)
void calc_load_nohz_start(void)
{
/*
* We're going into NO_HZ mode, if there's any pending delta, fold it
* into the pending NO_HZ delta.
*/
calc_load_nohz_fold(this_rq());
}
static void calc_load_nohz_fold(struct rq *rq)
{
long delta;
delta = calc_load_fold_active(rq, 0);
if (delta) {
int idx = calc_load_write_idx() {
int idx = calc_load_idx;
/*
* See calc_global_nohz(), if we observe the new index, we also
* need to observe the new update time.
*/
smp_rmb();
/*
* If the folding window started, make sure we start writing in the
* next NO_HZ-delta.
*/
if (!time_before(jiffies, READ_ONCE(calc_load_update)))
idx++;
return idx & 1;
}
atomic_long_add(delta, &calc_load_nohz[idx]);
}
}
CPU 退出 NO_HZ 时:
如果当前时间戳 jiffies 还未到达 CPU 报告时间戳 this_rq->calc_load_update 时,不做任何处理。
如果已经到达或者过了 CPU 报告时间戳 this_rq->calc_load_update 时, 那需要更新当前 CPU 下次报告时间戳 this_rq->calc_load_update,这是因为原本负责更新这个值的函数 calc_global_load_tick 已经错过了。
void calc_load_nohz_stop(void)
{
struct rq *this_rq = this_rq();
/*
* If we're still before the pending sample window, we're done.
*/
this_rq->calc_load_update = READ_ONCE(calc_load_update);
if (time_before(jiffies, this_rq->calc_load_update))
return;
/*
* We woke inside or after the sample window, this means we're already
* accounted through the nohz accounting, so skip the entire deal and
* sync up for the next window.
*/
if (time_before(jiffies, this_rq->calc_load_update + 10))
this_rq->calc_load_update += LOAD_FREQ;
}
void calc_global_load(void)
{
unsigned long sample_window;
long active, delta;
sample_window = READ_ONCE(calc_load_update);
if (time_before(jiffies, sample_window + 10))
return;
/*
* Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.
*/
delta = calc_load_nohz_read();
if (delta)
atomic_long_add(delta, &calc_load_tasks);
active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;
avenrun[0] = calc_load(avenrun[0], EXP_1, active);
avenrun[1] = calc_load(avenrun[1], EXP_5, active);
avenrun[2] = calc_load(avenrun[2], EXP_15, active);
WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);
/*
* In case we went to NO_HZ for multiple LOAD_FREQ intervals
* catch up in bulk.
*/
calc_global_nohz();
}
static long calc_load_nohz_read(void)
{
int idx = calc_load_read_idx();
long delta = 0;
if (atomic_long_read(&calc_load_nohz[idx]))
delta = atomic_long_xchg(&calc_load_nohz[idx], 0);
return delta;
}
当前时间戳如果未到达计算平均负载时间戳 (calc_load_update + 10)时则跳过。
如果 CPU 因为 idle 而错过报告状态为 runnable 和 uninterruptible 的进程数量时,即 this_rq->calc_load_update 时间戳出现在 CPU idle 期间,导致 CPU 无法通过 calc_global_load_tick 报告进程的数量。那可以通过函数 calc_load_nohz_read 获取当时 runnable 和 uninterruptible 的进程数量(CPU 进入 idle 后,这两种状态的进程数量将保持不变。如果 CPU 未 idle,则 calc_load_nohz_read 返回 0。
接着 call 函数 calc_global_nohz:
如果当前时间戳已经超过计算平均负载时间戳 (calc_load_update + 10)时,则说在 CPU idle 期间,至少有一次或者多次需要计算平均负载的时间戳,因此这里需要把这些错过的采样点补回来,代码如下所示:
static void calc_global_nohz(void)
{
unsigned long sample_window;
long delta, active, n;
sample_window = READ_ONCE(calc_load_update);
if (!time_before(jiffies, sample_window + 10)) {
/*
* Catch-up, fold however many we are behind still
*/
delta = jiffies - sample_window - 10;
n = 1 + (delta / LOAD_FREQ);
active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;
avenrun[0] = calc_load_n(avenrun[0], EXP_1, active, n);
avenrun[1] = calc_load_n(avenrun[1], EXP_5, active, n);
avenrun[2] = calc_load_n(avenrun[2], EXP_15, active, n);
WRITE_ONCE(calc_load_update, sample_window + n * LOAD_FREQ);
}
/*
* Flip the NO_HZ index...
*
* Make sure we first write the new time then flip the index, so that
* calc_load_write_idx() will see the new time when it reads the new
* index, this avoids a double flip messing things up.
*/
smp_wmb();
calc_load_idx++;
}
这种情况出现时 CPU Idle 期间,这个期间每个采样点的 runnable 和 uninterruptible 的进程数量没有变化,所以 calc_load_n 简化计算方式。
/*
* a1 = a0 * e + a * (1 - e)
*
* a2 = a1 * e + a * (1 - e)
* = (a0 * e + a * (1 - e)) * e + a * (1 - e)
* = a0 * e^2 + a * (1 - e) * (1 + e)
*
* a3 = a2 * e + a * (1 - e)
* = (a0 * e^2 + a * (1 - e) * (1 + e)) * e + a * (1 - e)
* = a0 * e^3 + a * (1 - e) * (1 + e + e^2)
*
* ...
*
* an = a0 * e^n + a * (1 - e) * (1 + e + ... + e^n-1) [1]
* = a0 * e^n + a * (1 - e) * (1 - e^n)/(1 - e)
* = a0 * e^n + a * (1 - e^n)
*
* [1] application of the geometric series:
*
* n 1 - x^(n+1)
* S_n := \Sum x^i = -------------
* i=0 1 - x
*/
unsigned long
calc_load_n(unsigned long load, unsigned long exp,
unsigned long active, unsigned int n)
{
return calc_load(load, fixed_power_int(exp, FSHIFT, n), active);
}
分解 Linux 平均负载的背后逻辑是什么?
在原文里面介绍多个工具组合分解平均负载分布情况,这里我们解释一下这些数字背后的逻辑。
Linux负载平均值可以完全分解成组件吗?这是一个例子:在一个空闲的 8 CPU 系统上,我启动了 tar 来归档一些未缓存的文件。它在磁盘读取上花费了几分钟大部分时间。以下是从三个不同的终端窗口收集的统计数据:
有些人发现似乎适用于他们的系统和工作负载的值:他们知道当负载超过 X 时,应用程序延迟很高并且客户开始抱怨。但这并没有真正的规则。对于 CPU 负载平均值,可以将该值除以 CPU 数量,然后说如果该比率超过 1.0,则您正在以饱和状态运行,这可能会导致性能问题。这有点模棱两可,因为它是一个可以隐藏变化的长期平均值(至少一分钟)。一个比率为 1.5 的系统可能运行良好,而另一个比率为 1.5 的系统在一分钟内突然爆发可能表现不佳。........................