调度流程
kube-scheduler 的主要作用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序,启动之后会一直监听 API Server,获取到 PodSpec.NodeName 为空的 Pod,对每个 Pod 都会创建一个 binding。
这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:
- 如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置一定都是一样的
- 如何保证每个节点都能被分配资源?
- 集群资源如何能够被高效利用?
- 集群资源如何才能被最大化使用?
- 如何保证 Pod 调度的性能和效率?
- 用户是否可以根据自己的实际需求定制自己的调度策略?
考虑到实际环境中的各种复杂情况,kubernetes 的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发,我们可以自定义一个调度器并以插件形式和 kubernetes 进行集成。
kubernetes 调度器的源码位于 kubernetes/pkg/scheduler 中,其中 Scheduler 创建和运行的核心程序,对应的代码在 pkg/scheduler/scheduler.go,如果要查看 kube-scheduler 的入口程序,对应的代码在 cmd/kube-scheduler/scheduler.go。
调度主要分为以下几个部分:
- 首先是预选过程,过滤掉不满足条件的节点,这个过程称为 Predicates(过滤)
- 然后是优选过程,对通过的节点按照优先级排序,称之为 Priorities(打分)
- 最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误
Predicates 阶段首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的节点将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。
所以我们在部署应用的时候,如果发现有 Pod 一直处于 Pending 状态,那么就是没有满足调度条件的节点,这个时候可以去检查下节点资源是否可用。
Priorities 阶段即再次对节点进行筛选,如果有多个节点都满足条件的话,那么系统会按照节点的优先级(priorites)大小对节点进行排序,最后选择优先级最高的节点来部署 Pod 应用。
下面是调度过程的简单示意图:
更详细的流程是这样的:
- 首先,客户端通过 API Server 的 REST API 或者 kubectl 工具创建 Pod 资源
- API Server 收到用户请求后,存储相关数据到 etcd 数据库中
- 调度器监听 API Server 查看到还未被调度(bind)的 Pod 列表,循环遍历地为每个 Pod 尝试分配节点,这个分配过程就是我们上面提到的两个阶段:
- 预选阶段(Predicates),过滤节点,调度器用一组规则过滤掉不符合要求的 Node 节点,比如 Pod 设置了资源的 request,那么可用资源比 Pod 需要的资源少的主机显然就会被过滤掉
- 优选阶段(Priorities),为节点的优先级打分,将上一阶段过滤出来的 Node 列表进行打分,调度器会考虑一些整体的优化策略,比如把 Deployment 控制的多个 Pod 副本尽量分布到不同的主机上,使用最低负载的主机等等策略
- 经过上面的阶段过滤后选择打分最高的 Node 节点和 Pod 进行 binding 操作,然后将结果存储到 etcd 中 最后被选择出来的 Node 节点对应的 kubelet 去执行创建 Pod 的相关操作(当然也是 watch APIServer 发现的)。
我们知道每当调度一个 Pod 时,都会按照两个过程来执行:调度过程和绑定过程。
调度过程为 Pod 选择一个合适的节点,绑定过程则将调度过程的决策应用到集群中(也就是在被选定的节点上运行 Pod),将调度过程和绑定过程合在一起,称之为调度上下文(scheduling context)。需要注意的是调度过程是同步运行的(同一时间点只为一个 Pod 进行调度),绑定过程可异步运行(同一时间点可并发为多个 Pod 执行绑定)。
调度过程和绑定过程遇到如下情况时会中途退出:(这个时候,该 Pod 将被放回到 待调度队列,并等待下次重试。)
- 调度程序认为当前没有该 Pod 的可选节点
- 内部错误
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-scheduler
spec:
selector:
matchLabels:
app: test-scheduler
template:
metadata:
labels:
app: test-scheduler
spec:
schedulerName: sample-scheduler # 指定使用的调度器,不指定使用默认的default-scheduler
containers:
- image: nginx:1.7.9
imagePullPolicy: IfNotPresent
name: nginx
ports:
- containerPort: 80
从 Kubernetes v1.17 版本开始,Scheduler Framework 内置的预选和优选函数已经全部插件化,所以要扩展调度器我们应该掌握并理解调度框架这种方式。
调度器调优
percentageOfNodesToScore:允许调度器在找到一定数量的可调度节点之后就停止继续寻找可调度节点。该功能能提高调度器在大规模集群下的调度性能,这个数值是集群规模的百分比,这个百分比通过 percentageOfNodesToScore 参数来进行配置,其值的范围在 1 到 100 之间,最大值就是 100%,如果设置为 0 就代表没有提供这个参数配置
设置这个值的作用在于:当集群的规模是数百个节点并且 percentageOfNodesToScore 参数设置的过低的时候,调度器筛选到的可调度节点数目基本不会受到该参数影响。当集群规模较小时,这个设置对调度器性能提升并不明显,但是在超过 1000 个 Node 的集群中,将调优参数设置为一个较低的值可以很明显的提升调度器性能。
作为 kubernetes 集群的默认调度器,kube-scheduler 主要负责将 Pod 调度到集群的 Node 上。在一个集群中,满足一个 Pod 调度请求的所有节点称之为 可调度 Node,调度器先在集群中找到一个 Pod 的可调度 Node,然后根据一系列函数对这些可调度 Node 进行打分,之后选出其中得分最高的 Node 来运行 Pod,最后,调度器将这个调度决定告知 kube-apiserver,这个过程叫做绑定。
优先级调度
与前面所讲的调度优选策略中的优先级(Priorities)不同,前面所讲的优先级指的是节点优先级,而我们这里所说的优先级指的是 Pod 的优先级,高优先级的 Pod 会优先被调度,或者在资源不足的情况牺牲低优先级的 Pod,以便于重要的 Pod 能够得到资源部署。
要定义 Pod 优先级,就需要先定义 PriorityClass 对象,该对象没有 Namespace 的限制:
apiVersion: v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."
其中:
- value 为 32 位整数的优先级,该值越大,优先级越高
- globalDefault 用于未配置 PriorityClassName 的 Pod,整个集群中应该只有一个 PriorityClass 将其设置为 true
然后通过在 Pod 的 spec.priorityClassName 中指定已定义的 PriorityClass 名称即可:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityClassName: high-priority
另外一个值得注意的是当节点没有足够的资源供调度器调度 Pod,导致 Pod 处于 pending 时,抢占(preemption)逻辑就会被触发,抢占会尝试从一个节点删除低优先级的 Pod,从而释放资源使高优先级的 Pod 得到节点资源进行部署。
亲和性和反亲和性
nodeSelector
在了解亲和性之前,我们先来了解一个非常常用的调度方式:nodeSelector。我们知道 label 标签是 kubernetes 中一个非常重要的概念,用户可以非常灵活的利用 label 来管理集群中的资源,比如最常见的 Service 对象通过 label 去匹配 Pod 资源,而 Pod 的调度也可以根据节点的 label 来进行调度
我们可以通过下面的命令查看我们的 node 的 label:
➜ kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
master1 Ready control-plane,master 82d v1.22.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master1,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers=
node1 Ready <none> 82d v1.22.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1,kubernetes.io/os=linux
node2 Ready <none> 82d v1.22.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux
现在我们先给节点 node2 增加一个com=youdianzhishi
的标签,命令如下:
➜ kubectl label nodes node2 com=youdianzhishi
node/node2 labeled
我们可以通过上面的 --show-labels
参数可以查看上述标签是否生效。当节点被打上了相关标签后,在调度的时候就可以使用这些标签了,只需要在 Pod 的 spec 字段中添加 nodeSelector 字段,里面是我们需要被调度的节点的 label 标签,比如,下面的 Pod 我们要强制调度到 node2 这个节点上去,我们就可以使用 nodeSelector 来表示了:
# node-selector-demo.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
app: busybox-pod
name: test-busybox
spec:
containers:
- command:
- sleep
- "3600"
image: busybox
imagePullPolicy: Always
name: test-busybox
nodeSelector:
com: youdianzhishi
然后我们可以通过 describe 命令查看调度结果:
➜ kubectl apply -f pod-selector-demo.yaml
pod/test-busybox created
➜ kubectl describe pod test-busybox
Name: test-busybox
Namespace: default
Priority: 0
Node: node2/192.168.31.46
......
Node-Selectors: com=youdianzhishi
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled <unknown> default-scheduler Successfully assigned default/test-busybox to node2
Normal Pulling 13s kubelet, node2 Pulling image "busybox"
Normal Pulled 10s kubelet, node2 Successfully pulled image "busybox"
Normal Created 10s kubelet, node2 Created container test-busybox
Normal Started 9s kubelet, node2 Started container test-busybox
我们可以看到 Events 下面的信息,我们的 Pod 通过默认的 default-scheduler 调度器被绑定到了 node2 节点。不过需要注意的是nodeSelector 属于强制性的,如果我们的目标节点没有可用的资源,我们的 Pod 就会一直处于 Pending 状态。
通过上面的例子我们可以感受到 nodeSelector 的方式比较直观,但是还够灵活,控制粒度偏大,接下来我们再和大家了解下更加灵活的方式:节点亲和性(nodeAffinity)。
亲和性和反亲和性调度
经过了 predicates 和 priorities 两个阶段,但是在实际的生产环境中,往往我们需要根据自己的一些实际需求来控制 Pod 的调度,这就需要用到 nodeAffinity(节点亲和性)
、podAffinity(pod 亲和性)
以及 podAntiAffinity(pod 反亲和性)
。
亲和性调度可以分成软策略和硬策略两种方式:
- 软策略就是如果现在没有满足调度要求的节点的话,Pod 就会忽略这条规则,继续完成调度过程,说白了就是满足条件最好了,没有的话也无所谓
- 硬策略就比较强硬了,如果没有满足条件的节点的话,就不断重试直到满足条件为止,简单说就是你必须满足我的要求,不然就不干了
对于亲和性和反亲和性都有这两种规则可以设置: preferredDuringSchedulingIgnoredDuringExecution
和requiredDuringSchedulingIgnoredDuringExecution
,前面的就是软策略,后面的就是硬策略。
节点亲和性
节点亲和性(nodeAffinity)主要是用来控制 Pod 要部署在哪些节点上,以及不能部署在哪些节点上的,它可以进行一些简单的逻辑组合了,不只是简单的相等匹配
比如现在我们用一个 Deployment 来管理8个 Pod 副本,现在我们来控制下这些 Pod 的调度,如下例子:
# node-affinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-affinity
labels:
app: node-affinity
spec:
replicas: 8
selector:
matchLabels:
app: node-affinity
template:
metadata:
labels:
app: node-affinity
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
name: nginxweb
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- master1
preferredDuringSchedulingIgnoredDuringExecution: # 软策略
- weight: 1
preference:
matchExpressions:
- key: com
operator: In
values:
- youdianzhishi
上面这个 Pod 首先是要求不能运行在 master1 这个节点上,如果有个节点满足 com=youdianzhishi 的话就优先调度到这个节点上。
由于上面 node02 节点我们打上了 com=youdianzhishi 这样的 label 标签,所以按要求会优先调度到这个节点来的,现在我们来创建这个 Pod,然后查看具体的调度情况是否满足我们的要求。
➜ kubectl apply -f node-affinty-demo.yaml
deployment.apps/node-affinity created
➜ kubectl get pods -l app=node-affinity -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
node-affinity-cdd9d54d9-bgbbh 1/1 Running 0 2m28s 10.244.2.247 node2 <none> <none>
node-affinity-cdd9d54d9-dlbck 1/1 Running 0 2m28s 10.244.4.16 node1 <none> <none>
node-affinity-cdd9d54d9-g2jr6 1/1 Running 0 2m28s 10.244.4.17 node1 <none> <none>
node-affinity-cdd9d54d9-gzr58 1/1 Running 0 2m28s 10.244.1.118 node1 <none> <none>
node-affinity-cdd9d54d9-hcv7r 1/1 Running 0 2m28s 10.244.2.246 node2 <none> <none>
node-affinity-cdd9d54d9-kvxw4 1/1 Running 0 2m28s 10.244.2.245 node2 <none> <none>
node-affinity-cdd9d54d9-p4mmk 1/1 Running 0 2m28s 10.244.2.244 node2 <none> <none>
node-affinity-cdd9d54d9-t5mff 1/1 Running 0 2m28s 10.244.1.117 node2 <none> <none>
从结果可以看出有5个 Pod 被部署到了 node2 节点上,但是可以看到并没有一个 Pod 被部署到 master1 这个节点上,因为我们的硬策略就是不允许部署到该节点上,而 node2 是软策略,所以会尽量满足。这里的匹配逻辑是 label 标签的值在某个列表中,现在 Kubernetes 提供的操作符有下面的几种:
- In:label 的值在某个列表中
- NotIn:label 的值不在某个列表中
- Gt:label 的值大于某个值
- Lt:label 的值小于某个值
- Exists:某个 label 存在
- DoesNotExist:某个 label 不存在
但是需要注意的是如果 nodeSelectorTerms 下面有多个选项的话,满足任何一个条件就可以了;如果 matchExpressions有多个选项的话,则必须同时满足这些条件才能正常调度 Pod。
Pod 亲和性
Pod 亲和性(podAffinity)主要解决 Pod 可以和哪些 Pod 部署在同一个拓扑域中的问题(其中拓扑域用主机标签实现,可以是单个主机,也可以是多个主机组成的 cluster、zone 等等),而 Pod 反亲和性主要是解决 Pod 不能和哪些 Pod 部署在同一个拓扑域中的问题,它们都是处理的 Pod 与 Pod 之间的关系,比如一个 Pod 在一个节点上了,那么我这个也得在这个节点,或者你这个 Pod 在节点上了,那么我就不想和你待在同一个节点上。
由于我们这里只有一个集群,并没有区域或者机房的概念,所以我们这里直接使用主机名来作为拓扑域,把 Pod 创建在同一个主机上面。
➜ kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
master1 Ready control-plane,master 82d v1.22.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master1,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers=
node1 Ready <none> 82d v1.22.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1,kubernetes.io/os=linux
node2 Ready <none> 82d v1.22.2 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux,com=youdianzhishi
同样,还是针对上面的资源对象,我们来测试下 Pod 的亲和性:
# pod-affinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pod-affinity
labels:
app: pod-affinity
spec:
replicas: 3
selector:
matchLabels:
app: pod-affinity
template:
metadata:
labels:
app: pod-affinity
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
name: nginxweb
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- busybox-pod
topologyKey: kubernetes.io/hostname
上面这个例子中的 Pod 需要调度到某个指定的节点上,并且该节点上运行了一个带有 app=busybox-pod 标签的 Pod。我们可以查看有标签 app=busybox-pod 的 pod 列表:
➜ kubectl get pods -l app=busybox-pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
test-busybox 1/1 Running 0 27m 10.244.2.242 node2 <none> <none>
我们看到这个 Pod 运行在了 node2 的节点上面,所以按照上面的亲和性来说,上面我们部署的3个 Pod 副本也应该运行在 node2 节点上:
➜ kubectl apply -f pod-affinity-demo.yaml
deployment.apps/pod-affinity created
➜ kubectl get pods -o wide -l app=pod-affinity
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-affinity-587f9b5b58-5nxmf 1/1 Running 0 26s 10.244.2.249 node2 <none> <none>
pod-affinity-587f9b5b58-m2j7s 1/1 Running 0 26s 10.244.2.248 node2 <none> <none>
pod-affinity-587f9b5b58-vrd7b 1/1 Running 0 26s 10.244.2.250 node2 <none> <none>
如果我们把上面的 test-busybox 和 pod-affinity 这个 Deployment 都删除,然后重新创建 pod-affinity 这个资源,看看能不能正常调度呢:
➜ kubectl delete -f node-selector-demo.yaml
pod "test-busybox" deleted
➜ kubectl delete -f pod-affinity-demo.yaml
deployment.apps "pod-affinity" deleted
➜ kubectl apply -f pod-affinity-demo.yaml
deployment.apps/pod-affinity created
➜ kubectl get pods -o wide -l app=pod-affinity
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-affinity-587f9b5b58-bbfgr 0/1 Pending 0 18s <none> <none> <none> <none>
pod-affinity-587f9b5b58-lwc8n 0/1 Pending 0 18s <none> <none> <none> <none>
pod-affinity-587f9b5b58-pc7ql 0/1 Pending 0 18s <none> <none> <none> <none>
我们可以看到都处于 Pending 状态了,这是因为现在没有一个节点上面拥有 app=busybox-pod 这个标签的 Pod,而上面我们的调度使用的是硬策略,所以就没办法进行调度了,大家可以去尝试下重新将 test-busybox 这个 Pod 调度到其他节点上,观察下上面的3个副本会不会也被调度到对应的节点上去。
Pod反亲和性
Pod 反亲和性(podAntiAffinity)则是反着来的,比如一个节点上运行了某个 Pod,那么我们的模板 Pod 则不希望被调度到这个节点上面去了。我们把上面的 podAffinity 直接改成 podAntiAffinity:
污点与容忍
对于 nodeAffinity 无论是硬策略还是软策略方式,都是调度 Pod 到预期节点上,而污点(Taints)恰好与之相反,如果一个节点标记为 Taints ,除非 Pod 也被标识为可以容忍污点节点,否则该 Taints 节点不会被调度 Pod。
我们使用 kubeadm 搭建的集群默认就给 master 节点添加了一个污点标记,所以我们看到我们平时的 Pod 都没有被调度到 master 上去:
➜ kubectl describe node master1
Name: master1
Roles: master
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
kubernetes.io/arch=amd64
kubernetes.io/hostname=master1
kubernetes.io/os=linux
node-role.kubernetes.io/master=
......
Taints: node-role.kubernetes.io/master:NoSchedule
Unschedulable: false
......
我们可以使用上面的命令查看 master 节点的信息,其中有一条关于 Taints 的信息:node-role.kubernetes.io/master:NoSchedule
,就表示master 节点打了一个污点的标记,其中影响的参数是 NoSchedule
,表示 Pod 不会被调度到标记为 taints 的节点,除了 NoSchedule 外,还有另外两个选项:
- PreferNoSchedule:NoSchedule 的软策略版本,表示尽量不调度到污点节点上去
- NoExecute:该选项意味着一旦 Taint 生效,如该节点内正在运行的 Pod 没有对应容忍(Tolerate)设置,则会直接被逐出
污点 taint 标记节点的命令如下:
➜ kubectl taint nodes node2 test=node2:NoSchedule
node "node2" tainted
上面的命名将 node2 节点标记为了污点,影响策略是 NoSchedule,只会影响新的 Pod 调度,如果仍然希望某个 Pod 调度到 taint 节点上,则必须在 Spec 中做出 Toleration 定义,才能调度到该节点,比如现在我们想要将一个 Pod 调度到 master 节点:
# taint-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: taint
labels:
app: taint
spec:
replicas: 3
selector:
matchLabels:
app: taint
template:
metadata:
labels:
app: taint
spec:
containers:
- name: nginx
image: nginx
ports:
- name: http
containerPort: 80
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
由于 master 节点被标记为了污点,所以我们这里要想 Pod 能够调度到改节点去,就需要增加容忍的声明:
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
然后创建上面的资源,查看结果:
我们可以看到有一个 Pod 副本被调度到了 master 节点,这就是容忍的使用方法。
对于 tolerations 属性的写法,其中的 key、value、effect 与 Node 的 Taint 设置需保持一致, 还有以下几点说明:
- 如果 operator 的值是 Exists,则 value 属性可省略
- 如果 operator 的值是 Equal,则表示其 key 与 value 之间的关系是 equal(等于)
- 如果不指定 operator 属性,则默认值为 Equal
Taints: node-role.kubernetes.io/master:NoSchedule
另外,还有两个特殊值:
- 空的 key 如果再配合 Exists 就能匹配所有的 key 与 value,也就是是能容忍所有节点的所有 Taints
- 空的 effect 匹配所有的 effect
污点 taint 标记节点的命令如下:
➜ kubectl taint nodes node2 test=node2:NoSchedule
node "node2" tainted
最后如果我们要取消节点的污点标记,可以使用下面的命令:
➜ kubectl taint nodes node2 test-
node "node2" untainted
课后习题
1、不用 DaemonSet,如何使用 Deployment 是否实现同样的功能?
我们知道 DaemonSet 控制器的功能就是在每个节点上运行一个 Pod,如何要使用 Deployment 来实现,首先就要设置副本数量为节点数,比如我们这里加上 master 节点一共3个节点,则要设置3个副本,要在 master 节点上执行自然要添加容忍,那么要如何保证一个节点上只运行一个 Pod 呢?是不是前面的提到的 Pod 反亲和性就可以实现,以自己 Pod 的标签来进行过滤校验即可,新的 Pod 不能运行在一个已经具有该 Pod 的节点上,是不是就是一个节点只能运行一个?模拟的资源清单如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mock-ds-demo
spec:
replicas: 3
selector:
matchLabels:
app: mock-ds-demo
template:
metadata:
labels:
app: mock-ds-demo
spec:
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: ngpt
affinity:
podAntiAffinity: # pod反亲合性
requiredDuringSchedulingIgnoredDuringExecution: # 硬策略
- labelSelector:
matchExpressions:
- key: app # Pod的标签
operator: In
values: ["mock-ds-demo"]
topologyKey: kubernetes.io/hostname # 以hostname为拓扑域
创建上面的资源清单验证:
➜ kubectl get nodes
NAME STATUS ROLES AGE VERSION
master1 Ready control-plane,master 84d v1.22.2
node1 Ready <none> 84d v1.22.2
node2 Ready <none> 84d v1.22.2
➜ kubectl get pods -l app=mock-ds-demo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
mock-ds-demo-8694759c69-tgqld 1/1 Running 0 30s 10.244.1.198 node1 <none> <none>
mock-ds-demo-8694759c69-wtnwv 1/1 Running 0 30s 10.244.2.29 node2 <none> <none>
mock-ds-demo-8694759c69-zt9pp 1/1 Running 0 30s 10.244.0.135 master1 <none> <none>
2.同样的如果想在每个节点(或指定的一些节点)上运行2个(或多个)Pod 副本,如何实现?
DaemonSet 是在每个节点上运行1个 Pod 副本,显然我们去创建2个(或多个)DaemonSet 即可实现该目标,但是这不是一个好的接近方案,而 PodAntiAffinity 只能将一个 Pod 调度到某个拓扑域中去,所以都不能很好的来解决这个问题。
要实现这种更细粒度的控制,我们可以通过设置拓扑分布约束来进行调度,设置拓扑分布约束来将 Pod 分布到不同的拓扑域下,从而实现高可用性或节省成本,具体实现方式请看下文。
拓扑分布约束❤
同样的如果想在每个节点(或指定的一些节点)上运行2个(或多个)Pod 副本,如何实现?
DaemonSet 是在每个节点上运行1个 Pod 副本,显然我们去创建2个(或多个)DaemonSet 即可实现该目标,但是这不是一个好的接近方案,而 PodAntiAffinity 只能将一个 Pod 调度到某个拓扑域中去,所以都不能很好的来解决这个问题。
要实现这种更细粒度的控制,我们可以通过设置拓扑分布约束来进行调度,设置拓扑分布约束来将 Pod 分布到不同的拓扑域下,从而实现高可用性或节省成本,具体实现方式请看下文。
在 k8s 集群调度中,亲和性相关的概念本质上都是控制 Pod 如何被调度 – 堆叠或打散 。podAffinity 以及 podAntiAffinity 两个特性对 Pod 在不同拓扑域的分布进行了一些控制,podAffinity
可以将无数个 Pod 调度到特定的某一个拓扑域,这是 堆叠 的体现;podAntiAffinity
则可以控制一个拓扑域只存在一个 Pod,这是 打散 的体现。但这两种情况都太极端了,在不少场景下都无法达到理想的效果,例如为了实现容灾和高可用,将业务 Pod 尽可能均匀的分布在不同可用区就很难实现。
PodTopologySpread(Pod 拓扑分布约束)
特性的提出正是为了对 Pod 的调度分布提供更精细的控制,以提高服务可用性以及资源利用率,PodTopologySpread 由 EvenPodsSpread 特性门所控制,在 v1.16 版本第一次发布,并在 v1.18 版本进入 beta 阶段默认启用。
使用规范
在 Pod 的 Spec 规范中新增了一个 topologySpreadConstraints
字段即可配置拓扑分布约束,如下所示:
spec:
topologySpreadConstraints:
- maxSkew: <integer>
topologyKey: <string>
whenUnsatisfiable: <string>
labelSelector: <object>
由于这个新增的字段是在 Pod spec 层面添加,因此更高层级的控制 (Deployment、DaemonSet、StatefulSet) 也能使用 PodTopologySpread 功能。
让我们结合上图来理解 topologySpreadConstraints
中各个字段的含义和作用:
labelSelector
: 用来查找匹配的 Pod,我们能够计算出每个拓扑域中匹配该 label selector 的 Pod 数量,在上图中,假如 label selector 是 app:foo,那么 zone1 的匹配个数为 2, zone2 的匹配个数为 0topologyKey
: 是 Node label 的 key,如果两个 Node 的 label 同时具有该 key 并且值相同,就说它们在同一个拓扑域。在上图中,指定 topologyKey 为 zone, 则具有 zone=zone1 标签的 Node 被分在一个拓扑域,具有 zone=zone2 标签的 Node 被分在另一个拓扑域。maxSkew
: 这个属性理解起来不是很直接,它描述了 Pod 在不同拓扑域中不均匀分布的最大程度(指定拓扑类型中任意两个拓扑域中匹配的 Pod 之间的最大允许差值),它必须大于零。每个拓扑域都有一个 skew 值,计算的公式是: skew[i] = 拓扑域[i]中匹配的 Pod 个数 - min{其他拓扑域中匹配的 Pod 个数}。在上图中,我们新建一个带有 app=foo 标签的 Pod:- 如果该 Pod 被调度到 zone1,那么 zone1 中 Node 的 skew 值变为 3,zone2 中 Node 的 skew 值变为 0 (zone1 有 3 个匹配的 Pod,zone2 有 0 个匹配的 Pod )
- 如果该 Pod 被调度到 zone2,那么 zone1 中 Node 的 skew 值变为 2,zone2 中 Node 的 skew 值变为 1(zone2 有 1 个匹配的 Pod,拥有全局最小匹配 Pod 数的拓扑域正是 zone2 自己 ),则它满足maxSkew: 1 的约束(差值为1)
whenUnsatisfiable
: 描述了如果 Pod 不满足分布约束条件该采取何种策略:- DoNotSchedule (默认) 告诉调度器不要调度该 Pod,因此也可以叫作硬策略;
- ScheduleAnyway 告诉调度器根据每个 Node 的 skew 值打分排序后仍然调度,因此也可以叫作软策略。
单个拓扑约束
假设你拥有一个 4 节点集群,其中标记为 foo:bar 的 3 个 Pod 分别位于 node1、node2 和 node3 中:
如果希望新来的 Pod 均匀分布在现有的可用区域,则可以按如下设置其约束:
kind: Pod
apiVersion: v1
metadata:
name: mypod
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
containers:
- name: pause
image: k8s.gcr.io/pause:3.1
topologyKey: zone
意味着均匀分布将只应用于存在标签键值对为 zone:<any value>
的节点。 whenUnsatisfiable: DoNotSchedule
告诉调度器如果新的 Pod 不满足约束,则不可调度。如果调度器将新的 Pod 放入 “zoneA”,Pods 分布将变为 [3, 1],因此实际的偏差为 2(3 - 1),这违反了 maxSkew: 1 的约定。此示例中,新 Pod 只能放置在 “zoneB” 上:
或者
你可以调整 Pod 约束以满足各种要求:
- 将
maxSkew
更改为更大的值,比如 “2”,这样新的 Pod 也可以放在 “zoneA” 上。 - 将
topologyKey
更改为 “node”,以便将 Pod 均匀分布在节点上而不是区域中。 在上面的例子中,如果maxSkew
保持为 “1”,那么传入的 Pod 只能放在 “node4” 上。 - 将
whenUnsatisfiable: DoNotSchedule
更改为whenUnsatisfiable: ScheduleAnyway
, 以确保新的 Pod 可以被调度。
多个拓扑约束
上面是单个 Pod 拓扑分布约束的情况,下面的例子建立在前面例子的基础上来对多个 Pod 拓扑分布约束进行说明。假设你拥有一个 4 节点集群,其中 3 个标记为 foo:bar 的 Pod 分别位于 node1、node2 和 node3 上
我们可以使用 2 个 TopologySpreadConstraint 来控制 Pod 在区域和节点两个维度上的分布:
# two-constraints.yaml
kind: Pod
apiVersion: v1
metadata:
name: mypod
labels:
foo: bar
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
- maxSkew: 1
topologyKey: node
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
foo: bar
containers:
- name: pause
image: k8s.gcr.io/pause:3.1
在这种情况下,为了匹配第一个约束,新的 Pod 只能放置在 “zoneB” 中;而在第二个约束中, 新的 Pod 只能放置在 “node4” 上,最后两个约束的结果加在一起,唯一可行的选择是放置 在 “node4” 上。
多个约束之间是可能存在冲突的,假设有一个跨越 2 个区域的 3 节点集群:
如果对集群应用 two-constraints.yaml
,会发现 “mypod” 处于 Pending
状态,这是因为为了满足第一个约束,“mypod” 只能放在 “zoneB” 中,而第二个约束要求 “mypod” 只能放在 “node2” 上,Pod 调度无法满足这两种约束,所以就冲突了。
为了克服这种情况,你可以增加 maxSkew
或修改其中一个约束,让其使用 whenUnsatisfiable: ScheduleAnyway
。
与 NodeSelector/NodeAffinity 一起使用
仔细观察可能你会发现我们并没有类似于 topologyValues 的字段来限制 Pod 将被调度到哪些拓扑去,默认情况会搜索所有节点并按 topologyKey 对其进行分组。有时这可能不是理想的情况,比如假设有一个集群,其节点标记为 env=prod、env=staging和 env=qa,现在你想跨区域将 Pod 均匀地放置到 qa 环境中,是否可行?
答案是肯定的,我们可以结合 NodeSelector 或 NodeAffinity 一起使用,PodTopologySpread 会计算满足选择器的节点之间的传播约束。
如上图所示我们可以通过指定 spec.affinity.nodeAffinity 将搜索范围限制为 qa 环境,在该范围内 Pod 将被调度到一个满足 topologySpreadConstraints 的区域,这里就只能被调度到 zone=zone2 的节点上去了。
集群默认约束
除了为单个 Pod 设置拓扑分布约束,也可以为集群设置默认的拓扑分布约束,默认拓扑分布约束在且仅在以下条件满足 时才会应用到 Pod 上:
- Pod 没有在其
.spec.topologySpreadConstraints
设置任何约束; - Pod 隶属于某个服务、副本控制器、ReplicaSet 或 StatefulSet。
你可以在 调度方案(Schedulingg Profile)中将默认约束作为 PodTopologySpread 插件参数的一部分来进行设置。 约束的设置采用和前面 Pod 中的规范一致,只是 labelSelector 必须为空。配置的示例可能看起来像下面这个样子:
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
profiles:
- pluginConfig:
- name: PodTopologySpread
args:
defaultConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
defaultingType: List
课后习题
解决上节课留下的一个问题 - 如果想在每个节点(或指定的一些节点)上运行2个(或多个)Pod 副本,如何实现?
这里以我们的集群为例,加上 master 节点一共有3个节点,每个节点运行2个副本,总共就需要6个 Pod 副本,要在 master 节点上运行,则同样需要添加容忍,如果只想在一个节点上运行2个副本,则可以使用我们的拓扑分布约束来进行细粒度控制,对应的资源清单如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: topo-demo
spec:
replicas: 6
selector:
matchLabels:
app: topo
template:
metadata:
labels:
app: topo
spec:
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: ngpt
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: topo
这里我们重点需要关注的就是 topologySpreadConstraints
部分的配置,我们选择使用 kubernetes.io/hostname
为拓扑域,相当于就是3个节点都是独立的,maxSkew: 1 表示最大的分布不均匀度为1,所以只能出现的调度结果就是每个节点运行2个 Pod。
➜ kubectl get nodes
NAME STATUS ROLES AGE VERSION
master1 Ready control-plane,master 85d v1.22.2
node1 Ready <none> 85d v1.22.2
node2 Ready <none> 85d v1.22.2
➜ kubectl get pods -l app=topo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
topo-demo-6bbf65d967-7969w 1/1 Running 0 7m16s 10.244.2.40 node2 <none> <none>
topo-demo-6bbf65d967-8vhb8 1/1 Running 0 7m16s 10.244.2.41 node2 <none> <none>
topo-demo-6bbf65d967-cvg7j 1/1 Running 0 7m16s 10.244.1.211 node1 <none> <none>
topo-demo-6bbf65d967-hzhv2 1/1 Running 0 7m16s 10.244.0.143 master1 <none> <none>
topo-demo-6bbf65d967-nvg4z 1/1 Running 0 7m16s 10.244.0.144 master1 <none> <none>
topo-demo-6bbf65d967-w7w29 1/1 Running 0 7m16s 10.244.1.212 node1 <none> <none>
Descheduler
从 kube-scheduler 的角度来看,它是通过一系列算法计算出最佳节点运行 Pod,当出现新的 Pod 进行调度时,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定,但是 Kubernetes 集群是非常动态的,由于整个集群范围内的变化,比如一个节点为了维护,我们先执行了驱逐操作,这个节点上的所有 Pod 会被驱逐到其他节点去,但是当我们维护完成后,之前的 Pod 并不会自动回到该节点上来,因为 Pod 一旦被绑定了节点是不会触发重新调度的,由于这些变化,Kubernetes 集群在一段时间内就可能会出现不均衡的状态,所以需要均衡器来重新平衡集群。
当然我们可以去手动做一些集群的平衡,比如手动去删掉某些 Pod,触发重新调度就可以了,但是显然这是一个繁琐的过程,也不是解决问题的方式。为了解决实际运行中集群资源无法充分利用或浪费的问题,可以使用 descheduler 组件对集群的 Pod 进行调度优化,descheduler 可以根据一些规则和配置策略来帮助我们重新平衡集群状态,其核心原理是根据其策略配置找到可以被移除的 Pod 并驱逐它们,其本身并不会进行调度被驱逐的 Pod,而是依靠默认的调度器来实现,目前支持的策略有:
- RemoveDuplicates
- LowNodeUtilization
- RemovePodsViolatingInterPodAntiAffinity
- RemovePodsViolatingNodeAffinity
- RemovePodsViolatingNodeTaints
- RemovePodsViolatingTopologySpreadConstraint
- RemovePodsHavingTooManyRestarts
- PodLifeTime
这些策略都是可以启用或者禁用的,作为策略的一部分,也可以配置与策略相关的一些参数,默认情况下,所有策略都是启用的。另外,还有一些通用配置,如下:
- nodeSelector:限制要处理的节点
- evictLocalStoragePods: 驱逐使用 LocalStorage 的 Pods
- ignorePvcPods: 是否忽略配置 PVC 的 Pods,默认是 False
- maxNoOfPodsToEvictPerNode:节点允许的最大驱逐 Pods 数
我们可以通过如下所示的 DeschedulerPolicy 来配置:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
nodeSelector: prod=dev
evictLocalStoragePods: true
maxNoOfPodsToEvictPerNode: 40
ignorePvcPods: false
strategies: # 配置策略
...
descheduler 可以以 Job、CronJob 或者 Deployment 的形式运行在 k8s 集群内,同样我们可以使用 Helm Chart 来安装 descheduler:
➜ helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
通过 Helm Chart 我们可以配置 descheduler 以 CronJob 或者 Deployment 方式运行,默认情况下 descheduler 会以一个 critical pod 运行,以避免被自己或者 kubelet 驱逐了,需要确保集群中有 system-cluster-critical 这个 Priorityclass
具体应用:https://www.qikqiak.com/k3s/scheduler/descheduler/
服务质量
QoS 是 Quality of Service 的缩写,即服务质量,为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes 针对不同服务质量的预期,通过 QoS 来对 pod 进行服务质量管理,对于一个 pod 来说,服务质量体现在两个具体的指标:CPU 和内存。当节点上内存资源紧张时,Kubernetes 会根据预先设置的不同 QoS 类别进行相应处理。
对于 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 三类,优先级从高到低。
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:(如果一个容器只指明 limit 而未设定 requests,则 requests 的值等于 limit 值)
containers:
name: foo
resources:
limits:
cpu: 10m
memory: 1Gi
name: bar
resources:
limits:
cpu: 100m
memory: 100Mi
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 上时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对 memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值高的进程,可以使用 cat /proc/$PID/oom_score
命令查看进程的 OOMScore。OOMScore 的取值范围为 [-1000, 1000],Guaranteed 类型的 pod 的默认值为 -998,Burstable pod 的值为 2~999,BestEffort pod 的值为 1000,也就是说当系统 OOM 时,首先会 kill 掉 BestEffort pod 的进程,若系统依然处于 OOM 状态,然后才会 kill 掉 Burstable pod,最后是 Guaranteed pod。
Kubernetes 是通过 cgroup 给 pod 设置 QoS 级别的,kubelet 中有一个 --cgroups-per-qos
参数(默认启用),启用后 kubelet 会为不同 QoS 创建对应的 level cgroups,在 Qos level cgroups 下也会为 pod 下的容器创建对应的 level cgroups,从 Qos –> pod –> container,层层限制每个 level cgroups 的资源使用量。
mount | grep cgroup
命令查看 RootCgroup:
➜ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
在 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
直接创建上面的资源对象即可:
➜ kubectl apply -f qos-demo.yaml
➜ kubectl get pods qos-demo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
qos-demo 1/1 Running 0 2m49s 10.244.1.29 node1 <none> <none>
➜ kubectl get pods qos-demo -o yaml |grep uid
uid: 489a19f2-8d75-474c-988f-5854b61b839f
➜ kubectl get pods qos-demo -o yaml |grep qosClass
qosClass: Burstable
如果资源充足,可将 QoS pods 类型均设置为 Guaranteed。用计算资源换业务性能和稳定性,减少排查问题时间和成本。如果想更好的提高资源利用率,业务服务可以设置为 Guaranteed,而其他服务根据重要程度可分别设置为 Burstable 或 Best-Effort。