Automated Placement 是 Kubernetes 中 scheduler 的核心功能,负责将新的 Pod 分配给合适的节点,满足容器的资源需求,同时遵守设定好的调度策略。
基于微服务的系统通常会包含数十个甚至数百个隔离的进程,容器和 Pod 为它们提供了很好的打包和部署机制,但并没有解决将众多的进程分配给适当的节点这项工作。
容器之间存在依赖关系,有些还需要关联到特定的节点,容器自身也有一定的资源需求。这些都会随着时间发生变化。同时集群本身的资源也不是恒定的,它会执行缩容或者扩容,其特定时刻下的容量也取决于已经放置的容器数量。
这些因素都会左右容器的调度。
Available Node Resources
首先需要考虑的就是节点上是否有足够的可用资源。Scheduler 会确保 Pod 申请的资源总和小于可分配节点上的可用容量。节点可用容量的计算公式:
Allocatable [capacity for application pods] =
Node Capacity [available capacity on a node]
- Kube-Reserved [Kubernetes daemons like kubelet, container runtime]
- System-Reserved [OS system daemons like sshd, udev]
Container Resource Demands
Pod 在调度时,另一个重要的考虑因素就是容器有着自己的运行时依赖和资源需求。
比如:
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
resources:
requests:
cpu: 100m
memory: 100Mi
limits:
cpu: 200m
memory: 200Mi
Placement Policies
Scheduler 配置了一组默认的优先级策略,适用于绝大多数场景。这个策略可以在 scheduler 启动时被替换掉。
scheduler 策略示例:
{
"kind" : "Policy",
"apiVersion" : "v1",
"predicates" : [
{"name" : "PodFitsHostPorts"},
{"name" : "PodFitsResources"},
{"name" : "NoDiskConflict"},
{"name" : "NoVolumeZoneConflict"},
{"name" : "MatchNodeSelector"},
{"name" : "HostName"}
],
"priorities" : [
{"name" : "LeastRequestedPriority", "weight" : 2},
{"name" : "BalancedResourceAllocation", "weight" : 1},
{"name" : "ServiceSpreadingPriority", "weight" : 2},
{"name" : "EqualPriority", "weight" : 1}
]
}
其中 Predicate 规则用于过滤掉不合格的节点。比如 PodFitsHostsPorts
关注特定的固定主机端口,只有在这些端口可用时对应的节点才会作为候选。
Priorities 用于根据一些偏好设置来对候选的节点进行排序。比如 LeastRequestedPriority
会赋予请求了较少资源的节点更高的优先级。
可以同时运行多个 scheduler,让 Pod 自己去指定使用哪一个。只需要在 Pod 的配置中添加一条 .spec.schedulerName
,其值为自定义 scheduler 的名字。
调度流程
只要 Pod 创建完成且还没有被分配给任何节点,scheduler 就会挑选出该 Pod,连同所有可用的节点及优先级策略。第一阶段借助过滤策略移除所有不满足要求的节点,剩余的节点在第二阶段有权重地排序。最后一个阶段得到最终的胜出节点。
在绝大多数情况下,最好都只让 scheduler 去做 Pod-to-Node 的分配工作,不要去尝试“微操”调度逻辑。
在某些特殊场景下,如果需要强制某个 Pod 只能分配给特定的一个或一组节点,可以借助 Pod 的 .spec.nodeSelector
字段。
该字段可以指定一些键值对,对应节点身上的标签。比如想要 Pod 运行在拥有 SSD 磁盘的硬件上:
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
nodeSelector:
disktype: ssd
除了通过自定义标签指定节点,还可以通过每个节点上都有的默认标签来筛选,比如 kubernetes.io/hostname
。
Node Affinity
Kubernetes 还支持更为灵活的配置调度流程的方式,比如 node affinity。其实它相当于 nodeSelector 机制的泛化,其规则可以被指定为“必需”或者“优先”。
“必需”表示相应的规则必须被满足,否则节点无法作为候选;“优先”则并不强制,只是提高匹配节点的权重。
此外,node affinity 支持多种操作符,如 In
、NotIn
、Exists
、DoesNotExist
、Gt
、Lt
等,从而获得更强的表达能力。
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: numberCores
operator: Gt
values: [ "3" ]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchFields:
- key: metadata.name
operator: NotIn
values: [ "master" ]
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
其中 requiredDuringSchedulingIgnoredDuringExecution
用来指定节点必须具备的条件,此规则不会在执行过程中重新计算。结合后面的 nodeSelectorTerms
配置,筛选出核心数大于 3 的节点。
preferredDuringSchedulingIgnoredDuringExecution
用于指定非必须的条件,表现为一个带有权重的 selector 列表。对于每一个节点,计算出所有匹配项的权重总和,结果最高的节点被选中,只要该节点已经满足了前面的“必需”条件。
PS:matchFields
只支持 In
和 NotIn
操作符,values
指定的列表中也只允许有一个值。
诚然,node affinity 相比于 nodeSelector
功能更为强大。它允许通过标签或者字段为 Pod 选择合适的节点,但不能够用来表达 Pod 之间的依赖关系,比如无法根据某个节点上已经部署的 Pod 判断某个新 Pod 是否也应该部署到该节点。这类需求可以通过 Pod affinity 实现。
Pod Affinity
Node affinity 工作在节点层级上,Pod affinity 则可以在多个拓扑层级上表达规则,达到粒度更细的控制。
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
confidential: high
topologyKey: security-zone
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
confidential: none
topologyKey: kubernetes.io/hostname
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
其中 podAffinity
部分的配置表示,符合条件的节点上必须有带有 confidential=high
标签的 Pod 在运行,且该节点有 security-zone
标签。
podAntiAffinity
定义的规则用于过滤掉匹配的节点。结合其配置,即节点上有带 confidential=none
标签的 Pod 在运行时,该节点不会用来部署当前 Pod。
Taints and Tolerations
Taints 和 Tolerations 是一类更高级的用于控制调度策略的特性。简单来说,node affinity 允许 Pod 根据规则选择合适的节点,taints 和 tolerations 则正相反,它允许节点自身去控制 Pod 是否应该分配给自己。
Taint 是节点自身的一种属性,当它存在时,会阻止 Pod 分配给自己,除非该 Pod 拥有针对 taint 的 tolerations。
Taint 可以使用 kubectl
命令添加。如 kubectl taint nodes master noderole.kubernetes.io/master="true":NoSchedule
,等效于下面的配置。
Tainted 节点:
apiVersion: v1
kind: Node
metadata:
name: master
spec:
taints:
- effect: NoSchedule
key: node-role.kubernetes.io/master
拥有此 taint 的节点不会被分配任何 Pod,除非有 Pod 指定了对应的 toleration。比如:
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
tolerations:
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
在生产级别的集群中,带有 noderole.kubernetes.io/master
配置的 taint 一般会指定给 master 节点,阻止 Pod 部署到 master 上。
这里给 Pod 添加的 toleration 会覆盖 taint 的 NoSchedule
效果,即无论如何都允许此 Pod 分配给 master 节点。
Taint 可以是硬性的,阻止节点作为候选(effect=NoSchedule
),也可以是软性的,尝试避免节点作为候选(effect=PreferNoSchedule
),还可以强制移除节点上已经在运行的 Pod(effect=NoExecute
)。
当 Pod 已经分配给某个节点,scheduler 的工作就已经算完成了,它不会再对完成的分配进行调整。除非该 Pod 被删除或者重建。随着时间的推移,这一定会导致资源的碎片化,集群利用率降低。
另一个潜在的问题是,Pod 被创建后具体分配给哪一个节点,依赖于当时集群的状态。而集群本身是动态的,节点的资源配置会更改,或者有新的节点加入进来,scheduler 并不会纠正已经存在的部署。此外,节点上的标签也有可能会变动,影响到之后的调度,但之前已经完成的调度依旧保持不变。
以上所有的场景都可以通过 descheduler 去解决。Kubernetes 的 descheduler 是一个可选的特性,通常作为 Job 执行,当管理员觉得是时候通过重新调度 Pod 来整理集群的碎片。
Descheduler 有一些预先定义的策略,可以被启用或者禁用:
- RemoveDuplicates:该策略会确保 ReplicaSet 或 Deployment 关联的单一 Pod 只运行在唯一一个节点上。当某个节点不健康时,controller 会在其他健康的节点上启动新的 Pod。此时若之前不健康的节点恢复正常重新加入集群,正在运行的 Pod 就会大于需要的数量。此策略就可以应用于这类的场景。同时 RemoveDuplicates 还可以在策略或集群架构发生变化后,将 Pod 更均匀地分散在更多的节点上
- LowNodeUtilization:该策略会找到使用率低的节点,并将高使用率节点上的 Pod 移除掉,希望这些移除的 Pod 可以重新分配到未充分利用的节点上。使用率低指 CPU、内存或 Pod 数量小于
thresholds
配置;使用率高指的是 CPU、内存或 Pod 数量大于targetThresholds
配置 - RemovePodsViolatingInterPodAntiAffinity:该策略会移除违反了 pod antiaffinity 规则的 Pod。这种情况可能发生在,添加规则时一些不符合规则的 Pod 就已经存在了
- RemovePodsViolatingNodeAffinity:移除违反了 node affinity 规则的 Pod
不管使用何种配置的策略,descheduler 会避免移除如下类型的 Pod:
- 在 annotation 中标记为
scheduler.alpha.kubernetes.io/criticalpod
的关键 Pod - 不由 ReplicaSet、Deployment 或 Job 管理的 Pod
- 由 DaemonSet 管理的 Pod
- 拥有本地存储的 Pod
- 配置了
PodDisruptionBudget
的 Pod,且移除时会违反此规则 - Descheduler Pod 本身
总结
容器调度是一个我们希望尽可能少干预的领域。从简单到复杂,以下方法控制着调度的具体策略:
- nodeName:最简单的分配方式,将 Pod 到节点的关系硬编码到配置中。理想的情况下,此字段应该由 scheduler 填充,策略去驱动,而不是手动指定
- nodeSelector:键值对映射。符合条件的节点必须包含此键值对指向的标签。在控制调度策略的可接受的方式中,最简单的一种
- Default scheduling alteration:必要的情况下,可以修改 default scheduler 的过滤规则和优先级策略、顺序、权重等
- Pod affinity 和 antiaffinity:此机制允许 Pod 表达自身对其他 Pod 的依赖关系
- Node affinity:允许 Pod 表达自身对节点的依赖关系,比如节点的硬件配置、地理位置等
- Taints 和 tolerations:允许节点去控制哪些 Pod 允许哪些不允许分配给自己。比如为一组 Pod 分配一个专用节点,甚至在运行时移除 Pod
- Custom scheduler:若上述方案都不能符合需求,还可以编写自定义的 scheduler。自定义 scheduler 可以替换掉标准的 Kubernetes scheduler,也可以两者一起运行