4.7 资源需求与资源限制
容器在运行时具有多个维度,例如内存占用、CPU占用和其他资源的消耗等。每个容器都应该声明其资源需求,并将该信息传递给管理平台。这些资源需求信息会在CPU、内存、网络、磁盘等维度对平台执行调度、自动扩展和容量管理等方面影响编排工具的决策。
4.7.1 资源需求与限制
在Kubernetes上,可由容器或Pod请求与消费的“资源”主要是指CPU和内存(RAM),它可统称为计算资源,另一种资源是事关可用存储卷空间的存储资源。本节主要描述计算资源的需求与限制,存储资源的话题将在第9章进行说明。
相比较而言,CPU属于可压缩型资源,即资源额度可按需弹性变化,而内存(当前)则是不可压缩型资源,对其执行压缩操作可能会导致某种程度的问题,例如进程崩溃等。目前,资源隔离仍属于容器级别,CPU和内存资源的配置主要在Pod对象中的容器上进行,并且每个资源存在如图4-16所示的需求和限制两种类型。为了表述方便,人们通常把资源配置称作Pod资源的需求和限制,只不过它是指Pod内所有容器上的某种类型资源的请求与限制总和。
▪资源需求:定义需要系统预留给该容器使用的资源最小可用值,容器运行时可能用不到这些额度的资源,但用到时必须确保有相应数量的资源可用。
▪资源限制:定义该容器可以申请使用的资源最大可用值,超出该额度的资源使用请求将被拒绝;显然,该限制需要大于等于requests的值,但系统在某项资源紧张时,会从容器回收超出request值的那部分。
在Kubernetes系统上,1个单位的CPU相当于虚拟机上的1颗虚拟CPU(vCPU)或物理机上的一个超线程(Hyperthread,或称为一个逻辑CPU),它支持分数计量方式,一个核心(1 core)相当于1000个微核心(millicores,以下简称为m),因此500m相当于是0.5个核心,即1/2个核心。内存的计量方式与日常使用方式相同,默认单位是字节,也可以使用E、P、T、G、M和K为单位后缀,或Ei、Pi、Ti、Gi、Mi和Ki形式的单位后缀。
4.7.2 容器资源需求
下面的配置清单示例(resource-requests-demo.yaml)中的自主式Pod要求为stress容器确保128MiB的内存及1/5个CPU核心(200m)资源可用。Pod运行stress-ng镜像启动一个进程(-m 1)进行内存性能压力测试,满载测试时stress容器也会尽可能多地占用CPU资源,另外再启动一个专用的CPU压力测试进程(-c 1)。stress-ng是一个多功能系统压力测试具,master/worker模型,master为主进程,负载生成和控制子进程,worker是负责执行各类特定测试的子进程,例如测试CPU的子进程,以及测试RAM的子进程等。
apiVersion: v1 kind: Pod metadata: name: stress-pod spec: containers: - name: stress image: ikubernetes/stress-ng command: ["/usr/bin/stress-ng", "-m 1", "-c 1", "-metrics-brief"] resources: requests: memory: "128Mi" cpu: "200m"
上面的配置清单中,stress容器请求使用的CPU资源大小为200m,这意味着一个CPU核心足以确保其以期望的最快方式运行。另外,配置清单中期望使用的内存大小为128MiB,不过其运行时未必真的会用到这么多。考虑到内存为非压缩型资源,当超出时存在因OOM被杀死的可能性,于是请求值是其理想中使用的内存空间上限。
接下来创建并运行此Pod对象以对其资源限制效果进行检查。因为显示结果涉及资源占用比例等,因此同样的测试配置对不同的系统环境来说,其结果也会有所不同,作者为测试资源需求和资源限制功能而使用的系统环境中,每个节点的可用CPU核心数为8,物理内存空间为16GB。
~$ kubectl create -f resource-requests-demo.yaml
而后在Pod资源的容器内运行top命令,观察CPU及内存资源占用状态,如下所示。其中{stress-ng-vm}是执行内存压测的子进程,它默认使用256MB的内存空间,{stress-ng-cpu}是执行CPU压测的专用子进程。
~$ kubectl exec stress-pod -- top Mem: 2884676K used, 13531796K free, 27700K shrd, 2108K buff, 1701456K cached CPU: 25% usr 0% sys 0% nic 74% idle 0% io 0% irq 0% sirq Load average: 0.57 0.60 0.71 3/435 15 PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND 9 8 root R 262m 2% 6 13% {stress-ng-vm} /usr/bin/stress-ng 7 1 root R 6888 0% 3 13% {stress-ng-cpu} /usr/bin/stress-ng 1 0 root S 6244 0% 1 0% /usr/bin/stress-ng -c 1 -m 1 --met ……
top命令的输出结果显示,每个测试进程的CPU占用率为13%(实际12.5%),{stress-ng-vm}的内存占用量为262MB(VSZ),此两项资源占用量都远超其请求的用量,原因是stress-ng会在可用范围内尽量多地占用相关的资源。两个测试线程分布于两个CPU核心,以满载的方式运行,系统共有8个核心,因此其使用率为25%(2/8)。另外,节点上的内存资源充裕,所以,尽管容器的内存用量远超128MB,但它依然可以运行。一旦资源紧张时,节点仅保证该容器有1/5个CPU核心(其需求中的定义)可用。在有着8个核心的节点上来说,它的占用率为2.5%,于是每个进程占比为1.25%,多占用的资源会被压缩。内存为非可压缩型资源,该Pod对象在内存资源紧张时可能会因OOM被杀死。
对于压缩型的资源CPU来说,若未定义容器的资源请求用量,以确保其最小可用资源量,该Pod占用的CPU资源可能会被其他Pod对象压缩至极低的水平,甚至到该Pod对象无法被调度运行的境地。而对于非压缩型内存资源来说,资源紧缺情形下可能导致相关的容器进程被杀死。因此,在Kubernetes系统上运行关键型业务相关的Pod时,必须要使用requests属性为容器明确定义资源需求。当然,我们也可以为Pod对象定义较高的优先级来改变这种局面。
集群中的每个节点都拥有定量的CPU和内存资源,调度器将Pod绑定至节点时,仅计算资源余量可满足该Pod对象需求量的节点才能作为该Pod运行的可用目标节点。也就是说,Kubernetes的调度器会根据容器的requests属性定义的资源需求量来判定哪些节点可接收并运行相关的Pod对象,而对于一个节点的资源来说,每运行一个Pod对象,该Pod对象上所有容器requests属性定义的请求量都要给予预留,直到节点资源被绑定的所有Pod对象瓜分完毕为止。
4.7.3 容器资源限制
容器为保证其可用的最少资源量,并不限制可用资源上限,因此对应用程序自身Bug等多种原因导致的系统资源被长时间占用无计可施,这就需要通过资源限制功能为容器定义资源的最大可用量。一旦定义资源限制,分配资源时,可压缩型资源CPU的控制阀可自由调节,容器进程也就无法获得超出其CPU配额的可用值。但是,若进程申请使用超出limits属性定义的内存资源时,该进程将可能被杀死。不过,该进程随后仍可能会被其控制进程重启,例如,当Pod对象的重启策略为Always或OnFailure时,或者容器进程存在有监视和管理功能的父进程等。
下面的配置清单文件(resource-limits-demo.yaml)中定义使用simmemleak镜像运行一个Pod对象,它模拟内存泄漏操作不断地申请使用内存资源,直到超出limits属性中memory字段设定的值而被杀死。
apiVersion: v1 kind: Pod metadata: name: memleak-pod labels: app: memleak spec: containers: - name: simmemleak image: ikubernetes/simmemleak imagePullPolicy: IfNotPresent resources: requests: memory: "64Mi" cpu: "1" limits: memory: "64Mi" cpu: "1"
下面将配置清单中定义的Pod对象创建到集群中,测试资源限制的实施效果。
~$ kubectl apply -f resource-limits-demo.yaml pod/memleak-pod created
Pod资源的默认重启策略为Always,于是在simmemleak容器因内存资源达到硬限制而被终止后会立即重启,因此用户很难观察到其因OOM而被杀死的相关信息。不过,多次因内存资源耗尽而重启会触发Kubernetes系统的重启延迟机制(退避算法),即每次重启的时间间隔会不断地拉长,因而用户看到Pod对象的相关状态通常为CrashLoopBackOff。
~$ kubectl get pods -l app=memleak NAME READY STATUS RESTARTS AGE memleak-pod 0/1 CrashLoopBackOff 1 24s
Pod对象的重启策略在4.5.3节介绍过,这里不再赘述。我们可通过Pod对象的详细描述了解其相关状态,例如下面的命令及部分结果所示。
~]$ kubectl describe pods memleak-pod Name: memleak-pod …… Last State: Terminated Reason: OOMKilled Exit Code: 137 Started: Mon, 31 Aug 2020 12:42:50 +0800 Finished: Mon, 31 Aug 2020 12:42:50 +0800 Ready: False Restart Count: 3 ……
上面的命令结果中,OOMKilled表示容器因内存耗尽而被终止,因此为limits属性中的memory设置一个合理值至关重要。与资源需求不同的是,资源限制并不影响Pod对象的调度结果,即一个节点上的所有Pod对象的资源限制数量之和可以大于节点拥有的资源量,即支持资源的过载使用(overcommitted)。不过,这么一来,一旦内存资源耗尽,几乎必然地会有容器因OOMKilled而终止。
另外需要说明的是,Kubernetes仅会确保Pod对象获得它们请求的CPU时间额度,它们能否取得额外(throttled)的CPU时间,则取决于其他正在运行作业的CPU资源占用情况。例如对于总数为1000m的CPU资源来说,容器A请求使用200m,容器B请求使用500m,在不超出它们各自最大限额的前下,则余下的300m在双方都需要时会以2 : 5(200m : 500m)的方式进行配置。
4.7.4 容器可见资源
细心的读者可能已经发现,在容器中运行top等命令观察资源可用量信息时,容器可用资源受限于requests和limits属性中的定义,但容器中可见的资源量依然是节点级别的可用总量。例如,为前面定义的stress-pod添加如下limits属性定义。
limits: memory: "512Mi" cpu: "400m"
重新创建stress-pod对象,并在其容器内分别列出容器可见的内存和CPU资源总量,命令及结果如下所示。
~$ kubectl exec stress-pod -- cat /proc/meminfo | grep ^MemTotal MemTotal: 16416472 kB $ kubectl exec stress-pod -- cat /proc/cpuinfo | grep -c ^processor 8
命令结果中显示其可用内存资源总量为16416472 kB(16GB),CPU核心数为8个,这是节点级的资源数量,而非由容器的limits属性所定义的512MiB和400m。其实,这不仅让查看命令的显示结果看起来有些奇怪,也会给有些容器应用的配置带来不小的负面影响。
较为典型的是在Pod中运行Java应用程序时,若未使用-Xmx选项指定JVM的堆内存可用总量,则会默认设置为主机内存总量的一个空间比例(例如30%),这会导致容器中的应用程序申请内存资源时很快达到上限,而转为OOMKilled状态。另外,即便使用了-Xmx选项设置其堆内存上限,但该设置对非堆内存的可用空间不产生任何限制作用,仍然存在达到容器内存资源上限的可能性。
另一个典型代表是在Pod中运行Nginx应用时,其配置参数worker_processes的值设置为auto,则会创建与可见CPU核心数量等同的worker进程数,若容器的CPU可用资源量远小于节点所需资源量时,这种设置在较大的访问负荷下会产生严重的资源竞争,并且会带来更多的内存资源消耗。一种较为妥当的解决方案是使用Downward API将limits定义的资源量暴露给容器,这将在后面的章节中予以介绍。
4.7.5 Pod服务质量类别
前面曾提到,Kubernetes允许节点的Pod对象过载使用资源,这意味着节点无法同时满足绑定其上的所有Pod对象以资源满载的方式运行。因而在内存资源紧缺的情况下,应该以何种次序终止哪些Pod对象就变成了问题。事实上,Kubernetes无法自行对此做出决策,它需要借助于Pod对象的服务质量和优先级等完成判定。根据Pod对象的requests和limits属性,Kubernetes把Pod对象归类到BestEffort、Burstable和Guaranteed这3个服务质量类别(Quality of Service,QoS)类别下。
▪Guaranteed:Pod对象为其每个容器都设置了CPU资源需求和资源限制,且二者具有相同值;同时为每个容器都设置了内存资需求和内存限制,且二者具有相同值。这类Pod对象具有最高级别服务质量。
▪Burstable:至少有一个容器设置了CPU或内存资源的requests属性,但不满足Guaranteed类别的设定要求,这类Pod对象具有中等级别服务质量。
▪BestEffort:不为任何一个容器设置requests或limits属性,这类Pod对象可获得的服务质量为最低级别。
一旦内存资源紧缺,BestEffort类别的容器将首当其冲地被终止,因为系统不为其提供任何级别的资源保证,但换来的好处是,它们能够做到尽可能多地占用资源。若此时系统上已然不存任何BestEffort类别的容器,则接下来将轮到Burstable类别的Pod被终止。Guaranteed类别的容器拥有最高优先级,它们不会被杀死,除非其内存资源需求超限,或者OOM时没有其他更低优先级的Pod对象存在。
每个运行状态的容器都有其OOM评分,评分越高越优先被杀死。OOM评分主要根据两个维度进行计算:由服务质量类别继承而来的默认分值,以及容器的可用内存资源比例,而同等类别的Pod对象的默认分值相同。下面的代码片段取自pkg/kubelet/qos/policy.go源码文件,它们定义的是各种类别的Pod对象的OOM调节(Adjust)分值,即默认分值。其中,Guaranteed类别Pod资源的Adjust分值为–998,而BestEffort类别的默认分值为1000,Burstable类别的Pod资源的Adjust分值经由相应的算法计算得出。
const ( PodInfraOOMAdj int = -998 KubeletOOMScoreAdj int = -999 DockerOOMScoreAdj int = -999 KubeProxyOOMScoreAdj int = -999 guaranteedOOMScoreAdj int = -998 besteffortOOMScoreAdj int = 1000 )
因此,同等级别优先级的Pod资源在OOM时,与自身的requests属性相比,其内存占用比例最大的Pod对象将先被杀死。例如,图4-17中的同属于Burstable类别的Pod A将先于Pod B被杀死,虽然其内存用量小,但与自身的requests值相比,它的占用比例为95%,要大于Pod B的80%。
需要特别说明的是,OOM是内存耗尽时的处理机制,与可压缩型资源CPU无关,因此CPU资源的需求无法得到保证时,Pod对象仅仅是暂时获取不到相应的资源来运行而已。