本节从计算资源管理(Compute Resources)、服务质量管理(QoS)、资源配额管理(LimitRange、ResourceQuota)等方面对Kubernetes集群中的资源管理进行详细说明,并结合实践操作、常见问题分析和一个完整示例,对Kubernetes集群资源管理相关的运维工作提供参考。
Kubernetes集群里的节点提供的资源主要是计算资源,计算资源是可计量的能被申请、分配和使用的基础资源,这使之区别于API资源(API Resources,例如Pod和Services等)。当前Kubernetes集群中的计算资源主要包括CPU、GPU及Memory,绝大多数常规应用是用不到GPU的,因此这里重点介绍CPU与Memory的资源管理问题。在一般情况下,我们在定义Pod时并没有限制Pod所占用的CPU和内存数量,此时Kubernetes会认为该Pod所需的资源很少,可以将其调度到任何可用的节点上,这样一来,当集群中的计算资源不很充足时,比如集群中的Pod负载突然加大,就会使某个节点的资源严重不足。为了避免系统挂掉,该节点(操作系统)会选择“杀掉”某些用户进程来释放资源,避免操作系统崩溃,若操作系统崩溃,则每个Pod都可能成为牺牲品。因此,Kubernetes需要有一套完备的资源配额限制及对应的Pod服务等级机制,来避免这种灾难的发生。Kubernetes给出了如下解决思路。
(1)可以全面限制一个应用及其中的Pod所能占用的资源配额。具体包括三种方式:
◎ 定义每个Pod上资源配额相关的参数,比如CPU/Memory Request/Limit;
◎ 自动为每个没有定义资源配额的Pod添加资源配额模板(LimitRange);
◎ 从总量上限制一个租户(应用)所能使用的资源配额的ResourceQuota。
(2)允许集群的资源被超额分配,以提高集群的资源利用率,同时允许用户根据业务的优先级,为不同的Pod定义相应的服务保障等级(QoS)。我们可以将Qos理解为“活命优先级”,当系统资源不足时,低等级的Pod会被操作系统自动清理,以确保高等级的Pod稳定运行。
我们知道,一个程序所使用的CPU与Memory是一个动态的量,确切地说,是一个范围,跟它的负载密切相关:负载增加时,CPU和Memory的使用量也会增加。因此最准确的说法是,某个进程的CPU使用量为0.1个CPU(Request)~1个CPU(Limit),内存占用则为500MB(Reuqest)~1GB(Limit)。对应到Kubernetes的Pod容器上,就是如下4个参数。
◎ spec.container[].resources.requests.cpu:容器初始要求的CPU数量。
◎ spec.container[].resources.limits.cpu:容器所能使用的最大CPU数量。
◎ spec.container[].resources.requests.memory:容器初始要求的内存数量。
◎ spec.container[].resources.limits.memory:容器所能使用的最大内存数量。
其中,limits对应资源量的上限,即最多允许使用这个上限的资源量。由于CPU资源是可压缩的,进程无论如何也不可能突破上限,因此设置起来比较容易。对于Memory这种不可压缩的资源来说,它的Limit设置是一个问题,如果设置得小了,则进程在业务繁忙期试图请求超过Limit限制的Memory时会被操作系统“杀掉”。因此,Memory的Request与Limit的值需要结合进程的实际需求谨慎设置。如果不设置CPU或Memory的Limit值,则会怎样呢?在这种情况下,该Pod的资源使用量有一个弹性范围,我们不用绞尽脑汁去思考这两个Limit的合理值,但问题也来了,考虑下面的例子:
Pod A的Memory Request被设置为1GB,Node A当时空闲的Memory为1.2GB,符合Pod A的需求,因此Pod A被调度到Node A上。运行3天后,Pod A的访问请求大增,内存需要增加到1.5GB,此时Node A的剩余内存只有200MB,由于Pod A新增的内存已经超出系统资源范围,所以Pod A在这种情况下会被Kubernetes“杀掉”。
没有设置Limit的Pod,或者只设置了CPU Limit或者Memory Limit两者之一的Pod,看起来都是很有弹性的,但实际上,与4个参数都被设置了的Pod相比,它们处于一种不稳定状态,只是稳定一点儿而已。理解了这一点,就很容易理解Resource QoS问题了。
如果我们有成百上千个不同的Pod,那么先手动设置每个Pod的这4个参数,再检查并确保这些参数的设置,都是合理的。比如不能出现内存超过2GB或者CPU占据两个核心的Pod。最后还得手工检查不同租户(命名空间)下的Pod资源使用量是否超过限额。为此,Kubernetes提供了另外两个相关对象:LimitRange及ResourceQuota,前者解决了没有设置配额参数的Pod的默认资源配额问题,同时是Pod资源配额设置的合法性校验参考;后者则约束租户的资源总量配额问题。
以CPU为例,图10.3显示了未设置Limits与设置了Requests和Limits的CPU使用率的区别。

图10.3 未设置Limits与设置了Requests和Limits的CPU使用率的区别
尽管Requests和Limits只能被设置到容器上,但是设置了Pod级别的Requests和Limits能大大提高管理Pod的便利性和灵活性,因此在Kubernetes中提供了对Pod级别的Requests和Limits的配置。对于CPU和内存而言,Pod的Requests或Limits指该Pod中所有容器的Requests或Limits的总和(对于Pod中没有设置Requests或Limits的容器,该项的值被当作0或者按照集群配置的默认值来计算)。下面对CPU和内存这两种计算资源的特点进行说明。
CPU的Requests和Limits是通过CPU数(cpus)来度量的。CPU的资源值是绝对值,而不是相对值,比如0.1CPU在单核或多核机器上是一样的,都严格等于0.1 CPU core。
内存的Requests和Limits计量单位是字节数。使用整数或者定点整数加上国际单位制(International System of Units)来表示内存值。国际单位制包括十进制的E、P、T、G、M、K、m,或二进制的Ei、Pi、Ti、Gi、Mi、Ki。KiB与MiB是以二进制表示的字节单位,常见的KB与MB则是以十进制表示的字节单位,比如:
◎ 1 KB(KiloByte)=1000 Bytes=8000 Bits;
◎ 1 KiB(KibiByte)=210 Bytes=1024 Bytes=8192 Bits。
因此,128974848、129e6、129M、123Mi的内存配置是一样的。
Kubernetes的计算资源单位是大小写敏感的,因为 m可以表示千分之一单位(milli unit),而M可以表示十进制的1000,二者的含义不同;同理,小写的k不是一个合法的资源单位。
以某个Pod中的资源配置为例:

如上所示,该Pod包含两个容器,每个容器配置的Requests都是0.25CPU和64MiB(226 Bytes)内存,而配置的Limits都是0.5CPU和128MiB(227 Bytes)内存。
这个Pod的Requests和Limits等于Pod中所有容器对应配置的总和,所以Pod的Requests是0.5CPU和128MiB(227 Bytes)内存,Limits是1CPU和256MiB(228 Bytes)内存。
当一个Pod创建成功时,Kubernetes调度器(Scheduler)会为该Pod选择一个节点来执行。对于每种计算资源(CPU和Memory)而言,每个节点都有一个能用于运行Pod的最大容量值。调度器在调度时,首先要确保调度后该节点上所有Pod的CPU和内存的Requests总和,不超过该节点能提供给Pod使用的CPU和Memory的最大容量值。
例如,某个节点上的CPU资源充足,而内存为4GB,其中3GB可以运行Pod,而某Pod的Memory Requests为1GB、Limits为2GB,那么在这个节点上最多可以运行3个这样的Pod。
这里需要注意:可能某节点上的实际资源使用量非常低,但是已运行Pod配置的Requests值的总和非常高,再加上需要调度的Pod的Requests值,会超过该节点提供给Pod的资源容量上限,这时Kubernetes仍然不会将Pod调度到该节点上。如果Kubernetes将Pod调度到该节点上,之后该节点上运行的Pod又面临服务峰值等情况,就可能导致Pod资源短缺。
接着上面的例子,假设该节点已经启动3个Pod实例,而这3个Pod的实际内存使用都不足 500MB,那么理论上该节点的可用内存应该大于1.5GB。但是由于该节点的Pod Requests总和已经达到节点的可用内存上限,因此Kubernetes不会再将任何Pod实例调度到该节点上。
kubelet在启动Pod的某个容器时,会将容器的Requests和Limits值转化为相应的容器启动参数传递给容器执行器(Docker或者rkt)。
如果容器的执行环境是Docker,那么容器的4个参数传递给Docker的过程如下。
1)spec.container[].resources.requests.cpu
这个参数值会被转化为core数(比如配置的100m会转化为0.1),然后乘以1024,再将这个结果作为--cpu-shares参数的值传递给docker run命令。在docker run命令中,--cpu-share参数是一个相对权重值(Relative Weight),这个相对权重值会决定Docker在资源竞争时分配给容器的资源比例。
这里举例说明--cpu-shares参数在Docker中的含义:比如将两个容器的CPU Requests分别设置为1和2,那么容器在docker run启动时对应的--cpu-shares参数值分别为1024和2048,在主机CPU资源产生竞争时,Docker会尝试按照1:2的配比将CPU资源分配给这两个容器使用。
这里需要区分清楚的是:这个参数对于Kubernetes而言是绝对值,主要用于Kubernetes调度和管理,Kubernetes同时会将这个参数的值传递给docker run的--cpu-shares参数。--cpu-shares参数对于Docker而言是相对值,主要用于设置资源分配比例。
2)spec.container[].resources.limits.cpu
这个参数值会被转化为millicore数(比如配置的1被转化为1000,而配置的100m被转化为100),将此值乘以100000,再除以1000,然后将结果值作为--cpu-quota参数的值传递给docker run命令。docker run命令中的另一个参数--cpu-period默认被设置为100000,表示Docker重新计量和分配CPU的使用时间间隔为100000μs(100ms)。
Docker的--cpu-quota参数和--cpu-period参数一起配合完成对容器CPU的使用限制:比如在Kubernetes中配置容器的CPU Limits为0.1,那么计算后--cpu-quota为10000,而--cpu-period为100000,这意味着Docker在100ms内最多给该容器分配10ms×core的计算资源用量,10/100=0.1 core的结果与Kubernetes配置的意义是一致的。
注意:如果kubelet服务的启动参数--cpu-cfs-quota被设置为true,那么kubelet会强制要求所有Pod都必须配置CPU Limits(如果没有配置Pod,则集群提供了默认配置也可以)。从Kubernetes 1.2版本开始,这个--cpu-cfs-quota启动参数的默认值就是true。
3)spec.container[].resources.requests.memory
这个参数值只提供给Kubernetes调度器作为调度和管理的依据,不会作为任何参数传递给Docker。
4)spec.container[].resources.limits.memory
这个参数值会被转化为单位为Bytes的整数,值作为--memory参数传递给docker run命令。
如果一个容器在运行过程中使用了超出了其内存Limits配置的内存限制值,那么它可能会被“杀掉”;如果这个容器是一个可重启的容器,那么它在之后会被kubelet重新启动。因此对容器的Limits配置需要进行准确测试和评估。
与内存Limits不同的是,CPU在容器技术中属于可压缩资源,因此对CPU的Limits配置一般不会因为偶然超标使用而导致容器被系统“杀掉”。
Pod的资源用量会作为Pod的状态信息一同上报给Master。如果在集群中配置了Heapster来监控集群的性能数据,那么还可以从Heapster中查看Pod的资源用量信息。
(1)Pod状态为Pending,错误信息为FailedScheduling。如果Kubernetes调度器在集群中找不到合适的节点来运行Pod,那么这个Pod会一直处于未调度状态,直到调度器找到合适的节点为止。每次调度器尝试调度失败时,Kubernetes都会产生一个事件,我们可以通过下面这种方式来查看事件的信息:

在上面这个例子中,名为frontend的Pod由于节点的CPU资源不足而调度失败(Pod ExceedsFreeCPU),同样,如果内存不足,则也可能导致调度失败(PodExceedsFreeMemory)。
如果一个或者多个Pod调度失败且有这类错误,那么可以尝试以下几种解决方法。
◎ 添加更多的节点到集群中。
◎ 停止一些不必要的运行中的Pod,释放资源。
◎ 检查Pod的配置,错误的配置可能导致该Pod永远无法被调度执行。比如整个集群中的所有节点都只有1 CPU,而Pod配置的CPU Requests为2,该Pod就不会被调度执行。
我们可以使用kubectl describe nodes命令来查看集群中节点的计算资源容量和已使用量:


超过可用资源容量上限(Capacity)和已分配资源量(Allocated resources)差额的Pod无法运行在该Node上。在这个例子中,如果一个Pod的Requests超过90 millicpus或者1646MiB内存,就无法运行在这个节点上。
(2)容器被强行终止(Terminated)。如果容器使用的资源超过了它配置的Limits,那么该容器可能被强制终止。我们可以通过kubectl describe pod命令来确认容器是否因为这个原因被终止:


Restart Count:5说明这个名为simmemleak的容器被强制终止并重启了5次。
我们可以在使用kubectl get pod命令时添加-o go-template=...格式的参数来读取已终止容器之前的状态信息:

可以看到这个容器因为reason:OOM Killed而被强制终止,说明这个容器的内存超出了限制(Out of Memory)。
在计算机发展的早期阶段,程序员是直接对内存物理地址编程的,需要自己管理内存,所以很容易由于内存地址错误导致操作系统崩溃,而且存在一些恶意程序对操作系统进行破坏。后来人们将硬件和软件(操作系统)结合,推出虚拟地址和内存页的概念,以及CPU的逻辑内存地址与物理内存(条)地址的映射关系。
在现代操作系统中,内存是以Page(页,有时也可以称之为Block)为单位进行管理的,而不以字节为单位,包括内存的分配和回收都基于Page进行。典型的Page大小为4KB,因此用户进程申请1MB内存,就需要操作系统分配256个Page,而1GB内存对应26万多个Page!
为了实现快速内存寻址,CPU内部以硬件方式实现了一个高性能的内存地址映射的缓存表——TLB(Translation Lookaside Buffer),用来保存逻辑内存地址与物理内存的对应关系。若目标地址的内存页物理地址不在TLB的缓存中或者TLB中的缓存记录失效,CPU就需要切换到低速的以软件方式实现的内存地址映射表进行内存寻址,这将大大降低CPU的运算速度。针对缓存条目有限的TLB缓存表,提高TLB效率的最佳办法就是将内存页增加,这样一来,一个进程所需的内存页数量会相应地减少很多。如果把内存页从默认的4KB改为2MB,那么1GB内存就只对应512个内存页了,TLB的缓存命中率会大大增加。这是不是意味着我们可以任意指定内存页的大小,比如1314MB的内存页?答案是否定的,因为这是由CPU来决定的,比如常见的Intel x86处理器可以支持的大内存页通常是2MB,个别型号的高端处理器则支持1GB的大内存页。
在Linux平台下,对于那些需要大量内存(1GB以上内存)的程序来说,大内存页的优势是很明显的,因为Huge Page大大提升了TLB的缓存命中率,又因为Linux对Huge Page提供了更为简单、便捷的操作接口,所以可以把它当作文件进行读写操作。Linux使用Huge Page文件系统hugetlbfs支持巨页,这种方式更为灵活,我们可以设置Huge Page的大小,比如1GB、2GB甚至2.5GB(前提是硬件和操作系统支持),然后设置有多少物理内存用于分配Huge Page,这样就设置了一些预先分配好的Huge Page。可以将hugetlbfs文件系统挂载到/mnt/huge目录下,通过执行下面的指令完成设置:

在完成设置后,用户进程就可以使用mmap映射Huge Page目标文件来使用大内存页了,Intel DPDK便采用了这种做法。有测试表明,应用使用大内存页比使用4KB的内存页,性能提高了10%~15%。
我们可以将Huge Page理解为一种特殊的计算资源:拥有大内存页的资源。而拥有Huge Page资源的Node也与拥有GPU资源的Node一样,属于一种新的可调度资源节点(Schedulable Resource Node),其上的kubelet进程需要报告自身的Huge Page相关的资源信息到Kubernetes Master,以供Scheduler调度器使用,将需要Huge Page资源的Pod调度到符合要求的目标节点上。在Kubernetes 1.14中,对Linux Huge Page的支持正式更新为GA稳定版本。
Huge Page类似于CPU或者Memory资源,但不同于CPU或者Memory,Huge Page资源属于不可超限使用的资源,也支持ResourceQuota实现配额限制。为此,Kubernetes引入了一个新的资源类型hugepages-<size>,来表示大内存页这种特殊的资源,比如hugepages-2Mi表示2MiB规格的大内存页资源。一个能提供2MiB规格Huge Page的Node,会上报自己拥有Hugepages-2Mi的大内存页资源属性,供需要这种规格的大内存页资源的Pod使用。
需要Huge Page资源的Pod只要给出相关Huge Page的声明,就可以被正确调度到匹配的目标Node上了,如下所示:


在上面的定义中有以下几个关键点:
◎ Huge Page需要被映射到Pod的文件系统中;
◎ Huge Page申请的request与limit必须相同,即申请固定大小的Huge Page,不能是可变的;
◎ 在目前的版本中,Huge Page属于Pod级别的资源,未来计划成为Container级别的资源,即实现更细粒度的资源管理;
◎ 存储卷emptyDir(挂载到容器内的/hugepages目录)的后台是由Huge Page支持的,因此应用不能使用超过request声明的内存大小。
如果需要更大的Huge Page,则可以在Pod的Voume声明中用medium:HugePages-<size>来表明,比如在下面这段代码中分别申请了2Mi与1Gi的Huge Page:

在Kubernetes未来的版本中计划继续实现下面的一些高级特性:
◎ 支持容器级别的Huge Page的隔离能力;
◎ 支持NUMA亲和能力,以提升服务的质量;
◎ 支持LimitRange配置Huge Page资源限额。
在默认情况下,Kubernetes不会对Pod加上CPU和内存限制,这意味着Kubernetes系统中的任何Pod都可以使用其所在节点所有可用的CPU和内存。通过配置Pod的计算资源Requests和Limits,我们可以限制Pod的资源使用,但对于Kubernetes集群管理员而言,配置每一个Pod的Requests和Limits是很烦琐的,而且很受限制。更多时候,我们需要对集群内Requests和Limits的配置做一个全局限制。常见的配置场景如下。
◎ 集群中的每个节点都有2GB内存,集群管理员不希望任何Pod申请超过2GB的内存,因为在整个集群中都没有任何节点能满足超过2GB内存的请求。如果某个Pod的内存配置超过2GB,那么该Pod将永远无法被调度到任何节点上执行。为了防止这种情况的发生,集群管理员希望能在系统管理功能中设置禁止Pod申请超过2GB内存。
◎ 集群由同一个组织中的两个团队共享,分别运行生产环境和开发环境。生产环境最多可以使用8GB内存,而开发环境最多可以使用512MB内存。集群管理员希望通过为这两个环境创建不同的命名空间,并为每个命名空间都设置不同的限制来满足这个需求。
◎ 用户创建Pod时使用的资源可能会刚好比整个机器资源的上限稍小,而恰好剩下的资源大小非常尴尬,不足以运行其他任务,但整个集群加起来又非常浪费。因此,集群管理员希望将每个Pod都设置为必须至少使用集群平均资源值(CPU和内存)的20%,这样集群就能够提供更好的资源一致性调度,从而减少资源浪费。
针对这些需求,Kubernetes提供了LimitRange机制对Pod和容器的Requests和Limits配置做进一步限制。在下面的示例中首先说明如何将LimitsRange应用到一个Kubernetes的命名空间中,然后说明LimitRange的几种限制方式,比如最大及最小范围、Requests和Limits的默认值、Limits与Requests的最大比例上限,等等。下面通过LimitRange的设置和应用对其进行说明。
创建一个名为limit-example的Namespace:

为命名空间limit-example创建一个简单的LimitRange。创建limits.yaml配置文件,内容如下:


创建该LimitRange:

查看namespace limit-example中的LimitRange:


下面解释LimitRange中各项配置的意义和特点。
(1)不论是CPU还是内存,在LimitRange中,Pod和Container都可以设置Min、Max和Max Limit/Requests Ratio参数。Container还可以设置Default Request和Default Limit参数,而Pod不能设置Default Request和Default Limit参数。
(2)对Pod和Container的参数解释如下。
◎ Container的Min(上面的100m和3Mi)是Pod中所有容器的Requests值下限;Container的Max(上面的2和1Gi)是Pod中所有容器的Limits值上限;Container的Default Request(上面的200m和100Mi)是Pod中所有未指定Requests值的容器的默认Requests值;Container的Default Limit(上面的300m和200Mi)是Pod中所有未指定Limits值的容器的默认Limits值。对于同一资源类型,这4个参数必须满足以下关系:Min≤Default Request≤Default Limit≤Max。
◎ Pod的Min(上面的200m和6Mi)是Pod中所有容器的Requests值的总和下限;Pod的Max(上面的4和2Gi)是Pod中所有容器的Limits值的总和上限。容器未指定Requests值或者Limits值时,将使用Container的Default Request值或者Default Limit值。
◎ Container的Max Limit/Requests Ratio(上面的5和4)限制了Pod中所有容器的Limits值与Requests值的比例上限;而Pod的Max Limit/Requests Ratio(上面的3和2)限制了Pod中所有容器的Limits值总和与Requests值总和的比例上限。
(3)如果设置了Container的Max,那么对于该类资源而言,整个集群中的所有容器都必须设置Limits,否则无法成功创建。Pod内的容器未配置Limits时,将使用Default Limit的值(本例中的300m CPU和200MiB内存),如果也未配置Default,则无法成功创建。
(4)如果设置了Container的Min,那么对于该类资源而言,整个集群中的所有容器都必须设置Requests。如果创建Pod的容器时未配置该类资源的Requests,那么在创建过程中会报验证错误。Pod里容器的Requests在未配置时,可以使用默认值defaultRequest(本例中的200m CPU和100MiB内存);如果未配置而且没有使用默认值defaultRequest,那么默认等于该容器的Limits;如果容器的Limits也未定义,就会报错。
(5)对于任意一个Pod而言,该Pod中所有容器的Requests总和都必须大于或等于6MiB,而且所有容器的Limits总和都必须小于或等于 1GiB;同样,所有容器的CPU Requests总和都必须大于或等于200m,而且所有容器的CPU Limits总和都必须小于或等于2。
(6)Pod里任何容器的Limits与Requests的比例都不能超过Container的Max Limit/Requests Ratio;Pod里所有容器的Limits总和与Requests总和的比例都不能超过Pod的Max Limit/Requests Ratio。
最后,让我们看看LimitRange生效时对容器的资源限制效果。
命名空间中的LimitRange只会在Pod创建或者更新时执行检查。如果手动修改LimitRange为一个新的值,那么这个新的值不会去检查或限制之前已经在该命名空间中创建好的Pod。
如果在创建Pod时配置的资源值(CPU或者内存)超出了LimitRange的限制,那么该创建过程会报错,在错误信息中会说明详细的错误原因。
下面通过创建一个单容器Pod来展示默认限制是如何被配置到Pod上的:

查看已创建的Pod:

查看该Pod的resources相关信息:


由于该Pod未配置资源Requests和Limits,所以使用了namespace limit-example中的默认CPU和内存定义的Requests和Limits值。
下面创建一个超出资源限制的Pod(使用3 CPU):

创建该Pod,可以看到系统报错,并且提供的错误原因为超过资源限制:

接下来的例子展示了LimitRange对maxLimitRequestRatio的限制过程:


由于limit-test-nginx这个Pod的全部内存Limits总和与Requests总和的比例为512:250,大于在LimitRange中定义的Pod的最大比率2(maxLimitRequestRatio.memory=2),因此创建失败:

下面的例子为满足LimitRange限制的Pod:

创建Pod将会成功:

查看该Pod的资源信息:

可以看到该Pod配置了明确的Limits和Requests,因此该Pod不会使用在namespace limit-example中定义的default和defaultRequest。
需要注意的是,CPU Limits强制配置这个选项在Kubernetes集群中默认是开启的;除非集群管理员在部署kubelet服务时通过设置参数--cpu-cfs-quota=false来关闭该限制:

如果集群管理员希望对整个集群中容器或者Pod配置的Requests和Limits做限制,就可以通过配置Kubernetes命名空间中的LimitRange来达到该目的。在Kubernetes集群中,如果Pod没有显式定义Limits和Requests,那么Kubernetes系统会将该Pod所在的命名空间中定义的LimitRange的default和defaultRequests配置到该Pod上。
本节对Kubernetes如何根据Pod的Requests和Limits配置来实现针对Pod的不同级别的资源服务质量控制(QoS)进行说明。
在Kubernetes的QoS体系中,需要保证高可靠性的Pod可以申请可靠资源,而一些非高可靠性的Pod可以申请可靠性较低或者不可靠的资源。在10.4.1节中讲到了容器的资源配置分为Requests和Limits,其中Requests是Kubernetes调度时能为容器提供的完全、可保障的资源量(最低保障),而Limits是系统允许容器运行时可能使用的资源量的上限(最高上限)。Pod级别的资源配置是通过计算Pod内所有容器的资源配置的总和得出来的。
Kubernetes中Pod的Requests和Limits资源配置有如下特点。
(1)如果Pod配置的Requests值等于Limits值,那么该Pod可以获得的资源是完全可靠的。
(2)如果Pod的Requests值小于Limits值,那么该Pod获得的资源可分为两部分:
◎ 完全可靠的资源,资源量的大小等于Requests值;
◎ 不可靠的资源,资源量最大等于Limits与Requests的差额,这份不可靠的资源能够申请到多少,取决于当时主机上容器可用资源的余量。
通过这种机制,Kubernetes可以实现节点资源的超售(Over Subscription),比如在CPU完全充足的情况下,某机器共有32GiB内存可供容器使用,容器配置为Requests值1GiB、Limits值2GiB,那么在该机器上最多可以同时运行32个容器,每个容器最多可以使用2GiB内存,如果这些容器的内存使用峰值能错开,那么所有容器都可以正常运行。
超售机制能有效提高资源的利用率,也不会影响容器申请的完全可靠资源的可靠性。
根据前面的内容可知,容器的资源配置满足以下两个条件:
◎ Requests≤节点可用资源;
◎ Requests≤Limits。
Kubernetes根据Pod配置的Requests值来调度Pod,Pod在成功调度之后会得到Requests值定义的资源来运行;如果Pod所在机器上的资源有空余,则Pod可以申请更多的资源,最多不能超过Limits的值。下面看一下Requests和Limits针对不同计算资源类型的限制机制的差异。这种差异主要取决于计算资源类型是可压缩资源还是不可压缩资源。
◎ Kubernetes目前支持的可压缩资源是CPU。
◎ Pod可以得到Requests配置的CPU使用量,而能否使用超过Requests值的部分取决于系统的负载和调度。不过由于目前Kubernetes和Docker的CPU隔离机制都是在容器级别起作用的,所以Pod级别的资源配置并不能完全得到保障;Pod级别的cgroups正在紧锣密鼓地开发中,如果将来引入,就可以确保Pod级别的资源配置准确运行。
◎ 空闲的CPU资源按照容器Requests值的比例分配。举例说明:容器A的CPU配置为Requests 1 Limits 10,容器B的CPU配置为Request 2 Limits 8,A和B同时运行在一个节点上,初始状态下容器的可用CPU为3cores,那么A和B恰好得到在其Requests中定义的CPU用量,即1CPU和2CPU。如果A和B都需要更多的CPU资源,而恰好此时系统的其他任务释放了1.5CPU,那么这1.5CPU将按照A和B的Requests值的比例1:2分配给A和B,即最终A可使用1.5CPU,B可使用3CPU。
◎ 如果Pod的CPU用量超过了在Limits 10中配置的CPU用量,那么cgroups会对Pod中容器的CPU用量进行限流(Throttled);如果Pod没有配置Limits 10,那么Pod会尝试抢占所有空闲的CPU资源(Kubernetes从1.2版本开始默认开启--cpu-cfs-quota,因此在默认情况下必须配置Limits)。
◎ Kubernetes目前支持的不可压缩资源是内存。
◎ Pod可以得到在Requests中配置的内存。如果Pod的内存用量小于它的Requests的配置,那么这个Pod可以正常运行(除非出现操作系统级别内存不足等严重问题);如果Pod的内存用量超过了它的Requests配置,那么这个Pod有可能被Kubernetes“杀掉”:比如Pod A使用了超过Requests而不到Limits的内存量,此时同一机器上另一个Pod B之前只使用了远少于自己的Requests值的内存,此时程序压力增大,Pod B向系统申请的总量不超过自己的Requests值的内存,那么Kubernetes可能会直接“杀掉”Pod A;另外一种情况是Pod A使用了超过Requests而不到Limits的内存,此时Kubernetes将一个新的Pod调度到这台机器上,新的Pod需要使用内存,而只有Pod A使用了超过了自己的Requests值的内存,那么Kubernetes也可能会“杀掉”Pod A来释放内存资源。
◎ 如果Pod的内存用量超过了它的Limits设置,那么操作系统内核会“杀掉”Pod所有容器的所有进程中内存使用量最多的一个,直到内存不超过Limits时为止。
◎ Kubernetes的kube-scheduler通过计算Pod中所有容器的Requests的总和来决定对Pod的调度。
◎ 不管是CPU还是内存,Kubernetes调度器和kubelet都会确保节点上所有Pod的Requests总和不会超过在该节点上可分配给容器使用的资源容量上限。
在一个超用(Over Committed,容器Limits总和大于系统容量上限)系统中,容器负载的波动可能导致操作系统的资源不足,最终导致部分容器被“杀掉”。在这种情况下,我们当然会希望优先“杀掉”那些不太重要的容器,那么如何衡量重要程度呢?Kubernetes将容器划分成3个QoS等级:Guaranteed(完全可靠的)、Burstable(弹性波动、较可靠的)和BestEffort(尽力而为、不太可靠的),这三种优先级依次递减,如图10.4所示。

图10.4 QoS等级和优先级的关系
从理论上来说,QoS级别应该作为一个单独的参数来提供API,并由用户对Pod进行配置,这种配置应该与Requests和Limits无关。但在当前版本的Kubernetes设计中,为了简化模式及避免引入太多的复杂性,QoS级别直接由Requests和Limits定义。在Kubernetes中,容器的QoS级别等于容器所在Pod的QoS级别,而Kubernetes的资源配置定义了Pod的三种QoS级别,如下所述。
如果Pod中的所有容器对所有资源类型都定义了Limits和Requests,并且所有容器的Limits值都和Requests值相等(且都不为0),那么该Pod的QoS级别就是Guaranteed。注意:在这种情况下,容器可以不定义Requests,因为Requests值在未定义时默认等于Limits。
在下面这两个例子中定义的Pod QoS级别就是Guaranteed。
例一,未定义Requests值,所以其默认等于Limits值:

例二,其中定义的Requests与Limits的值完全相同:

如果Pod中所有容器都未定义资源配置(Requests和Limits都未定义),那么该Pod的QoS级别就是BestEffort。例如下面这个Pod定义:

当一个Pod既不为Guaranteed级别,也不为BestEffort级别时,该Pod的QoS级别就是Burstable。Burstable级别的Pod涉及两种情况。第1种情况:Pod中的一部分容器在一种或多种资源类型的资源配置中定义了Requests值和Limits值(都不为0),且Requests值小于Limits值;第2种情况:Pod中的一部分容器未定义资源配置(Requests和Limits都未定义)。注意:在容器未定义Limits时,Limits值默认等于节点资源容量的上限。
下面几个例子中的Pod的QoS等级都是Burstable。
(1)容器foo的CPU Requests不等于Limits:

(2)容器bar未定义资源配置,而容器foo定义了资源配置:


(3)容器foo未定义CPU,而容器bar未定义内存:

(4)容器bar未定义资源配置,而容器foo未定义Limits值:

在Pod的CPU Requests无法得到满足(比如节点的系统级任务占用过多的CPU导致无法分配足够的CPU给容器使用)时,容器得到的CPU会被压缩限流。
由于内存是不可压缩的资源,所以针对内存资源紧缺的情况,会按照以下逻辑处理。
(1)BestEffort Pod的优先级最低,在这类Pod中运行的进程会在系统内存紧缺时被第一优先“杀掉”。当然,从另一个角度来看,BestEffort Pod由于没有设置资源Limits,所以在资源充足时,它们可以充分使用所有闲置资源。
(2)Burstable Pod的优先级居中,这类Pod在初始时会被分配较少的可靠资源,但可以按需申请更多的资源。当然,如果整个系统内存紧缺,又没有BestEffort容器可以被杀掉以释放资源,那么这类Pod中的进程可能被“杀掉”。
(3)Guaranteed Pod的优先级最高,而且一般情况下这类Pod只要不超过其资源Limits的限制就不会被“杀掉”。当然,如果整个系统内存紧缺,又没有其他更低优先级的容器可以被“杀掉”以释放资源,那么这类Pod中的进程也可能会被“杀掉”。
OOM(Out Of Memory)计分规则包括如下内容。
◎ OOM计分的计算方法:计算进程所使用的内存在系统中所占的百分比,取其中不含百分号的数值,再乘以 10,该结果是进程OOM的基础分;将进程OOM基础分的分值再加上这个进程的OOM_SCORE_ADJ(分数调整)值,作为进程OOM的最终分值(除root启动的进程外)。在系统发生OOM时,OOM Killer会优先“杀掉”OOM计分更高的进程。
◎ 进程的OOM计分的基本分数值范围是0~1000,如果A进程的调整值OOM_SCORE_ADJ减去B进程的调整值的结果大于1000,那么A进程的OOM计分最终值必然大于B进程,会优先“杀掉”A进程。
◎ 不论调整OOM_SCORE_ADJ值为多少,任何进程的最终分值范围也是0~1000。
在Kubernetes中,不同QoS的OOM计分调整值如表10.1所示。
表10.1 不同QoS的OOM计分调整值

对表中内容说明如下。
◎ BestEffort Pod设置OOM_SCORE_ADJ调整值为1000,因此BestEffort Pod中容器里所有进程的OOM最终分肯定是1000。
◎ Guaranteed Pod设置OOM_SCORE_ADJ调整值为-998,因此Guaranteed Pod中容器里所有进程的OOM最终分一般是0或者1(因为基础分不可能是1000)。
◎ 对Burstable Pod规则分情况说明:如果Burstable Pod的内存Requests超过系统可用内存的99.8%,那么这个Pod的OOM_SCORE_ADJ调整值固定为2;否则,设置OOM_SCORE_ADJ调整值为1000-10×(% of memory requested);如果内存Requests为0,那么OOM_SCORE_ADJ调整值固定为999。这样的规则能确保OOM_SCORE_ADJ调整值的范围为2~999,而Burstable Pod中所有进程的OOM最终分数范围为2~1000。Burstable Pod进程的OOM最终分数始终大于Guaranteed Pod的进程得分,因此它们会被优先“杀掉”。如果一个Burstable Pod使用的内存比它的内存Requests少,那么可以肯定的是,它的所有进程的OOM最终分数会小于1000,此时能确保它的优先级高于BestEffort Pod。如果在一个Burstable Pod的某个容器中某个进程使用的内存比容器的Requests值高,那么这个进程的OOM最终分数会是1000,否则它的OOM最终分数会小于1000。假设在下面的容器中有一个占用内存非常大的进程,那么当一个使用内存超过其Requests的Burstable Pod与另外一个使用内存少于其Requests的Burstable Pod发生内存竞争冲突时,前者的进程会被系统“杀掉”。如果在一个Burstable Pod内部有多个进程的多个容器发生内存竞争冲突,那么此时OOM评分只能作为参考,不能保证完全按照资源配置的定义来执行OOM Kill。
OOM还有一些特殊的计分规则,如下所述。
◎ kubelet进程和Docker进程的调整值OOM_SCORE_ADJ为-998。
◎ 如果配置进程调整值OOM_SCORE_ADJ为-999,那么这类进程不会被OOM Killer“杀掉”。
目前Kubernetes基于QoS的超用机制日趋完善,但还有一些限制。
(1)不支持内存Swap,当前的QoS策略都假定了主机不启用内存Swap,Kubernetes从1.8版本开始默认关闭Swap特性,但如果主机启用了Swap功能,上面的QoS策略就可能失效。举例说明:如果两个Guaranteed Pod都刚好达到了内存Limits,那么由于内存Swap机制,它们还可以继续申请使用更多的内存。如果Swap空间不足,那么最终这两个Pod中的进程可能被“杀掉”。
(2)缺乏更丰富的QoS策略,当前的QoS策略都是基于Pod的资源配置(Requests和Limits)来定义的,而资源配置本身又承担着对Pod资源管理和限制的功能。两种不同维度的功能使用同一个参数来配置,可能会导致某些复杂需求无法被满足,比如当前Kubernetes无法支持弹性的、高优先级的Pod。自定义QoS优先级能提供更大的灵活性,完美地实现各类需求,但同时会引入更高的复杂性,而且过于灵活的设置会给予用户过高的权限,对系统管理也提出了更大的挑战。
如果一个Kubernetes集群被多个用户或者多个团队共享,就需要考虑资源公平使用的问题,因为某个用户可能会使用超过基于公平原则分配给其的资源量。
Resource Quotas就是解决这个问题的工具。通过ResourceQuota对象,我们可以定义资源配额,这个资源配额可以为每个命名空间都提供一个总体的资源使用限制:它可以限制命名空间中某种类型的对象的总数量上限,也可以设置命名空间中Pod可以使用的计算资源的总上限。
典型的资源配额使用方式如下。
◎ 不同的团队工作在不同的命名空间中,目前这是非约束性的,在未来的版本中可能会通过ACL(Access Control List,访问控制列表)来实现强制性约束。
◎ 集群管理员为集群中的每个命名空间都创建一个或者多个资源配额项。
◎ 当用户在命名空间中使用资源(创建Pod或者Service等)时,Kubernetes的配额系统会统计、监控和检查资源用量,以确保使用的资源用量没有超过资源配额的配置。
◎ 如果在创建或者更新应用时资源使用超出了某项资源配额的限制,那么创建或者更新的请求会报错(HTTP 403 Forbidden),并给出详细的出错原因说明。
◎ 如果命名空间中计算资源(CPU和内存)的资源配额已启用,那么用户必须为相应的资源类型设置Requests或Limits,否则配额系统可能会直接拒绝Pod的创建。这里可以使用LimitRange机制来为没有配置资源的Pod提供默认的资源配置。
下面的例子展示了一个非常适合使用资源配额来做资源控制管理的场景。
◎ 集群共有32GB内存和16 CPU,两个小组。A小组使用20GB内存和10 CPU,B小组使用10GB内存和2 CPU,剩下的2GB内存和2 CPU作为预留资源。
◎ 在名为testing的命名空间中限制使用1 CPU和1GB内存;在名为production的命名空间中,资源使用不受限制。
在使用资源配额时,需要注意以下两点。
◎ 如果集群中总的可用资源小于各命名空间中资源配额的总和,那么可能会导致资源竞争。在发生资源竞争时,Kubernetes系统会遵循先到先得的原则。
◎ 不管是资源竞争还是配额修改,都不会影响已创建的资源使用对象。
资源配额可以通过在kube-apiserver的--admission-control参数值中添加ResourceQuota参数进行开启。如果在某个命名空间的定义中存在ResourceQuota,那么对于该命名空间而言,资源配额就是开启的。一个命名空间可以有多个ResourceQuota配置项。
资源配额可以限制一个命名空间中所有Pod的计算资源的总和。ResourceQuota目前支持闲置的计算资源类型如表10.2所示。
表10.2 ResourceQuota目前支持限制的计算资源类型

可以在给定的命名空间中限制所使用的存储资源(Storage Resources)的总量,目前支持的存储资源名称如表10.3所示。
表10.3 ResourceQuota支持限制的计算资源类型

指定类型的对象数量可以被限制,例如,我们可以通过资源配额来限制在命名空间中创建的Pod的最大数量。这种配置可以防止某些用户大量创建Pod而迅速耗尽整个集群的Pod IP和计算资源。表10.4列出了ResourceQuota支持限制的对象类型。
表10.4 ResourceQuota支持限制的对象类型

具体表示如下。
◎ count/<resource>.<group>:用于非核心(core)组的资源,例如count/deployments.apps、count/cronjobs.batch。
◎ count/<resource>:用于核心组的资源,例如count/services、count/pods。
相同的语法也可用于自定义资源CRD。例如,若要对example.com API组中CRD资源widgets对象的数量进行配额设置,则可以使用count/widgets.example.com表示。
对每项资源配额都可以单独配置一组作用域,配置了作用域的资源配额只会对符合其作用域的资源使用情况进行计量和限制,作用域范围超出了资源配额的请求都会被报验证错误。表10.5列出了ResourceQuota的4种作用域。
表10.5 ResourceQuota的4种作用域

其中,BestEffort作用域可以限定资源配额来追踪Pod资源的使用;而Terminating、NotTerminating、NotBestEffort和PriorityClass除了可以追踪Pod,还可以追踪CPU、limits.cpu、limits.memory、memory、requests.cpu、requests.memory等资源的使用情况。
这里特别提一下基于Pod优先级的资源配额(PriorityClass)特性,这是Kubernetes 1.17实现的新特性,也是比较实用的特性,配置示例说明如下。
(1)通过Pod的priorityClassName属性将Pod划分为不同的优先级,比如low、medium、high:

(2)在ResourceQuota中通过scopeSelector选择匹配的目标Pod的优先级,赋予相应的资源配额:


通过PriorityClass的配额机制,我们就可以实现标准的基于Pod优先级的资源配额管控方式了,这种方式相对于隐式的QoS来说更为直观、明确。
在资源配额中也可以设置Requests和Limits。如果在资源配额中指定了requests.cpu或requests.memory,那么它会强制要求每个容器都配置自己的CPU Requests或CPU Limits(可使用LimitRange提供的默认值)。同理,如果在资源配额中指定了limits.cpu或limits.memory,那么它也会强制要求每个容器都配置自己的内存Requests或内存Limits(可使用LimitRange提供的默认值)。
下面通过几个例子对资源配额进行设置和应用。
与LimitRange相似,ResourceQuota也被设置在命名空间中。创建名为myspace的命名空间:

创建ResourceQuota配置文件compute-resources.yaml,用于设置计算资源的配额:

创建该项的资源配额:

创建另一个名为object-counts.yaml的文件,用于设置对象数量的配额:

创建该ResourceQuota:

查看各ResourceQuota的详细信息:


资源配额与集群资源总量是完全独立的。资源配额是通过绝对的单位来配置的,这也就意味着如果在集群中新添加了节点,那么资源配额不会自动更新,而该资源配额所对应的命名空间中的对象也不能自动增加资源上限。
在某些情况下,我们可能希望资源配额支持更复杂的策略,如下所述。
◎ 对于不同的租户,按照某种比例划分整个集群的资源。
◎ 允许每个租户按照需要来提高资源用量,但是有一个较宽容的限制,以防止意外的资源耗尽情况发生。
◎ 探测某个命名空间的需求,添加物理节点并扩大资源配额值。
这些策略可以这样实现:手动编写一个控制器,持续监控各命名空间中的资源使用情况,并按需调整命名空间的资源配额数量。
资源配额将整个集群中的资源总量做了一个静态划分,但它并没有对集群中的节点做任何限制:不同命名空间中的Pod仍然可以运行在同一个节点上。
根据前面对资源管理的介绍,这里将通过一个完整的例子说明如何通过资源配额和资源配置范围的配合来控制一个命名空间的资源使用。
集群管理员根据集群用户的数量来调整集群配置,以达到这个目的:能控制特定命名空间中的资源使用量,最终实现集群的公平使用和成本控制。
需要实现的功能如下。
◎ 限制运行状态的Pod的计算资源用量。
◎ 限制持久存储卷的数量以控制对存储的访问。
◎ 限制负载均衡器的数量以控制成本。
◎ 防止滥用网络端口这类稀缺资源。
◎ 提供默认的计算资源Requests以便系统做出更优化的调度。
创建名为quota-example的命名空间,namespace.yaml文件的内容如下:

查看命名空间:

通过设置限定对象数量的资源配额,可以控制持久存储卷、负载均衡器、NodePort这些资源的数量。
创建名为object-counts的ResourceQuota:


配额系统会检测到资源项配额的创建,统计和限制该命名空间中的资源消耗。
查看该配额是否生效:

至此,配额系统会自动阻止那些使资源用量超过资源配额限定值的请求。
下面再创建一项限定计算资源的资源配额,以限制该命名空间中计算资源的使用总量。
创建名为compute-resources的ResourceQuota:

查看该配额是否生效:

配额系统会自动防止在该命名空间中同时拥有超过4个非“终止态”的Pod。此外,由于该项资源配额限制了CPU和内存的Limits和Requests总量,因此会强制要求该命名空间中的所有容器都显式定义CPU和内存的Limits、Requests(可使用默认值,Requests默认等于Limits)。
在命名空间已经配置了限定计算资源的资源配额的情况下,如果尝试在该命名空间中创建一个不指定Requests和Limits的Pod,那么Pod的创建可能会失败。下面是一个失败的例子。
创建一个Nginx的Deployment:

查看创建的Pod,会发现Pod没有创建成功:

再查看Deployment的详细信息:


该Deployment会尝试创建一个Pod,但是失败,查看其中ReplicaSet的详细信息:

可以看到Pod创建失败的原因:Master拒绝这个ReplicaSet创建Pod,因为在这个Pod中没有指定CPU和内存的Requests、Limits。
为了避免这种失败,我们可以使用LimitRange为这个命名空间中的所有Pod都提供一个资源配置的默认值。下面的例子展示了如何为这个命名空间添加一个指定了默认资源配置的LimitRange。
创建一个名为limits的LimitRange:


在LimitRange创建成功后,若用户在该命名空间中创建了未指定资源限制的Pod,系统就会自动为该Pod设置默认的资源限制。
例如,每个新建的未指定资源限制的Pod都等价于使用下面的资源限制:

至此,我们已经为该命名空间配置好默认的计算资源了,我们的ReplicaSet应该能够创建Pod了。查看一下,发现创建Pod成功:

接下来可以随时查看资源配额的使用情况:

可以看到,每个Pod在创建时都会消耗指定的资源量,而这些使用量都会被Kubernetes准确跟踪、监控和管理。
假设我们并不想为某个命名空间配置默认的计算资源配额,而是希望限定在命名空间中运行的QoS为BestEffort的Pod总数,例如让集群中的部分资源运行QoS为非BestEffort的服务,并让闲置的资源运行QoS为BestEffort的服务,即可避免集群的所有资源仅被大量的BestEffort Pod耗尽。这可以通过创建两个资源配额来实现,如下所述。
创建一个名为quota-scopes的命名空间:

创建一个名为best-effort的ResourceQuota,指定Scope为BestEffort:


再创建一个名为not-best-effort的ResourceQuota,指定Scope为NotBestEffort:

查看创建成功的ResourceQuota:


之后,没有配置Requests的Pod将被名为best-effort的ResourceQuota限制;而配置了Requests的Pod会被名为not-best-effort的ResourceQuota限制。
创建两个Deployment:

名为best-effort-nginx的Deployment因为没有配置Requests和Limits,所以它的QoS级别为BestEffort,因此它的创建过程由best-effort资源配额项来限制,而not-best-effort资源配额项不会对它进行限制。best-effort资源配额项没有限制Requests和Limits,因此best-effort-nginx Deployment可以成功创建8个Pod。
名为not-best-effort-nginx的Deployment因为配置了Requests和Limits,且二者不相等,所以它的QoS级别为Burstable,因此它的创建过程由not-best-effort资源配额项限制,而best-effort资源配额项不会对它进行限制。not-best-effort资源配额项限制了Pod的Requests和Limits的总上限,not-best-effort-nginx Deployment并没有超过这个上限,所以可以成功创建两个Pod。
查看已经创建的Pod,可以看到10个Pod都创建成功:


再查看两个资源配额项的使用情况,可以看到best-effort资源配额项已经统计了在best-effort-nginx Deployment中创建的8个Pod的资源使用信息,not-best-effort资源配额项也已经统计了在not-best-effort-nginx Deployment中创建的两个Pod的资源使用信息:

通过这个例子可以发现:资源配额的作用域(Scopes)提供了一种将资源集合分割的机制,可以使集群管理员更加方便地监控和限制不同类型的对象对各类资源的使用情况,同时为资源分配和限制提供更好的灵活性和便利性。
Kubernetes中资源管理的基础是容器和Pod的资源配置(Requests和Limits)。容器的资源配置指定了容器请求的资源和容器能使用的资源上限,Pod的资源配置则是Pod中所有容器的资源配置总和上限。
通过资源配额机制,我们可以对命名空间中所有Pod使用资源的总量进行限制,也可以对这个命名空间中指定类型的对象的数量进行限制。使用作用域可以让资源配额只对符合特定范围的对象加以限制,因此作用域机制可以使资源配额的策略更加丰富、灵活。
如果需要对用户的Pod或容器的资源配置做更多的限制,则可以使用资源配置范围(LimitRange)来达到这个目的。LimitRange可以有效限制Pod和容器的资源配置的最大、最小范围,也可以限制Pod和容器的Limits与Requests的最大比例上限,LimitRange还可以为Pod中的容器提供默认的资源配置。
Kubernetes基于Pod的资源配置实现了资源服务质量(QoS)。不同QoS级别的Pod在系统中拥有不同的优先级:高优先级的Pod有更高的可靠性,可以用于运行对可靠性要求较高的服务;低优先级的Pod可以实现集群资源的超售,有效提高集群资源利用率。
上面的多种机制共同组成了当前版本Kubernetes的资源管理体系。这个资源管理体系可以满足大部分资源管理需求。同时,Kubernetes的资源管理体系仍然在不停地发展和进化,对于目前无法满足的更复杂、更个性化的需求,我们可以继续关注Kubernetes未来的发展和变化。
下面对计算资源以外的其他几种资源的管理方式进行说明,包括Pod内多个容器的共享进程命名空间、PID资源管理、节点的CPU资源管理策略和拓扑管理器。
在某些应用场景中,属于同一个Pod的多个容器相互之间希望能够访问其他容器的进程,例如使用一个debug容器时,需要对业务应用容器内的进程进行查错,这对多个容器环境的进程命名空间(Process Namespace)的共享提出需求,该机制的支持从Kubernetes 1.10版本开始引入,到1.17版本时达到Stable阶段。
启用进程命名空间共享机制很简单,只需在Pod定义中设置shareProcessNamespace=true即可完成。我们通过下面这个例子看看一个Pod中两个容器共享进程命名空间的效果,share-process-namespace.yaml配置文件的内容如下:

其中,主容器为一个nginx提供的服务,另一个容器为busybox提供的查错工具,被命名为“shell”。在shell容器的securityContext.capabilities中增加了CAP_SYS_PTRACE能力,用于提供进程跟踪操作能力。
使用kubectl create命令创建这个Pod:

进入shell的容器环境中,使用ps命令可以查看到nginx和自身容器的全部进程:

由于shell容器具备CAP_SYS_PTRACE能力,所以它还可以对其他进程发送操作系统信号,例如对nginx容器中的6号进程发出SIGHUP信号用于重启nginx程序:

可以看到,nginx的原worker进程(PID=38)重启后启动了一个新的PID=51的worker进程。
有两个容器共享进程命名空间的Pod环境有以下特性。
◎ 各容器的进程ID(PID)混合在一个环境中,都不再拥有进程号PID=1的启动进程,1号进程由Pod的Pause容器使用。对于某些必须以进程号1作为启动程序PID的容器来说,将会无法启动,例如以systemd作为启动命令的容器。
◎ 进程信息在多个容器间相互可见,这包括/proc目录下的所有信息,其中可能有包含密码类敏感信息的环境变量,只能通过UNIX文件权限进行访问控制,需要设置容器内的运行用户或组。
◎ 一个容器的文件系统存在于/proc/$pid/root目录下,所以不同的容器也能访问其他容器的文件系统的内容,这对于debug查错来说非常有用,但也意味着没有容器级别的安全隔离,只能通过UNIX文件权限进行访问控制,需要设置容器内的运行用户或组。
例如,在shell容器内可以查看到nginx容器的配置文件的内容:

PID(进程ID)在Linux系统中是最重要的一种基础资源,操作系统会设置一台主机可以运行的最大进程数上限。虽然在通常情况下不太容易出现PID耗尽的情况,但为了避免存在缺陷的程序耗尽主机PID资源(进而导致守护进程如kubelet无法正常工作),Kubernetes在1.10版本中开始引入对Pod级别的PID资源管理机制,用于限制单个Pod内可以创建的最大进程数量,并在1.14版本中引入Node级别的PID资源管理机制,确保Node的PID不会被所有Pod耗尽,以保护在Node上运行的守护进程(如kubelet、容器运行时程序等),该PID资源管理机制在1.15版本时达到Beta阶段。
为了使用Pod级别的PID资源管理机制,我们首先需要在kubelet服务的启动参数中开启SupportPodPidsLimit特性开关(--feature-gates=SupportPodPidsLimit=true),然后通过启动参数--pod-max-pids设置一个Pod中允许的最大PID数量(将其设置为-1表示继承使用Node系统配置的PID数量)。
为了使用Node级别的PID资源管理,我们首先需要在kubelet服务的启动参数中开启SupportNodePidsLimit特性开关(--feature-gates=SupportNodePidsLimit=true),开启该特性之后,系统会自动为守护进程预留一些PID资源,也会用于kubelet后续判断是否需要驱逐Pod的计算逻辑中。
在一个Node上可分配的PID数量的算法如下:

kubelet默认使用CFS Quota技术基于Pod的CPU Limit对Node上CPU资源的使用进行限制和管理(CFS,Completely Fair Scheduler,即完全公平调度算法)。当在一个Node上运行了很多CPU密集型Pod时,容器进程可能会被调度到不同的CPU核上进行运算,这取决于调度时哪些CPU核是可用的,Pod使用的CPU资源是否达到了上限。许多应用对这种CPU的切换不敏感,无须特别的干预也可正常工作。
然而,有些应用的性能明显受到CPU缓存亲和性及调度延迟的影响。针对这类应用,Kubernetes提供了一个可选的CPU管理策略,来确定节点上CPU资源调度的优先级,为Pod运行达到更好的性能提供支持。该特性从Kubernetes 1.12版本开始引入,目前为Beta阶段。
CPU管理策略通过Node上的kubelet启动参数--cpu-manager-policy进行指定,目前支持两种策略。
◎ none:使用默认的调度策略。
◎ static:允许为节点上具有特定资源特征的Pod授予更高的CPU亲和性和独占性。
CPU管理器定期通过CRI接口将资源更新写入容器中,以保证内存中的CPU分配与cgroupfs保持一致。同步频率通过kubelet启动参数--cpu-manager-reconcile-period进行设置,如果不指定,则默认与--node-status-update-frequency设置的值相同。
下面对这两种策略的原理和示例进行说明。
None策略使用默认的CPU亲和性方案,即操作系统默认的CPU调度策略。对于QoS级别为Guaranteed的Pod,会强制使用CFS Quota机制对CPU资源进行限制。
Static策略针对具有特定CPU资源需求的Pod。对于QoS级别为Guaranteed的Pod,如果其Container设置的CPU Request为大于等于1的整数,Kubernetes就能允许容器绑定节点上的一个或多个CPU核独占运行。这种独占是使用cpuset cgroup控制器来实现的。
注意:容器运行时(Container Runtime)和kubelet等系统服务也可以运行在独占的CPU核上,这种独占性是相对于其他Pod而言的;CPU管理器不支持在运行时下线和上线CPU。此外,如果节点上的在线CPU集合发生了变化,则必须驱逐节点上的Pod,并删除kubelet根目录中的状态文件cpu_manager_state来手动重置CPU管理器。
该策略管理一个共享CPU资源池,该资源池最初包含节点上的所有CPU资源。可用的独占性CPU资源数量等于节点的CPU总量减去通过--kube-reserved或--system-reserved参数设置保留给系统的CPU资源数量。
Kubernetes从1.17版本开始,CPU保留列表可以通过kubelet服务的--reserved-cpus参数显式地设置。通过--reserved-cpus设置的CPU列表优先于使用--kube-reserved和--system-reserved参数设置的CPU保留值。
启用Static策略时,要求使用--kube-reserved和(或)--system-reserved或--reserved-cpus为kubelet保留一部分CPU资源,并且保留的CPU资源数量必须大于0。这是因为如果系统保留CPU为0,则共享池有变为空的可能,导致kubelet无法正常工作。
通过这些参数预留的CPU单位为整数,按物理内核ID升序从初始共享池中获取。共享池是QoS级别为BestEffort和Burstable的Pod运行所需的CPU集合。QoS级别为Guaranteed的Pod中的容器,如果声明了非整数值的CPU Request,则也将运行在共享池的CPU上,只有声明了整数CPU Request的容器才会被分配独占的CPU资源。
当QoS级别为Guaranteed的Pod被调度到节点上时,如果容器的CPU资源需求设置符合静态分配的要求,则所需的CPU核会被从共享池中取出并放到容器的cpuset中,供容器独占使用。容器cpuset中的CPU核数与Pod定义中指定的整数个CPU limit相等,无须再使用CFS Quota机制分配CPU资源。这种静态分配机制增强了CPU的亲和性,减少了CPU上下文切换的次数。
下面是几种不同QoS级别的容器使用CPU资源时的策略示例。
(1)BestEffort类型。容器如果没有设置CPU Request和CPU Limit,则将运行在共享CPU池中。例如:

(2)Burstable类型。容器如果没有设置CPU资源,或者其他资源(如内存)的Request不等于Limit,则将运行在共享CPU池中。例如:

或者

(3)Guaranteed类型。容器如果设置了CPU资源,并且设置Request等于Limit且为整数,则将运行在两个独占的CPU核上。例如:

或者(若未显式设置Request,则系统将默认设置Request=Limit):

(4)Guaranteed类型。若容器设置了CPU资源,并且设置Request等于Limit但设置为小数,则将运行在共享CPU池中。例如:

总之,为了能让容器独占CPU资源运行,需要满足以下条件:
◎ 设置kubelet服务的启动参数--cpu-manager-policy=static;
◎ 容器的CPU资源需求QoS级别必须是Guaranteed级别,即Request=Limit;
◎ 必须将容器的CPU Limit设置为大于等于1的整数。
在Kubernetes集群中部署容器应用时,我们常常根据应用的资源需求设置资源分配策略,包括CPU、GPU、内存、设备等资源,但是对不同类型资源的管理是由单独的组件进行的。随着容器化技术的成熟,越来越多的应用系统利用CPU、GPU、硬件加速等资源组合来支持对延迟要求更高的任务和高吞吐量并行计算。为了获得最佳性能,需要进行与CPU隔离、内存优化、本地设备有关的优化。Kubernetes从1.16版本开始引入拓扑管理器(Topology Manager)功能,旨在协调对多种资源进行优化的功能组件,为高性能计算应用的多种资源需求组合提供支持,目前为Beta阶段。本节对拓扑管理器的工作原理和策略示例进行说明。
在引入拓扑管理器之前,Kubernetes中的CPU和设备管理器都需要独立做出资源分配决策,这可能导致在多核系统上出现与期望不一致的资源分配结果(比如从不同的NUMA节点分配CPU和设备,从而导致更长的计算延迟),使得对性能或延迟敏感的应用造成影响。
拓扑管理器是kubelet中的一个组件,起着信息源的作用,以便kubelet的其他组件做出与拓扑结构相对应的资源分配决定。拓扑管理器为资源管理组件提供了一个名为建议提供者(Hint Providers)的接口,以发送和接收拓扑(Topology)的相关信息。
拓扑管理器管理着Node级别的一组策略,从建议提供者处接收拓扑信息,将其保存为表示可用NUMA节点和首选分配指示的位掩码(bitmask)。拓扑管理器对接收到的建议(Hint)执行一组操作,并根据策略对建议进行收敛计算以得到最优解。如果拓扑管理器保存了不符合预期的建议,则将该建议的优选字段设置为false。在当前策略中,首选的是最窄的优选掩码。所选建议将被存储为拓扑管理器的一部分。取决于所配置的策略,所选建议可用于决定节点是否接受或拒绝Pod。之后,建议会被存储在拓扑管理器中,供建议提供者进行资源分配决策时使用。
要启用拓扑管理器特性,就需要在Kubernetes各个服务的特性开关中进行开启:--feature-gates=...,TopologyManager=true,从Kubernetes 1.18版本开始默认启用。
拓扑管理器目前会对所有QoS类的Pod执行对齐(Align)操作,并针对建议提供者提供的拓扑建议,对请求的资源进行对齐(Align)操作。
说明:为了将Pod定义中的CPU资源与其他请求资源对齐,需要启用CPU管理器并且在节点上配置适当的CPU管理器策略(参考10.4.8节的说明)。
拓扑管理器支持4种分配策略,可以通过kubelet启动参数--topology-manager-policy设置分配策略,包括:none(不执行任何拓扑对齐操作,默认值为none)、best-effort、restricted和single-numa-node。下面对后三种分配策略进行说明。
(1)best-effort分配策略。对于QoS级别为Guaranteed的Pod中的每个容器,kubelet都将调用每个建议提供者以确定资源的可用性。基于这些信息,拓扑管理器将为各容器存储首选NUMA节点亲和性。如果亲和性不是首选,则拓扑管理器将存储该亲和性,并且无论如何都将Pod调度到该节点上。之后建议提供者就可以在做出资源分配决策时使用此信息了。
(2)restricted分配策略。对于QoS级别为Guaranteed的Pod中的每个容器,与best-effort相同,即kubelet调用每个建议提供者以确定其资源可用性,基于这些信息,拓扑管理器将为各容器存储首选NUMA节点亲和性。如果亲和性不是首选,则拓扑管理器将拒绝调度Pod到该节点上。这将导致Pod处于Terminated状态,错误信息为准入(admission)失败。一旦Pod处于Terminated状态,Kubernetes调度器就将不再尝试重新调度该Pod。建议使用ReplicaSet或Deployment来重新部署Pod。也可以通过实现外部控制器,以对存在Topology Affinity错误信息的Pod进行重新部署。如果Pod被允许运行在某节点上,建议提供者就可以在做出资源分配决策时使用此信息了。
(3)single-numa-node分配策略。对于QoS级别为Guaranteed的Pod中的每个容器,kubelet调用每个建议提供者以确定其资源可用性。基于这些信息,拓扑管理器确定是否可能实现单个NUMA节点的亲和性。如果可能,拓扑管理器将存储此信息,之后建议提供者就可以在做出资源分配决策时使用此信息了。如果不可能,拓扑管理器将拒绝Pod运行于该节点上,这将导致Pod处于Terminated状态,错误信息为准入(admission)失败。一旦Pod处于Terminated状态,Kubernetes调度器则将不会尝试重新调度该Pod。建议使用ReplicaSet或Deployment来重新部署Pod。也可以通过实现外部控制器,以对存在Topology Affinity错误信息的Pod进行重新部署。
考虑以下两种不同QoS级别的容器配置。
BestEffort类型的Pod定义,未设置CPU Request和CPU Limit,例如:

Burstable类型的Pod定义,设置的资源Request小于Limit,例如:

如果选择的拓扑管理策略是none以外的任何其他策略(best-effort、restricted或single-numa-node),则拓扑管理器都会评估这些Pod的定义。拓扑管理器会询问建议提供者获取拓扑建议。如果策略为static,则CPU管理器策略会返回默认的拓扑建议,因为以上Pod并没有显式地请求CPU资源。
Guaranteed类型的Pod定义,设置的资源Request等于Limit,例如:

或者

BestEffort类型的Pod定义,未设置CPU和内存资源,例如:

拓扑管理器对上述几个Pod的管理机制如下。
拓扑管理器将询问建议提供者,即CPU管理器和设备管理器,以获取拓扑建议。
(1)对于Guaranteed类型的CPU请求数为整数的Pod,在CPU管理策略为“static”时将返回与独占CPU请求有关的建议;而设备管理器将返回有关所请求设备的建议。
(2)对于Guaranteed类型的CPU请求可共享的Pod,在CPU管理策略为“static”时将返回默认的拓扑建议,因为没有排他性的CPU请求;设备管理器则针对所请求的设备返回有关建议。
在上述Guaranteed Pod情况下,CPU管理策略为none时都会返回默认的拓扑建议。
(3)对于BestEffort类的Pod,由于没有设置CPU Request,CPU管理策略为static时将返回默认建议,而设备管理器将为每个请求的设备都返回建议。
基于此信息,拓扑管理器将为Pod计算最佳建议并存储该信息,以供建议提供者在进行资源分配决策时使用。
拓扑管理器在当前有以下局限性。
(1)拓扑管理器所能处理的最大NUMA节点数量为8个。如果NUMA节点数量超过8个,则尝试枚举所有可能的NUMA亲和性并为之生成建议时,可能会发生状态爆炸(State Explosion)。
(2)调度器无法做到拓扑感知,因此可能会调度Pod到某个节点上,但由于拓扑管理器的原因导致Pod无法在该节点上运行。
(3)目前仅有设备管理器(Device Manager)和CPU管理器(CPU Manager)两个组件适配了拓扑管理器的HintProvider接口。这意味着NUMA对齐只能针对CPU管理器和设备管理器所管理的资源进行实现。内存(Memory)和巨页(Hugepage)在拓扑管理器决定NUMA对齐时都还不会被考虑在内。