21-优先级调度:你必须掌握的 Pod 抢占式资源调度

发布时间 2024-01-11 13:44:09作者: miao酱

随着我们在 Kubernetes 集群中部署越来越多的业务,势必要考虑集群的资源利用率问题。尤其是当集群资源比较紧张的时候,如果此时还要部署一些比较重要的关键业务,那么该如何去提前“抢占”集群资源,从而使得关键业务在集群中跑起来呢?

这里一个最常见的做法就是采用优先级方案。通过给 Pod 设置高优先级,让其比其他 Pod 显得更为重要,通过这种“插队”的方式优先获得调度器的调度。现在我们就来看看 Kubernetes 是如何定义这种优先级的。

PriorityClass

Kubernetes 通过对象 PriorityClass 来定义上述优先级,具体怎么定义呢?我们可以通过之前说过的kubectl create命令来创建这个对象:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."

这里我们创建了就是一个名为 high-priority 的 PriorityClass 定义,使用的 API 版本是 scheduling.k8s.io/v1。这个对象是个集群级别的定义,并不属于任何 namespace,可以被全局使用,即各个 namespace 中创建的 Pod 都能使用 PriorityClass。我们在定义这样一个 PriorityClass 对象的时候,名字不可以包含 system- 这个前缀。

关于这个数值的设置,有几点要说明。系统自定义了两个优先级值:

// HighestUserDefinablePriority is the highest priority for user defined priority classes. Priority values larger than 1 billion are reserved for Kubernetes system use.
HighestUserDefinablePriority = int32(1000000000)
// SystemCriticalPriority is the beginning of the range of priority values for critical system components.
SystemCriticalPriority = 2 * HighestUserDefinablePriority

那么当你自己创建 PriorityClass 的时候,定义的优先级值是不能高于这里的 HighestUserDefinablePriority 的, 即 1000000000 (10 亿)。

Kubernetes 在初始化的时候就自带了两个 PriorityClasses,即 system-cluster-critical 和 system-node-critical,优先级值为 2000000000。 你可以通过下面的命令查看到这两个最高优先级的 PriorityClasse:

$ kubectl get priorityclass
NAME                      VALUE        GLOBAL-DEFAULT   AGE
system-cluster-critical   2000000000   false            59d
system-node-critical      2000001000   false            59d

这是两个公共的优先级类,主要用来确保 Kubernetes 系统的关键组件或者关键插件总是能够优先被调度,比如 coredns 等。因为一旦这些关键组件、关键插件被驱逐,或者由于镜像需要升级,新建的 Pod 无法被调度,始终处于 Pending 的状态,这些都会导致整个集群异常,甚至停止工作。所以对于这些关键的组件,我们务必保证其优先级是最高的,这样才会被最先调度。我们可以来参考下 coredns 的做法,以下是截取的一部分配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  ...
  name: coredns
  namespace: kube-system
  ...
spec:
  ...
  template:
    ...
    spec:
      ...
      priorityClassName: system-cluster-critical
      ...
status:
  ...

下面我们来看看如何在 Pod 中使用 PriorityClass:通过 spec.priorityClassName 即可指定要使用的 PriorityClass:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  priorityClassName: high-priority

通过kubectl create创建好该 Pod 后,我们查看详情可以看到 PriorityClass 的名字和优先级数值都被写入了 Pod 中:

$ kubectl describe pod nginx
Name:                 nginx
Namespace:            default
Priority:             1000000
Priority Class Name:  high-priority
...

注意,在使用的时候不允许自己去设置 spec.priority 的数值,我们只能通过 spec.priorityClassName 来设置优先级。现在,我们回过头再来观察下上面定义的这个 high-priority 的 PriorityClass 对象,里面有两个可选字段:globalDefault 和 description。

globalDefault 用来表明是否将该 PriorityClass 的数值作为默认值,并将其应用在所有未设置 priorityClassName 的 Pod 上。 既然是默认值,那么就意味着整个 Kubernetes 集群中只能存在一个 globalDefault 设为 true 的 PriorityClass 对象。

如果没有任何一个 PriorityClass 对象的 globalDefault 被设置为 true 的话,就意味着所有未设置 spec.priorityClassName 的 Pod 的优先级为 0。即便你新建了某个 PriorityClass 对象,并将其 globalDefault 设置为 true,那么也不不影响集群中的存量 Pod,只会对新建的 Pod 生效。

你可以在 description 字段中对该 PriorityClass 进行一些使用上的解释说明。

现在我们就来做个优先级抢占的测试,使用一个单节点的集群做演示,该节点的 CPU 为 2 核。先创建一个优先级值更小的名为 low-priority 的 PriorityClass,其 YAML 文件如下:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 1000
globalDefault: false

创建好了以后,通过kubectl get可以查看到创建的这两个 PriorityClass:

$ kubectl get priorityclass | grep -v system
NAME                      VALUE        GLOBAL-DEFAULT   AGE
high-priority             1000000      false            30m
low-priority              1000         false            8m35s

现在我们分别使用这两个 PriorityClass 创建两个 Pod。先来创建一个使用 low-priority 作为优先级的 Pod,下面是其对应的 YAML 文件:

apiVersion: v1
kind: Pod
metadata:
 name: nginx-low-pc
spec:
 containers:
 - name: nginx
   image: nginx
   imagePullPolicy: IfNotPresent
   resources:
    requests:
     memory: "64Mi"
     cpu: "1200m"       #CPU需求设置较大
    limits:
     memory: "128Mi"
     cpu: "1300m"
 priorityClassName: low-priority     #使用低优先级

这里我们指定了一个较大比重的 CPU 资源。我们假定集群中的节点为 2 核,把 Pod 需要的最低 CPU 需求设置为1200m,超过了 2核 CPU 总算力的一半。

同样我们使用 high-priority 作为其优先级创建一个 Pod,其对应的 YAML 文件如下:

apiVersion: v1
kind: Pod
metadata:
 name: nginx-high-pc
spec:
 containers:
 - name: nginx
   image: nginx
   imagePullPolicy: IfNotPresent
   resources:
    requests:
     memory: "64Mi"
     cpu: "1200m"
    limits:
     memory: "128Mi"
     cpu: "1300m"
 priorityClassName: high-priority        #使用高优先级

如此一来,这两个 Pod 无法被调度到同一个只有 2 核 CPU 的节点上的,因为 2 个 Pod 加起来的 CPU 请求总量超过了 2 核。

我们可以先创建低优先级 Pod 命名为 nginx-low-pc ,可以看到该 Pod 已经成功运行,并消耗了超过一半的 CPU:

$ kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
nginx-low-pc   1/1     Running   0          22s
$ kubectl describe pod nginx-low-pc
...
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests     Limits
--------           --------     ------
  cpu                1220m (61%)  1300m (65%)       #Node的CPU使用率已经过半
  memory             64Mi (1%)    128Mi (3%)
  ephemeral-storage  0 (0%)       0 (0%)

现在我们再创建高优先级 Pod 命名为 nginx-high-pc,可以看到低优先级 Pod nginx-low-pc 由于高优先级的资源抢占被驱逐,运行状态也从 Runing 状态变为了 Terminating:

$ kubectl get pods
NAME            READY   STATUS        RESTARTS   AGE
nginx-high-pc   0/1     Pending       0          7s
nginx-low-pc    0/1     Terminating   0          87s
$ kubectl get pods
NAME            READY   STATUS    RESTARTS   AGE
nginx-high-pc   1/1     Running   0          12s

最终,高优先级 Pod 成功完成了抢占,整个节点上只有它在运行。当新 Pod 被创建后,它们就进入了调度器的队列,等待被调度。调度器会根据 PriorityClass 的优先级来挑选出优先级最高的 Pod,并且尝试把它调度到一个节点上。

如果这个时候没有任何一个节点能够满足这个 Pod的所有要求,包括资源、调度偏好等要求,就会触发抢占逻辑。调度器会尝试寻找一个节点,通过移除一个或者多个比该 Pod 的优先级低的 Pod, 尝试使目标 Pod 可以被调度。

如果找到了这样一个节点,那么运行在该节点上的一个或者多个低优先级的 Pod 就会被驱逐,这样这个高优先级的 Pod 就可以调度到目标节点上了。哈哈,是不是有点“鸠占鹊巢”的感觉!

当然对于某些场景,如果你并不希望 Pod 被驱逐掉,只是希望可以优先调度,那么你可以使用非抢占式的 PriorityClass。比如这是一个非抢占式的 PriorityClass 的配置:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority-nonpreempting
value: 1000000
preemptionPolicy: Never
globalDefault: false
description: "This priority class will not cause other pods to be preempted."

这个配置跟我们上面的定义基本一致,只不过多了一个字段 preemptionPolicy。这样就可以避免属于该 PriorityClass 的 Pod 抢占其他 Pod。当然你也可以通过给 kube-scheduler 设置参数禁用抢占能力。但是关键组件的 Pod 还是依赖调度器这种抢占机制,以便在集群资源压力较大时可以优先得到调度。因此,不建议你禁用抢占能力。

kube-scheduler 的抢占能力是通过 disablePreemption 这个参数来控制的,该标志默认为 false。如果你还是坚持禁用抢占,可以将 disablePreemption 设置为true,目前这一参数的配置,只能通过 KubeSchedulerConfiguration 来设置,即:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration
algorithmSource:
  provider: DefaultProvider
...
disablePreemption: true

写在最后

提高集群的资源利用率最常见的做法就是采用优先级的方案。在实际使用的时候,要避免恶意用户创建高优先级的 Pod,这样会导致其他 Pod 被驱逐或者无法调度,严重影响集群的稳定性和安全性。集群管理员可以为特定用户创建特定优先级级别,来防止他们恶意使用高优先级的 PriorityClass。

到这里这节课就结束了,如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。