Kubernetes 的高级调度
原文:Advanced Scheduling in Kubernetes
Kubernetes 的调度器能够满足绝大多数要求,例如保证 Pod 只在资源足够的节点上运行,会尝试把同一个集合的 Pod 分散在不同的节点上,还会尝试平衡不同节点的资源使用率等。
不过有时候你希望控制 Pod 的调度。例如你希望确认某个 Pod 只运行在有特定硬件的节点上;或者想要让频繁互相通信的服务能就近部署;又或者你希望用独立的节点给部分用户提供服务。而且最终,用户总是比 Kubernetes 更了解自己的应用。
所以 Kubernetes 1.6 提供了四个高级调度功能:
- 节点亲和/互斥
- Taint(污染、变质) 和 Tolerations(容忍、耐受)
- Pod 的亲和/互斥
- 以及自定义调度
上述功能在 Kubernetes 1.6 中还属于 Beta 阶段。
节点亲和/互斥
节点的亲和和互斥是一种设置调度器选择节点的规则。这个规则是 nodeSelector(1.0 开始就有的功能)的衍生物。这一规则使用类似给 Node 添加自定义标签,在 Pod 中定义选择器的方式。规则在调度器中可以有必要和推荐两种级别。
必要的规则要求 Pod 必须调度到某指定节点上。如果没有符合条件(当然也包括通用的调度要求,例如节点必须有足够的资源)。如果没有符合要求的节点,Pod 就不会被调度,必要规则在 nodeAffinity
的 requiredDuringSchedulingIgnoredDuringExecution
字段中定义。
例如在一个 GCE 上的多区域 Kubernetes 集群中,我们希望把 Pod 运行在一个 us-central1-a
的区域中,我们可以在 Pod 中使用如下的亲和规则:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "failure-domain.beta.kubernetes.io/zone"
operator: In
values: ["us-central1-a"]
IgnoredDuringExecution
表示在 Pod 已经成功运行后,如果 Node 的标签发生了变化导致其不再符合 Pod 的调度要求,Pod 依然会继续运行;requiredDuringSchedulingRequiredDuringExecution
则相反,一旦出现这种变化,他会立即从 Node 上驱逐 Pod。
推荐级别的规则表示优先选择符合规则要求的节点,如果找不到,则降级选择普通节点。我们可以用优先规则代替必要规则,选择us-central1-a
进行 Pod 的运行,只要修改成preferredDuringSchedulingIgnoredDuringExecution
:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "failure-domain.beta.kubernetes.io/zone"
operator: In
values: ["us-central1-a"]
节点的互斥可以利用否定操作符来实现。所以如果让 Pod 避免运行在us-central1-a
,可以这样实现:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: "failure-domain.beta.kubernetes.io/zone"
operator: NotIn
values: ["us-central1-a"]
可用的操作符包括:
- In
- NotIn
- Exists
- DoesNotExist
- Gt
- Lt
需要这一功能的场景还包括节点的硬件结构、操作系统版本或者特殊硬件等。节点的亲和与互斥在 Kubernetes 1.6 之中处于 Beta 阶段。
Taint(污染、变质)和 Tolerations(容忍、耐受)
这俩名词让我非常挠头,不好下嘴。
另外这里的阐述比起 Kubectl help taint 来说,清晰程度差了太多。
这一功能让用户可以把一个节点标记为 taint 的话,除非 Pod 被标识为可以耐受污染节点,否则不会有任何 Pod 被调度到该节点上。之所以把 taint 标记到节点而不是像亲和性一样标记在 Pod 上,是因为在这种情况下,绝大多数的 Pod 都不应该部署到 Taint 的节点上。例如用户可能希望把主节点保留给 Kubernetes 系统组件使用,或者把一部分节点保留给一组用户,或者把一组具有特殊硬件的服务器保留给有需求的 Pod。
可以用 kubectl
命令对节点进行 taint 操作:
kubectl taint nodes node1 key=value:NoSchedule
在节点上创建了一个 tiant,一个 Pod 必须在 Spec 中做出这样的 Toleration 定义,才能调度到该节点:
tolerations:
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"
effect 除了 NoSchedule 这个值之外,还有一个 Prefer 版本的 PreferNoSchedule
,另外还有一个 NoExecute
选项,这个选项意味着这一 Taint 生效之时,如果该节点内正在运行的 Pod 没有对应的 Tolerate 设置,会被直接逐出。
目前这一特性在 Kubernetes 1.6 升级为 Beta,我们加入了一个 Alpha 特性,可以指定在节点遇到问题的时候,该节点之上的 Pod 可以保持该绑定的时间长度(缺省五分钟)。
Pod 的亲和与互斥
Node 的亲和与互斥特性允许用户通过对 Pod 的定义来选择运行的 Node。但是还有一种需求就是,Pod 的相互关系,例如对同一个服务里面的 Pod 进行分布或者集中,或者和其他服务的 Pod 如何相处?Pod 的亲和与互斥就应运而生,这一特性在 Kubernetes 1.6 中也处于 Beta 阶段。
看一个例子。假设有一个叫 S1 的前端服务,会经常和一个叫 S2 的后端服务进行通信(南北通信模式),所以我们希望这两个服务能够被安排在同一个云服务区域,但是我们也不想做手工选择——一旦某个区域出了问题,我们希望这些 Pod 能够再次迁移到同一个区域。这里就可以定义 Pod 亲和性来达成这一目的了(假设我们给两组服务都设置 Label,service=s1/s2):
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: service
operator: In
values: ["S1"]
topologyKey: failure-domain.beta.kubernetes.io/zone
和节点的亲和性类似,这里也有一个变量:preferredDuringSchedulingIgnoredDuringExecution
。
Pod 的亲和性弹性很大。设想在性能测试的过程中,发现两个服务的容器处于同一个节点时,S1 的容器会干扰 S2 的容器的性能,这可能会是由缓存或者网络的拥堵造成的。或者出于安全考虑,我们不想两个服务共享同一个节点。要实现这种互斥操作,只要稍微改动一下哎上面的代码:
podAffinity
改为podAntiAffinity
topologyKey
改为kubernetes.io/hostname
自定义调度
如果 Kubernetes 调度器的众多特性还没能满足你的控制欲,可以用自己独立运行的调度器来对指定的 Pod 进行调度。在 Kubernetes 1.6 中,多调度器特性也进入了 Beta 阶段。
一般情况下,每个新 Pod 都会由缺省调度器进行调度。但是如果 Pod 中提供了自定义的调度器名称,那么缺省调度器就会忽略该 Pod,转由指定的调度器把该 Pod 分配给节点。下面举例说明。
代码中的 Pod 指定了schedulerName
字段:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
schedulerName: my-scheduler
containers:
- name: nginx
image: nginx:1.10
如果我们在不部署自定义调度器的情况下,创建这个 Pod,缺省调度器会忽略这个 Pod,后果是他会在Pending
状态下停滞不前。所以我们需要为他创建一个schedulerName
值为my-scheduler
的调度器。
可以用任何语言来实现简单或复杂的调度器。下面的简单例子是用 Bash 实现的——随机指派一个节点。注意首先要运行kubectl proxy
来支持这一脚本的运行。
#!/bin/bash
SERVER='localhost:8001'
while true;
do
for PODNAME in $(kubectl --server $SERVER get pods -o json | jq '.items[] | select(.spec.schedulerName == "my-scheduler") | select(.spec.nodeName == null) | .metadata.name' | tr -d '"')
;
do
NODES=($(kubectl --server $SERVER get nodes -o json | jq '.items[].metadata.name' | tr -d '"'))
NUMNODES=${#NODES[@]}
CHOSEN=${NODES[$[ $RANDOM % $NUMNODES ]]}
curl --header "Content-Type:application/json" --request POST --data '{"apiVersion":"v1", "kind": "Binding", "metadata": {"name": "'$PODNAME'"}, "target": {"apiVersion": "v1", "kind"
: "Node", "name": "'$CHOSEN'"}}' http://$SERVER/api/v1/namespaces/default/pods/$PODNAME/binding/
echo "Assigned $PODNAME to $CHOSEN"
done
sleep 1
done
更多
Kubernetes 1.6 的 release notes 中提供了更多的这些特性的相关信息,其中尤其包括了如果已经使用了 Alpha 版本如何进行升级的问题。
鸣谢
文中描写的功能,包括 Alpha 和 Beta 阶段的功能,都是社区中来自 Google、华为、IBM 以及 Red Hat 等公司的工程师的努力成果。