跳转至

服务质量

服务质量

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 主要分为GuaranteedBurstableBest-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:

containers:
  name: foo
    resources:
      limits:
        memory: 1Gi
  name: bar
    resources:
      limits:
        cpu: 100m

上面两种情况定义的 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:

containers:
  name: foo
    resources:
  name: bar
    resources:

QoS 解析

首先我们要明确在调度时调度器只会根据 requests 值进行调度

oom_score_adj 的计算

当系统 OOM 时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对 memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值高的进程

可以使用命令查看进程的 OOMScore

cat /proc/$PID/oom_score

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 类型的计算:

oomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity

就是这段公式计算出容器的 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 文件中:

> cat memory.max

2147483648 # 2147483648 / 1024 / 1024 / 1024 = 2

这样我们的容器进程就会受到该 cgroup 的限制了,在 pod 的资源清单中我们设置了 memory 的 limits 值为 2Gi,kubelet 则会将该限制值写入到 memory.limit_in_bytes 中去:

同样对于 cpu 资源一样可以在对应的子系统中找到创建的对应 cgroup:

> cat cpu.max

50000 100000 # 500m

建议

如果资源充足,可将 QoS pods 类型均设置为 Guaranteed,用计算资源换业务性能和稳定性,减少排查问题时间和成本

如果想更好的提高资源利用率,业务服务可以设置为 Guaranteed,而其他服务根据重要程度可分别设置为 Burstable 或 Best-Effort