《面向应用开发者的系统指南》CPU篇之Linux系统平均负载

2020-06-20
8分钟阅读时长

本文是《面向应用开发者的系统指南》文档其中的一篇,完整的目录见《面向应用开发者的系统指南》导论

概述

Linux中可以使用uptime、top等命令来查看系统的平均负载情况,比如:

$ uptime
 10:54:37 up 29 days,  1:35,  2 users,  load average: 0.81, 0.65, 0.64

其中的load average: 0.81, 0.65, 0.64数据,给出了系统在最近1分钟、5分钟、15分钟的系统平均负载情况。

这一节讲解系统平均负载这个数据的来源,内容包括以下几方面:

  • 系统平均负载值来源于哪里?
  • 平均负载包括了哪些指标?
  • 内核是如何计算平均负载值的?
  • 平均负载的意义是什么?

平均负载值的来源

通过uptime命令可以看到系统最近1分钟、5分钟以及15分钟的平均负载值,所以要知道这个值的来源,最简单的方式就是了解uptime命令是从哪里获取到这些数据的,一方面可以看uptime命令的代码实现,但是直觉告诉我们一般这类命令都是通过读取/proc文件系统来获取系统的一些指标,所以更简单的方式是strace一下uptime命令,看看都去读取了哪些/proc文件系统的文件,果然看到了如下一行:

openat(AT_FDCWD, "/proc/loadavg", O_RDONLY) = 4
lseek(4, 0, SEEK_SET)                   = 0
read(4, "0.42 0.20 0.07 3/137 1322\n", 8191) = 26
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0

可以看到读取了/proc/loadavg文件,通过man proc命令来看看关于这个文件的说明:

/proc/loadavg
		The first three fields in this file are load average figures giving the number of jobs in the run queue (state R) or  wait‐
		ing  for  disk  I/O  (state  D) averaged over 1, 5, and 15 minutes.  They are the same as the load average numbers given by
		uptime(1) and other programs.  The fourth field consists of two numbers separated by a slash (/).  The first  of  these  is
		the  number of currently runnable kernel scheduling entities (processes, threads).  The value after the slash is the number
		of kernel scheduling entities that currently exist on the system.  The fifth field is the PID of the process that was  most
		recently created on the system.

以上文档中说明了,系统负载的统计数据源,包括:

  • 系统当前就绪队列中的进程(处于就绪状态即R状态);
  • 以及处于等待IO的不可被信号中断(即D状态)的进程。

/proc/loadavg文件的前三个字段,正是uptime命令用于输出系统平均负载的数据来源,可以cat该文件内容来看看:

➜  ~ cat /proc/loadavg
0.03 0.01 0.00 1/135 1735

于是,这里的第一个疑问解决了:uptime等命令行工具是通过读取/proc/loadavg文件获取当前系统在最近1分钟、5分钟、15分钟的平均负载值。

负载进程来源

在前面的描述中,提到了参与计算平均负载的进程包括:

  • 系统当前就绪队列中的进程(处于就绪状态即R状态);
  • 以及处于等待IO的不可被信号中断(即D状态)的进程。

处于R状态的进程被算入其中符合预期,因为系统负载本来就是对CPU这一项资源的度量,但是处于等待I/O并未占用CPU资源的D状态进程也被算入其中就让人有些意外了。

Linux Load Averages: Solving the Mystery一文中,Brendan Gregg详细探究了Linux系统这么做的原因,有兴趣的读者可以点开链接看看这篇文章。简单的把结论放在这里:

  • 在Linux系统中,平均负载就是system load avg,包括了正在工作和在等待工作(磁盘IO、锁等)的进程。与此等价的说法是,在Linux系统中平均负载反映的是系统中所有非idle的进程。
  • 而在非Linux系统中,平均负载只包括运行已经就绪准备运行的进程。

由以上可知,在Linux系统中,通过uptime等命令查看出来的平均负载如果突然变高,并不见得就是系统一定出现了问题,不能简单的把负载数量除以CPU资源数量,因为其中可能包括了在等待磁盘I/O的D状态进程。在上文中,提到了如果发现系统负载突然升高,还可以配合以下手段进一步查验:

  • 每CPU利用率: mpstat -P ALL 1
  • 每进程的CPU利用率:top, pidstat 1
  • 每线程的run queue调度延迟:/proc/PID/schedstats, delaystats, perf sched
  • CPU run queue延迟:/proc/schedstat, perf sched, runqlat, bcc工具
  • CPU run queue队列长度:vmstat 1的r列,runqlen bcc工具

前两个是利用率指标,可用来识别工作负载的特征;后三个是饱和度指标,可用来进行性能分析。除了CPU量化,还可以对磁盘I/O进行量化。这些命令具体的使用后续章节会给出。

以上,解决了系统负载数据的来源问题,接着又有了下一个疑问:/proc文件系统是内核用于输出一些系统状态的文件系统,内核又是如何计算系统的平均负载值的?

内核如何计算平均负载

指数平滑法

内核不可能一直监控着内核中的进程数量,更现实的做法是隔一段时间采集一组数据,再与前面的数据一起来预测当前的系统负载。如果将过去和现在的数据都同等对待,给予同样的权重,那么计算公式就是简单的使用过去的数据加上现在的数据来求平均值。

然而如果这样的话,过去的数据就和现在的数据一样重要。很显然,在使用数据来预测未来时,还是更近的数据权重更高更有说服力。

在这部分,内核采用的方式是所谓的指数平滑法(Exponential smoothing),其思想在于:给当前采集的数据,以及上一次采集周期的数据分别以权重值,通过把两部分加成起来计算平滑均值,公式如下:

$$ load_t = α * x_{t- 1} + n * (1 - α) * load_{t-1} $$

其中:

  • $load_t$:时间t的平滑平均值。
  • $x_{t-1}$:时间t-1的实际值。
  • $s_{t-1}$:时间t-1的平滑平均值。
  • α:平滑常数(平滑因子),范围在[0,1]之间。
  • n:当前时间的活跃进程数(状态为是RUNNABLE状态和TASK_UNINTERRUPTIBLE状态的进程数)。

注意,这里的平滑常数α,如果趋近于1,则$s_{t-1}$对$s_t$的影响越小。

内核就是通过这个算法来计算最近5分钟,10分钟,十五分钟的平滑均值。

其中的平滑常数,Linux内核是这样来选择的:

$$ α = e^{-5/(60*m)} $$

其中: 5:表示5s,作分子。 60:表示60s。 m: 表示分钟,1, 5, 15。 60 * m作为分母。

把m带入到公式计算,分别能计算出α为0.920044415,0.983471454,0.994459848

定点运算(Fixed-point arithmetic)

然而即便是这样,内核也不能直接计算系统的平均负载,因为内核并不能直接进行浮点计算。

因此,需要首先把浮点数运算转换成定点数运算,采用的办法就是将浮点数乘以相应进制的n次方,其中n为保留的小数点的位数。比如3.1415926,如果要转换成只保留3位小数的定点数运算,就需要乘以1000,也就是得到3141,后面的926丢弃。

在这里,Linux内核将上面的平滑常数α转换成2进制的定点数,其中保留的精度为11位,因此:

$$ 0.920044415 * 2^{11} = 1884 $$ $$ 0.983471454 * 2^{11} = 2014 $$ $$ 0.994459848 * 2^{11} = 2037 $$

Linux内核的实现代码

有了前面的分析,可以看到通过“指数退避算法”,并不需要详细的保存过去1分钟、5分钟、15分钟的所有负载值来做计算,而是在每次计算的时候,拿到当前的值以及上一次保存的值,套用到前面的公式计算即可,这样做的好处是只需要保持一份上一次的数据即可。同时,由于需要拿到就绪状态以及不可中断中断状态的进程数量之和,也是通过采样的方式去定时获取这个值,默认是5秒采样一次。

与平均负载计算相关的几个宏列举如下:

(linux/sched.h)
#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) */

其中:

  • FSHIFT:计算精度,可以看到是11位精度。
  • FIXED_1:2进程11位精度下的1.0。
  • LOAD_FREQ:计算间隔,5秒计算一次平均负载。
  • EXP_1、EXP_5、EXP_15:1分钟、5分钟、15分钟平滑时间常数的2进程11位精度定点数,也就是前面定点计算部分介绍的预计算出来的几个常量。

内核中根据“指数退避算法”计算平均负载的工作由函数calc_load完成:

(sched/proc.c)

/*
 * a1 = a0 * e + a * (1 - e)
 */
static unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{
	load *= exp;
	load += active * (FIXED_1 - exp);
	load += 1UL << (FSHIFT - 1);
	return load >> FSHIFT;
}

可以看到,该函数的内容,就是通过前面列举出来的宏,套用指数退避算法的公式计算即可。

内核中使用数组avenrun来保存上一次计算得到的1分钟、5分钟、15分钟的平均负载,每一次的计算结果保存在这个数组之中,下一次再计算的时候将上次保存的结果拿出来做为这一次计算的参数就好。

有了算法、数据,最后就是介绍计算的触发点以及入口函数了,这个函数是内核中的calc_global_load函数:

/*
 * calc_load - update the avenrun load estimates 10 ticks after the
 * CPUs have updated calc_load_tasks.
 */
void calc_global_load(unsigned long ticks)
{
	long active, delta;

	if (time_before(jiffies, calc_load_update + 10))
		return;

	/*
	 * Fold the 'old' idle-delta to include all NO_HZ cpus.
	 */
	delta = calc_load_fold_idle();
	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);

	calc_load_update += LOAD_FREQ;

	/*
	 * In case we idled for multiple LOAD_FREQ intervals, catch up in bulk.
	 */
	calc_global_nohz();
}

系统平均负载的意义

从以上的分析,可以看出来,“系统平均负载”值反映的是系统在过去一段时间中,“就绪状态进程数量”加上“不可被中断进程数量”之和的一种趋势,只是进程状态的一些统计信息,和CPU的使用率并没有直接关系。

因为“系统平均负载”统计了这两类进程的总和,所以如果仔细区分起来,与CPU使用率可能存在以下几种关系:

  • 系统中有大量CPU密集型的进程,比如大量进程都在进行计算,此时的系统平均负载与CPU使用率的关联就很大。
  • 系统中有大量IO密集型进程,这些进程中有很多时间都在等待IO完成,此时系统平均负载高但是CPU使用率并不见得就高。
  • 就绪队列中有大量处于就绪状态等待CPU资源的进程,这种情况下会导致系统平均负载升高,而且CPU使用率也变高。

因此,当发现系统平均负载升高时,还需要其他工具来查看究竟是上面的哪种情况。

总结

  • uptime等查看当前CPU负载的命令,通过读取/proc/loadavg文件获取最近1分钟、5分钟、15分钟系统的平均负载。
  • Linux计算平均负载时包括了R状态进程和D状态进程,Linux系统中平均负载反映的是系统中所有非idle的进程。在发现Linux系统平均负载升高时需要配合其他工具一起查证。
  • 内核使用指数平滑法来计算系统的平均负载。
  • 最近1分钟、5分钟、15分钟系统的平均负载反映的是系统负载的趋势走向,而不是一个精准的值。
  • 内核在计算平均负载时,将相应的参数转换为定点数进行计算,保留的数据精度是11位,计算间隔是5秒一次。

参考资料