Kubernetes 资源预留
  系统资源可分为两类:可压缩资源(CPU)和不可压缩资源(memory、storage)。可压缩资源比如CPU超配后,在系统满负荷时会划分时间片分时运行进程,系统整体会变慢(一般不会导致太大的问题)。但不可压缩资源如Memory,当系统内存不足时,就有可能触发系统 OOM;这时候根据 oom score 来确定优先杀死哪个进程,而 oom_score_adj
又是影响 oom score 的重要参数,其值越低,表示 oom 的优先级越低。在计算节点中,进程的 oom_score_adj
如下:
Name | Score |
---|---|
sshd等(sshd/dmevented / systemd-udevd) | -1000 |
K8S 管理进程(kubelet / docker / journalctl) | -999 |
Guaranteed Pod | -998 |
其它进程(内核 init 进程等) | 0 |
Burstable Pod | min(max(2, 1000 – (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999) |
BestEffort Pod | 1000 |
所以,OOM 的优先级如下:
1 |
|
  在Kubernetes平台,默认情况下Pod能够使用节点全部可用资源。如果节点上的Pod负载较大,那么这些Pod可能会与节点上的系统守护进程和k8s组件争夺资源并导致节点资源短缺,甚至引发系统OOM,导致某些进程被Linux系统的OOM killer机制杀掉,假如被杀掉的进程是系统进程或K8S组件,会导致比较严重的问题。
Node Allocatable简介
  kubelet的启动配置中有一个Node Allocatable
特性,来为系统守护进程和k8s组件预留计算资源,使得即使节点满负载运行时,也不至于出现pod去和系统守护进程以及k8s组件争抢资源,导致节点挂掉的情况。目前支持对CPU, memory, ephemeral-storage三种资源进行预留。kubernetes官方建议根据各个节点的负载情况来具体配置相关参数。
节点计算资源的分配如下图所示:
1 |
|
其中各个部分的含义如下:
- Node Capacity:Node的硬件资源总量
- kube-reserved:给k8s系统进程预留的资源(包括kubelet、container runtime、node problem detector等,但不会给以pod形式起的k8s系统进程预留资源)
- system-reserved:给linux系统守护进程预留的资源
- eviction-threshold:通过
--eviction-hard
参数为节点预留内存,当节点可用内存值低于此值时,kubelet会进行pod的驱逐 - allocatable:是真正可供节点上Pod使用的容量,kube-scheduler调度Pod时的参考此值(kubectl describe node可以看到,Node上所有Pods的request量不超过Allocatable)
节点可供Pod使用资源总量的计算公式如下:
1 |
|
从公式可以看出,默认情况下(不设置kube-reserved、system-reserved、eviction-threshold)节点上默认可以让Pod使用的资源总量等于节点的总容量,会导致Pod与系统进程和k8s组件争抢资源的情况发生。
示例场景
下面是一个例子来说明节点Allocatable
计算:
- 节点拥有
32Gi memory
,16 CPUs
,100Gi
存储。 --kube-reserved
设置为cpu=1,memory=2Gi,ephemeral-storage=1Gi
。--system-reserved
设置为cpu=500m,memory=1Gi,ephemeral-storage=1Gi
。--eviction-hard
设置为memory.available<500Mi,nodefs.available<10%
。
在此场景下, Allocatable
为 14.5 CPUs
,28.5Gi
内存, 88Gi
本地存储。调度器确保该节点上所有pod上的总内存请求不超过28.5Gi
,存储也不超过88Gi
。当跨pod的总体内存使用量超过28.5Gi
时,或者如果整个磁盘使用量超过88Gi
时,Kubelet 将驱逐pod。如果节点上的所有进程都尽可能多地消耗CPU,那么pod一起消耗的CPU不能超过 14.5 CPUs
。
如果不强制执行kube-reserved
和/或system-reserved
,并且系统守护进程超出了它们的保留,那么当节点内存使用总量大于31.5Gi
或存储容量大于90Gi
时,kubelet就会驱逐pod。
配置参数
kubelet的启动参数中涉及资源预留的主要有如下几个:
--cgroups-per-qos
--cgroup-driver
--enforce-node-allocatable=pods[,][system-reserved][,][kube-reserved]
--kube-reserved=[cpu=100m][,][memory=100Mi][,][ephemeral-storage=1Gi]
--kube-reserved-cgroup
--system-reserved=[cpu=100mi][,][memory=100Mi][,][ephemeral-storage=1Gi]
--system-reserved-cgroup
--eviction-hard
参数详解
--cgroups-per-qos
可选,默认开启。开启这个参数后,kubelet会将所有的pod创建在kubelet管理的cgroup层次结构下(这样才有了限制所有Pod使用资源总量的基础)。要想启用Node Allocatable特性,这个参数必须开启。
--cgroup-driver
可选。指定kubelet使用的cgroup driver。默认为cgroupfs,还可以是systemd,但是这个值需要和docker runtime所使用的cgroup driver保持一致。
--enforce-node-allocatable
指定kubelet为哪些进程做硬限制,可选的值有:pods,kube-reserved,system-reserve。
这个参数默认开启并指定pods,此时kubelet会为所有pod的总cgroup做资源限制(通过cgroup中的kubepods.limit_in_bytes),限制为公式计算出的allocatable的大小。而用cgroup做了硬限制后,当所有pod使用量达到allocatable后,会有pod被OOM killer
机制杀掉,以保证实际使用量不会超过allocatable。(后面会有实验验证)
假如想为系统进程和k8s进程也做cgroup级别的硬限制,还可以在限制列表中再加system-reserved和kube-reserved,同时还要分别加上--kube-reserved-cgroup
和--system-reserved-cgroup
以指定分别限制在哪个cgroup里。
--kube-reserved
指定为k8s系统组件(kubelet、kube-proxy、dockerd等)预留的资源量,如:--kube-reserved=cpu=1,memory=2Gi,ephemeral-storage=1Gi
。
这里需要注意一点的是这里的kube-reserved只为非pod形式启动的kube组件预留资源,假如组件要是以static pod形式启动的,那并不在这个kube-reserved管理并限制的cgroup中,而是在kubepod这个cgroup中。
--kube-reserved-cgroup
这个参数用来指定k8s系统组件所使用的cgroup。注意,这里指定的cgroup及其子系统需要预先创建好,kubelet并不会为你自动创建好。
--system-reserved
为系统守护进程(sshd, udev等)预留的资源量,如:--system-reserved=cpu=500m,memory=1Gi,ephemeral-storage=1Gi
。注意,除了考虑为系统进程预留的量之外,还应该为kernel和用户登录会话预留一些内存。
--system-reserved-cgroup
这个参数用来指定系统守护进程所使用的cgroup。注意,这里指定的cgroup及其子系统需要预先创建好,kubelet并不会为你自动创建好。
--eviction-hard
设置进行pod驱逐的阈值,这个参数只支持内存和磁盘。通过--eviction-hard
标志预留一些内存后,当节点上的可用内存降至保留值以下时,kubelet 将会对pod进行驱逐。
实践
限制pod、k8s组件资源
1.将以下内容添加到kubelet的 /var/lib/kubelet/config.yaml
文件中:
1 |
|
修改完成后,如果 --kube-reserved-cgroup
不存在,Kubelet
不会创建它,启动 Kubelet
将会失败。由于子系统较多,具体是哪一个子系统不存在不好定位,我们可以将 kubelet
的日志级别调整为 --v=4
,就可以看到具体丢失的 cgroup 路径:
1 |
|
现在可以看到具体的 cgroup 不存在的路径信息了:(kubelet.INFO日志中)
1 |
|
至于如何设置cgroup结构,请参考官方建议。
2.为kubelet.slice创建子系统:
1 |
|
注意:systemd 的 cgroup 驱动对应的 cgroup 名称是以 .slice 结尾的,比如如果你把 cgroup 名称配置成 kubelet.service,那么对应的创建的 cgroup 名称应该为 kubelet.service.slice。如果你配置的是 cgroupfs 的驱动,则用配置的值即可。无论哪种方式,通过查看错误日志都是排查问题最好的方式。
我们也可以通过查看代码知道需要创建那些子系统:
1 |
|
3.重启kubelet
验证
1.验证公式计算的allocatable与实际一致
按以上设置节点上所有Pod实际使用的资源总和不会超过:
1 |
|
1 |
|
根据公式 capacity - kube-reserved - eviction-hard
,memory的allocatable的值为 7.6G - 1G - 0.5G = 6.1G
,与Allocatable的值一致。
查看kubelet.service控制组中对内存的限制值memory.limit_in_bytes:
1 |
|
2.验证公式计算的总使用量限制与实际值一致
查看kubepods控制组中对内存的限制值memory.limit_in_bytes(memory.limit_in_bytes值决定了Node上所有的Pod实际能使用的内存上限)
1 |
|
根据公式 capacity - kube-reserved
,Node上Pod能实际使用的资源上限值为:7.6G - 1G = 6.6G
,与实际一致。
限制system系统资源
  我们也可以用同样的方式为系统配置预留值,system-reserved
用于为诸如 sshd、udev 等系统守护进程争取资源预留,system-reserved
也应该为 kernel 预留 内存,因为目前 kernel 使用的内存并不记在 Kubernetes 的 pod 上。但是在执行 system-reserved
预留操作时请加倍小心,因为它可能导致节点上的关键系统服务 CPU 资源短缺或因为内存不足而被终止,所以如果不是自己非常清楚如何配置,可以不用配置系统预留值。
  同样通过 kubelet 的参数 --system-reserved
配置系统预留值,但是也需要配置 --system-reserved-cgroup
参数为系统进程设置 cgroup。
  请注意,如果 --system-reserved-cgroup
不存在,kubelet 不会创建它,kubelet 会启动失败。
建议优化
1 |
|