服务质量
服务质量
QoS 是 Quality of Service 的缩写,即服务质量,为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes 针对不同服务质量的预期,通过 QoS 来对 pod 进行服务质量管理,对于一个 pod 来说,服务质量体现在两个具体的指标:CPU 和内存
当节点上内存资源紧张时,Kubernetes 会根据预先设置的不同 QoS 类别进行相应处理
资源限制
如果未做过节点 nodeSelector、亲和性(node affinity)或 pod 亲和、反亲和性等高级调度策略设置,我们没有办法指定服务部署到指定节点上,这样就可能会造成 CPU 或内存等密集型的 pod 同时分配到相同节点上,造成资源竞争。另一方面,如果未对资源进行限制,一些关键的服务可能会因为资源竞争因 OOM 等原因被 kill 掉,或者被限制 CPU 使用。
对于每一个资源,container 可以指定具体的资源需求(requests)和限制(limits)
- requests 申请范围是0到节点的最大配置
0 <= requests <= Node Allocatable
- limits 申请范围是 requests 到无限
requests <= limits <= Infinity
对于 CPU,如果 pod 中服务使用的 CPU 超过设置的 limits,pod 不会被 kill 掉但会被限制,因为 CPU 是可压缩资源,如果没有设置 limits,pod 可以使用全部空闲的 CPU 资源。
对于内存,当一个 pod 使用内存超过了设置的 limits,pod 中容器的进程会被 kernel 因 OOM kill 掉,当 container 因为 OOM 被 kill 掉时,系统倾向于在其原所在的机器上重启该 container 或本机或其他重新创建一个 pod。
QoS 分类
Kubelet 提供 QoS 服务质量管理,支持系统级别的 OOM 控制
在 Kubernetes 中,QoS 主要分为
Guaranteed
、Burstable
和Best-Effort
三类,优先级从高到低
- Guaranteed:Pod 里的每个容器都必须有内存/CPU 限制和请求,而且值必须相等
- Burstable:Pod 里至少有一个容器有内存或者 CPU 请求且不满足 Guarantee 等级的要求,即内存/CPU 的值设置的不同
- BestEffort:容器必须没有任何内存或者 CPU 的限制或请求
QoS 分类并不是通过一个配置项来直接配置的,而是通过配置 CPU/内存的 limits 与 requests 值的大小来确认服务质量等级的,我们通过使用 kubectl get pod xxx -o yaml
可以看到 pod 的配置输出中有 qosClass
一项,该配置的作用是为了给资源调度提供策略支持,调度算法根据不同的服务质量等级可以确定将 pod 调度到哪些节点上
有保证的 Guaranteed
系统用完了全部内存,且没有其他类型的容器可以被 kill 时,该类型的 pods 会被 kill 掉,也就是说最后才会被考虑 kill 掉,属于该级别的 pod 有以下两种情况:
- pod 中的所有容器都且仅设置了 CPU 和内存的 limits
- pod 中的所有容器都设置了 CPU 和内存的 requests 和 limits ,且单个容器内的
requests==limits
(requests不等于0)
pod 中的所有容器都且仅设置了 limits:
containers:
name: foo
resources:
limits:
cpu: 10m
memory: 1Gi
name: bar
resources:
limits:
cpu: 100m
memory: 100Mi
因为如果一个容器只指明 limit 而未设定 requests,则 requests 的值等于 limit 值,所以上面 pod 的 QoS 级别属于 Guaranteed
另外一个就是 pod 中的所有容器都明确设置了 requests 和 limits,且单个容器内的 requests==limits
:
containers:
name: foo
resources:
limits:
cpu: 10m
memory: 1Gi
requests:
cpu: 10m
memory: 1Gi
name: bar
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 100m
memory: 100Mi
容器 foo 和 bar 内 resources 的 requests 和 limits 均相等,该 pod 的 QoS 级别属于 Guaranteed
不稳定的 Burstable
系统用完了全部内存,且没有 Best-Effort 类型的容器可以被 kill 时,该类型的 pods 会被 kill 掉。pod 中只要有一个容器的 requests 和 limits 的设置不相同,该 pod 的 QoS 即为 Burstable
比如容器 foo 指定了 resource,而容器 bar 未指定:
containers:
name: foo
resources:
limits:
cpu: 10m
memory: 1Gi
requests:
cpu: 10m
memory: 1Gi
name: bar
或者容器 foo 设置了内存 limits,而容器 bar 设置了 CPU limits:
上面两种情况定义的 pod 都属于 Burstable 类别的 QoS
另外需要注意
- 若容器指定了 requests 而未指定 limits,则 limits 的值等于节点资源的最大值
- 若容器指定了 limits 而未指定 requests,则 requests 的值等于 limits
尽最大努力 Best-Effort
系统用完了全部内存时,该类型 pods 会最先被 kill 掉。如果 pod 中所有容器的 resources 均未设置 requests 与 limits,那么该 pod 的 QoS 即为 Best-Effort
比如容器 foo 和容器 bar 均未设置 requests 和 limits:
QoS 解析
首先我们要明确在调度时调度器只会根据 requests 值进行调度
oom_score_adj 的计算
当系统 OOM 时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对 memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值高的进程
可以使用命令查看进程的 OOMScore
kubernetes 借助系统的 OOM KILL 提升服务质量
关于不同等级的定义
const (
// KubeletOOMScoreAdj is the OOM score adjustment for Kubelet
KubeletOOMScoreAdj int = -999
// KubeProxyOOMScoreAdj is the OOM score adjustment for kube-proxy
KubeProxyOOMScoreAdj int = -999
guaranteedOOMScoreAdj int = -997
besteffortOOMScoreAdj int = 1000
)
规则也比较简单只有一段代码:
详见:https://github.com/kubernetes/kubernetes/blob/v1.26.0/pkg/kubelet/qos/policy.go#L40
// GetContainerOOMScoreAdjust returns the amount by which the OOM score of all processes in the
// container should be adjusted.
// The OOM score of a process is the percentage of memory it consumes
// multiplied by 10 (barring exceptional cases) + a configurable quantity which is between -1000
// and 1000. Containers with higher OOM scores are killed if the system runs out of memory.
// See https://lwn.net/Articles/391222/ for more information.
func GetContainerOOMScoreAdjust(pod *v1.Pod, container *v1.Container, memoryCapacity int64) int {
if types.IsNodeCriticalPod(pod) {
// Only node critical pod should be the last to get killed.
return guaranteedOOMScoreAdj
}
switch v1qos.GetPodQOS(pod) {
case v1.PodQOSGuaranteed:
// Guaranteed containers should be the last to get killed.
return guaranteedOOMScoreAdj
case v1.PodQOSBestEffort:
return besteffortOOMScoreAdj
}
// Burstable containers are a middle tier, between Guaranteed and Best-Effort. Ideally,
// we want to protect Burstable containers that consume less memory than requested.
// The formula below is a heuristic. A container requesting for 10% of a system's
// memory will have an OOM score adjust of 900. If a process in container Y
// uses over 10% of memory, its OOM score will be 1000. The idea is that containers
// which use more than their request will have an OOM score of 1000 and will be prime
// targets for OOM kills.
// Note that this is a heuristic, it won't work if a container has many small processes.
memoryRequest := container.Resources.Requests.Memory().Value()
oomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity
// A guaranteed pod using 100% of memory can have an OOM score of 10. Ensure
// that burstable pods have a higher OOM score adjustment.
if int(oomScoreAdjust) < (1000 + guaranteedOOMScoreAdj) {
return (1000 + guaranteedOOMScoreAdj)
}
// Give burstable pods a higher chance of survival over besteffort pods.
if int(oomScoreAdjust) == besteffortOOMScoreAdj {
return int(oomScoreAdjust - 1)
}
return int(oomScoreAdjust)
}
首先看这个容器所属的 Pod 是属于什么级别的
- 如果是 Guaranteed 级别的直接返回 -998 也是最高级最后被 Kill 掉的
- 如果是 BestEffort 级别则直接返回 1000 是最低级别的,最有可能被杀掉
- 如果是 Burstable 则是中间级别需要按照资源的申请量来计算 oom score
Burstable 类型的计算:
就是这段公式计算出容器的 score,最后得到的值会在 2-999 之间,从这个公式能够看出来 Burstable 级别的:
- 如果越是资源申请的越多则给的分越低,这就意味着容器越不容易被杀掉
- 如果是申请量越少得出的分越高则越容易被杀掉
OOMScore 的取值范围为 [-1000, 1000]
,Guaranteed 类型的 pod 的默认值为 -998,Burstable pod 的值为 2~999
,BestEffort pod 的值为 1000,也就是说当系统 OOM 时,首先会 kill 掉 BestEffort pod 的进程,若系统依然处于 OOM 状态,然后才会 kill 掉 Burstable pod,最后是 Guaranteed pod
cgroup
Kubernetes 是通过 cgroup 给 pod 设置 QoS 级别的
kubelet 中有一个 --cgroups-per-qos
参数(默认启用),启用后 kubelet 会为不同 QoS 创建对应的 level cgroups,在 Qos level cgroups 下也会为 pod 下的容器创建对应的 level cgroups,从 Qos –> pod –> container
,层层限制每个 level cgroups 的资源使用量
由于我们这里使用的是 containerd 这种容器运行时,则 cgroup 的路径与之前的 docker 不太一样:
- Guaranteed 类型的 cgroup level 会直接创建在
RootCgroup/kubepods.slice/kubepods-pod<uid>.slice/cri-containerd:<container-id>
- Burstable 的创建在
RootCgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<uid>.slice/cri-containerd:<container-id>
- BestEffort 类型的创建在
RootCgroup/kubepods.slice/kubepods-besteffort/kubepods-besteffort-pod<uid>.slice/cri-containerd:<container-id>
可以通过 mount | grep cgroup
命令查看 RootCgroup:
> mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
none on /run/cilium/cgroupv2 type cgroup2 (rw,relatime)
在 cgroup 的每个子系统下都会创建 QoS level cgroups, 此外在对应的 QoS level cgroups 还会为 pod 创建 Pod level cgroups。比如我们创建一个如下所示的 Pod:
# qos-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: qos-demo
spec:
containers:
- name: nginx
image: nginx:latest
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
cpu: 500m
memory: 2Gi
由于该 pod 的设置的资源 requests != limits,所以其属于 Burstable 类别的 pod,kubelet 会在其所属 QoS 下创建 RootCgroup/system.slice/kubepods-burstable-pod<uid>.slice:cri-containerd:<container-id>
这个 cgroup level,比如查看内存这个子系统的 cgroup:
# 还有一个 pause 容器的 cgroup level
> ls /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-poddba294ab_05fe_4314_a6d0_f9e0b3848104.slice
total 0
drwxr-xr-x 4 root root 0 Dec 26 15:52 .
drwxr-xr-x 15 root root 0 Dec 26 15:51 ..
drwxr-xr-x 2 root root 0 Dec 26 15:51 cri-containerd-0b4746699dcf58de9023ea469c9209ad35bcd03535c95e44343983c7c1e4ec4a.scope
# 通过 crictl ps 可查看到容器 id
drwxr-xr-x 2 root root 0 Dec 26 15:52 cri-containerd-d009a3eaf084cf7ac26a2fa6d39a27c44e24268a7121f56a41ce8931eeffea70.scope
-r--r--r-- 1 root root 0 Dec 26 15:51 cgroup.controllers
-r--r--r-- 1 root root 0 Dec 26 15:51 cgroup.events
...
-r--r--r-- 1 root root 0 Dec 26 15:51 misc.current
-rw-r--r-- 1 root root 0 Dec 26 15:51 misc.max
-r--r--r-- 1 root root 0 Dec 26 15:51 pids.current
-r--r--r-- 1 root root 0 Dec 26 15:51 pids.events
-rw-r--r-- 1 root root 0 Dec 26 15:51 pids.max
-r--r--r-- 1 root root 0 Dec 26 15:51 rdma.current
-rw-r--r-- 1 root root 0 Dec 26 15:51 rdma.max
上面创建的应用容器进程 ID 会被写入到上面的 tasks 文件中:
这样我们的容器进程就会受到该 cgroup 的限制了,在 pod 的资源清单中我们设置了 memory 的 limits 值为 2Gi,kubelet 则会将该限制值写入到 memory.limit_in_bytes
中去:
同样对于 cpu 资源一样可以在对应的子系统中找到创建的对应 cgroup:
建议
如果资源充足,可将 QoS pods 类型均设置为 Guaranteed,用计算资源换业务性能和稳定性,减少排查问题时间和成本
如果想更好的提高资源利用率,业务服务可以设置为 Guaranteed,而其他服务根据重要程度可分别设置为 Burstable 或 Best-Effort