细节篇(6):限制了CPU为什么还有高平均负载

3,416 阅读9分钟

上一讲提到 CPU Cgroup 可以限制进程的 CPU 资源使用,但对容器的资源限制还是存在盲点的:无法控制 Load Average 的平均负载。没有这个限制就会影响系统资源的合理调度,很可能导致系统变得很慢。

问题再现

有时可能发现明明容器里所有进程的 CPU 使用率都很低,甚至整个宿主机的 CPU 使用率都很低,而机器的 Load Average 里的值却很高,容器里进程运行得也很慢。

比如下面的 top 输出,可以看到整个机器 的 CPU Usage 几乎为 0,因为"id"显示 99.9%。

但 1 分钟的"load average"的值却高达 9.09,这里的数值 9 几乎意味着使用了 9 个 CPU,这样 CPU Usage 和 Load Average 的数值看上去就很矛盾了。

image.png

那问题来了,我们在看一个系统里 CPU 使用情况时,到底是看 CPU Usage 还是 Load Average 呢?

这里就涉及到今天要解决的两大问题:

  1. Load Average 是什么,CPU Usage 和 Load Average 有什么差别? 2. 如果 Load Average 值升高,应用的性能下降了,这背后的原因是什么呢?

什么是 Load Average?

无论是运行 uptime, 还是 top,都可以看到类似这个输出"load average:2.02, 1.83, 1.20"。后面的三个数值分别代表过去 1 分钟,5 分钟,15 分钟在这个节点上的 Load Average

RFC546定义了 Load Average,这里定义的 Load Average 是一种 CPU 资源需求的度 量。

举个例子,对于一个单 CPU 的系统,如果 1 分钟里处理器上始终有一个进程在运行,同时OS的进程可运行队列中始终都有 9 个进程在等待获取 CPU 资源。那么对于这 1 分钟来说,系统的"load average"就是 1+9=10,这个定义对绝大部分的 Unix 系统都适用。

对于 Linux,如果只考虑 CPU 的资源,Load Average 等于单位时间内正在运行的进程加上可运行队列的进程,这个定义也是成立的。通过这个定义归纳了下面三点对 Load Average 的理解。

  1. 不论 CPU 是空闲还是满负载,Load Average 都是 Linux 进程调度器中可运行队列(Running Queue)里的一段时间的平均进程数目。
  2. CPU 还有空闲的情况下(可运行队列中的进程数目小于 CPU 个数),CPU Usage 可以直接反映到"load average"上,单位时间进程 CPU Usage 相加的平均值应该就是"load average"的值。
  3. CPU 满负载的情况下,同时还有更多的进程在排队需要 CPU 资源。这时"load average"就不能和 CPU Usage 等同了。

比如单个 CPU 的系统,CPU Usage 最大只是有 100%,也就 1 个 CPU;而"load average"的值可以远远大于 1,因为"load average"看的是操作系统中可运行队列中进程的个数。

动手验证一下。先准备一个可以消耗任意 CPU Usage 程序,在执行这个程序的时候,后面加个数字作为参数,

比如下面的设置,参数是 2,就是说这个进程会创建两个线程,每个线程都跑满 100% 的 CPU,消耗两个 CPU 资源。

./threads-cpu 2

接下来跑两个例子,第一个例子是执行 2 个满负载的线程,第二个例子执行 6 个 满负载的线程,同样都是在一台 4 个 CPU 的节点上。

先来看第一个例子。在程序运行几分钟后,运行 top 来查看一下 CPU Usage 和 Load Average。

可以看到两个 threads-cpu 各自都占了将近 100% 的 CPU,对于 4 个 CPU 的计算机来说,CPU Usage 占了 50%。

这时,Load Average 里第一项(前 1 分钟的数值)为 1.98,近似于 2。和运行的 200%CPU Usage 相对应。

Linux 内核中不使用浮点计算,这导致 Load Average 里的 1 分钟,5 分钟,15 分钟的时间值并不精确,但这不影响我们查看 Load Average 的数值,所以先不用管这个时间的准确性。

image.png

再来跑第二个例子,执行 threads-cpu,设置参数为 6,让这个进程建出 6 个线程,每个线程都会尽量去抢占 CPU,但计算机总共只有 4 个 CPU,所以这 6 个线程的 CPU Usage 加起来只是 400%。

显然这时 4 个 CPU 都被占满了,我们可以看到整个节点的 idle(id)也已经是 0.0% 了。

但前 1 分钟的 Load Average 是 5.93 接近 6。即 Load Average 表示的是一段时间里运行队列中需要被调度的进程 / 线程平均数目。

image.png

到这里是不是就可以认定 Load Average 就代表一段时间里运行队列中需要被调度的进程或者线程平均数目了呢? 或许对其他的 Unix 系统来说,这个理解已经够了,但对于 Linux 系统还不能这么认定。

故事还要从 Linux 早期的历史说起,那时开发者 Matthias 发现把快速的磁盘换成了慢速的磁盘,运行同样的负载,系统的性能是下降的,但 Load Average 却没有反映。

他发现因为 Load Average 只考虑运行态的进程数目,而没有考虑等待 I/O 的进程。所以他认为 Load Average 如果只是考虑进程运行队列中需要被调度的进程或线程平均数目是不够的,因为对于处于 I/O 资源等待的进程都是处于 TASK_UNINTERRUPTIBLE 状态(非中断状态:进程为等待某个系统资源而进入了睡眠状态,睡眠状态不能被信号打断)的。

那他是怎么处理这件事的呢?他给内核加一个 patch,把处于 TASK_UNINTERRUPTIBLE 状态的进程数目也计入了 Load Average 中。

为了验证这一点,我们可以模拟一下 UNINTERRUPTIBLE 的进程,来看看 Load Average 的变化。

这里我们做一个 kernel module,通过一个 /proc 文件系统给用户程序提供一个读取的接口,只要用户进程读取了这个接口就会进入 UNINTERRUPTIBLE。这样就可以模拟两个处于 UNINTERRUPTIBLE 状态的进程,然后查看一下 Load Average 有没有增加。

程序跑了几分钟之后,前 1 分钟的 Load Average 差不多从 0 增加到了 2.16,节点上 CPU Usage 几乎为 0,idle 为 99.8%。

可运行队列中的进程数目是 0,只有休眠队列中有两个进程,并且这两个进程显示为 D state 进程,也就是模拟出来的 TASK_UNINTERRUPTIBLE 状态的进程。

所以即使 CPU 上不做任何的计算,Load Average 仍然会升高。如果 TASK_UNINTERRUPTIBLE 状态的进程数目有几百几千个,那么 Load Average 的数值也可以达到几百几千。

image.png

平均负载统计了这两种情况的进程:

  1. Linux 进程调度器中可运行队列一段时间(1 分钟,5 分 钟,15 分钟)的进程平均数。
  2. Linux 进程调度器中休眠队列里的一段时间的 TASK_UNINTERRUPTIBLE 状态下的进程平均数。

所以,最后的公式就是:Load Average= 可运行队列进程平均数 + 休眠队列中不可打断的进程平均数

打个比方来说明 Load Average。你可以想象每个 CPU 是一条道路,每个进程是一辆车,看单位时间通过的车辆,一条道上的车越多,那么这条道路的负载也就越高。

此外,Linux 计算系统负载时,还额外做了个补丁把 TASK_UNINTERRUPTIBLE 状态的进程也考虑了,就像道路中要把红绿灯情况也考虑进去。一旦有了红灯,汽车就要停下来排队,那么即使道路很空,但是红灯多了,汽车也要排队等待,也开不快。

现象解释:为什么load average会升高?

回到最开始的问题,为什么对容器已经用 CPU Cgroup 限制了它的 CPU Usage,容器里的进程还是可以造成整个系统很高的 Load Average。

因为 Linux 下的 Load Averge 不仅仅计算了 CPU Usage 的部分,还计算了系统中 TASK_UNINTERRUPTIBLE 状态的进程数目。

如果 Load Average 值升高,应用的性能已经下降了,真正的原因是什么?问题就出在 TASK_UNINTERRUPTIBLE 状态的进程上了。

怎么验证这个判断呢?只要运行 ps aux | grep “ D ” ,就可以看到容器中有多少 TASK_UNINTERRUPTIBLE 状态(在 ps 命令中这个状态的进程标示为"D"状态)的进程,为了方便,后面简称为 D 状态进程。正是这些 D 状态进程引起了 Load Average 的升高。

D 状态进程产生的本质是什么?

在 Linux 内核中有数百处调用点,它们会把进程设置为 D 状态,主要集中在 disk I/O 的访问和信号量(Semaphore)锁的访问上。

无论是对 disk I/O 的访问还是对信号量的访问,都是对 Linux 里的资源的一种竞争。当进程处于 D 状态时,就说明进程还没获得资源,这会在应用程序的最终性能上体现,也就是说用户会发觉应用的性能下降了。

但目前 D 状态进程引起的容器中进程性能下降问题,Cgroups 还不能解决。因为 Cgroups 更多的是以进程为单位进行隔离,而 D 状态进程是内核中系统全局资源引入的,所以Cgroups 影响不了它。

我们可以做的是,在生产环境中监控容器的宿主机节点里 D 状态的进程数量,然后对 D 状态进程数目异常的节点进行分析,比如磁盘硬件出现问题引起 D 状态进程数目增加,这时就需要更换硬盘。

总结

在其他 Unix OS里 Load Average 只考虑 CPU 部分,Load Average 计算的是进程调度器中可运行队列里的一段时间的平均进程数目,Linux 在这个基础上加上了进程调度器中休眠队列里的一段时间的 TASK_UNINTERRUPTIBLE 状态的平均进程数目。

image.png

因为 TASK_UNINTERRUPTIBLE 状态的进程同样也会竞争系统资源,所以它会影响到应用程序的性能。我们可以在容器宿主机的节点对 D 状态进程做监控,定向分析解决。