You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

17 KiB

07 | Load Average加了CPU Cgroup限制为什么我的容器还是很慢

你好我是程远。今天我想聊一聊平均负载Load Average的话题。

在上一讲中我们提到过CPU Cgroup可以限制进程的CPU资源使用但是CPU Cgroup对容器的资源限制是存在盲点的。

什么盲点呢就是无法通过CPU Cgroup来控制Load Average的平均负载。而没有这个限制就会影响我们系统资源的合理调度很可能导致我们的系统变得很慢。

那么今天这一讲我们要来讲一下为什么加了CPU Cgroup的配置后即使保证了容器的CPU资源容器中的进程还是会运行得很慢

问题再现

在Linux的系统维护中我们需要经常查看CPU使用情况再根据这个情况分析系统整体的运行状态。有时候你可能会发现明明容器里所有进程的CPU使用率都很低甚至整个宿主机的CPU使用率都很低而机器的Load Average里的值却很高容器里进程运行得也很慢。

这么说有些抽象,我们一起动手再现一下这个情况,这样你就能更好地理解这个问题了。

比如说下面的top输出第三行可以显示当前的CPU使用情况我们可以看到整个机器的CPU Usage几乎为0因为"id"显示99.9%这说明CPU是处于空闲状态的。

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

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

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

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

好了,这一讲我们就带着这两个问题,一起去揭开谜底。

什么是Load Average?

要回答前面的问题很显然我们要搞明白这个Linux里的"load average"这个值是什么意思,又是怎样计算的。

Load Average这个概念你可能在使用Linux的时候就已经注意到了无论你是运行uptime, 还是top都可以看到类似这个输出"load average2.02, 1.83, 1.20"。那么这一串输出到底是什么意思呢?

最直接的办法当然是看手册了,如果我们用"Linux manual page"搜索uptime或者top就会看到对这个"load average"和后面三个数字的解释是"the system load averages for the past 1, 5, and 15 minutes"。

这个解释就是说后面的三个数值分别代表过去1分钟5分钟15分钟在这个节点上的Load Average但是看了手册上的解释我们还是不能理解什么是Load Average。

这个时候你如果再去网上找资料就会发现Load Average是一个很古老的概念了。上个世纪70年代早期的Unix系统上就已经有了这个Load AverageIETF还有一个RFC546定义了Load Average这里定义的Load Average是一种CPU资源需求的度量。

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

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

第一不论计算机CPU是空闲还是满负载Load Average都是Linux进程调度器中可运行队列Running Queue里的一段时间的平均进程数目。

第二计算机上的CPU还有空闲的情况下CPU Usage可以直接反映到"load average"上什么是CPU还有空闲呢具体来说就是可运行队列中的进程数目小于CPU个数这种情况下单位时间进程CPU Usage相加的平均值应该就是"load average"的值。

第三计算机上的CPU满负载的情况下计算机上的CPU已经是满负载了同时还有更多的进程在排队需要CPU资源。这时"load average"就不能和CPU Usage等同了。

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

这样的解释可能太抽象了,为了方便你理解,我们一起动手验证一下。

怎么验证呢?我们可以执行个程序来模拟一下,先准备好一个可以消耗任意CPU Usage的程序,在执行这个程序的时候,后面加个数字作为参数,

比如下面的设置参数是2就是说这个进程会创建出两个线程并且每个线程都跑满100%的CPU2个线程就是2 * 100% = 200%的CPU Usage也就是消耗了整整两个CPU的资源。

# ./threads-cpu 2

准备好了这个CPU Usage的模拟程序我们就可以用它来查看CPU Usage和Load Average之间的关系了。

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

先来看第一个例子我们在一台4个CPU的计算机节点上运行刚才这个模拟程序还是设置参数为2也就是使用2个CPU Usage。在这个程序运行了几分钟之后我们运行top来查看一下CPU Usage和Load Average。

我们可以看到两个threads-cpu各自都占了将近100%的CPU两个就是200%2个CPU对于4个CPU的计算机来说CPU Usage占了50%,空闲了一半,这个我们也可以从 idle id49.9%得到印证。

这时候Load Average里第一项也就是前1分钟的数值为1.98近似于2。这个值和我们一直运行的200%CPU Usage相对应也验证了我们之前归纳的第二点——CPU Usage可以反映到Load Average上。

因为运行的时间不够前5分钟前15分钟的Load Average还没有到2而且后面我们的例子程序一般都只会运行几分钟所以这里我们只看前1分钟的Load Average值就行。

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


那我们再来跑第二个例子同样在这个4个CPU的计算机节点上如果我们执行CPU Usage模拟程序threads-cpu设置参数为6让这个进程建出6个线程这样每个线程都会尽量去抢占CPU但是计算机总共只有4个CPU所以这6个线程的CPU Usage加起来只是400%。

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

但这个时候我们看看前1分钟的Load Average数值不是4而是5.93接近6我们正好模拟了6个高CPU需求的线程。这也告诉我们Load Average表示的是一段时间里运行队列中需要被调度的进程/线程平均数目。

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

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

他发现这是因为Load Average只考虑运行态的进程数目而没有考虑等待I/O的进程。所以他认为Load Average如果只是考虑进程运行队列中需要被调度的进程或线程平均数目是不够的因为对于处于I/O资源等待的进程都是处于TASK_UNINTERRUPTIBLE状态的。

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

在这里我们又提到了TASK_UNINTERRUPTIBLE状态的进程在前面的章节中我们介绍过我再给你强调一下TASK_UNINTERRUPTIBLE是Linux进程状态的一种是进程为等待某个系统资源而进入了睡眠的状态并且这种睡眠的状态是不能被信号打断的。

下面就是1993年Matthias的kernel patch你有兴趣的话可以读一下。

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;
 }

那么对于Linux的Load Average来说除了可运行队列中的进程数目等待队列中的UNINTERRUPTIBLE进程数目也会增加Load Average。

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

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

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

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

这个例子证明了Linux将TASK_UNINTERRUPTIBLE状态的进程数目计入了Load Average中所以即使CPU上不做任何的计算Load Average仍然会升高。如果TASK_UNINTERRUPTIBLE状态的进程数目有几百几千个那么Load Average的数值也可以达到几百几千。

好了到这里我们就可以准确定义Linux系统里的Load Average了其实也很简单你只需要记住平均负载统计了这两种情况的进程

第一种是Linux进程调度器中可运行队列Running Queue一段时间1分钟5分钟15分钟的进程平均数。

第二种是Linux进程调度器中休眠队列Sleeping Queue里的一段时间的TASK_UNINTERRUPTIBLE状态下的进程平均数。

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

如果打个比方来说明Load Average的统计原理。你可以想象每个CPU就是一条道路每个进程都是一辆车怎么科学统计道路的平均负载呢就是看单位时间通过的车辆一条道上的车越多那么这条道路的负载也就越高。

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

现象解释为什么Load Average会升高

解释了Load Average这个概念我们再回到这一讲最开始的问题为什么对容器已经用CPU Cgroup限制了它的CPU Usage容器里的进程还是可以造成整个系统很高的Load Average。

我们理解了Load Average这个概念之后就能区分出Load Averge和CPU使用率的区别了。那么这个看似矛盾的问题也就很好回答了因为Linux下的Load Averge不仅仅计算了CPU Usage的部分它还计算了系统中TASK_UNINTERRUPTIBLE状态的进程数目。

讲到这里为止我们找到了第一个问题的答案那么现在我们再看第二个问题如果Load Average值升高应用的性能已经下降了真正的原因是什么问题就出在TASK_UNINTERRUPTIBLE状态的进程上了。

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

找到了Load Average升高的问题出在D状态进程了我们想要真正解决问题还有必要了解D状态进程产生的本质是什么

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

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

那么D状态进程导致了性能下降我们肯定是想方设法去做调试的。但目前D状态进程引起的容器中进程性能下降问题Cgroups还不能解决这也就是为什么我们用Cgroups做了配置即使保证了容器的CPU资源 容器中的进程还是运行很慢的根本原因。

这里我们进一步做分析为什么CPU Cgroups不能解决这个问题呢就是因为Cgroups更多的是以进程为单位进行隔离而D状态进程是内核中系统全局资源引入的所以Cgroups影响不了它。

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

重点总结

这一讲我们从CPU Usage和Load Average差异这个现象讲起最主要的目的是讲清楚Linux下的Load Average这个概念。

在其他Unix操作系统里Load Average只考虑CPU部分Load Average计算的是进程调度器中可运行队列Running Queue里的一段时间1分钟5分钟15分钟的平均进程数目而Linux在这个基础上又加上了进程调度器中休眠队列Sleeping Queue里的一段时间的TASK_UNINTERRUPTIBLE状态的平均进程数目。

这里你需要重点掌握Load Average的计算公式如下图。

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

最后我还想强调一下这一讲中提到的对D状态进程进行监控也很重要因为这是通用系统性能的监控方法。

思考题

结合今天的学习你可以自己动手感受一下Load Average是怎么产生的请你创建一个容器在容器中运行一个消耗100%CPU的进程运行10分钟后然后查看Load Average的值。

欢迎在留言区晒出你的经历和疑问。如果有收获,也欢迎你把这篇文章分享给你的朋友,一起学习和讨论。