# Kubernetes 的高级调度

原文：[Advanced Scheduling in Kubernetes
](https://kubernetes.io/blog/2017/03/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 中使用如下的亲和规则：

~~~yaml

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 `：

~~~yaml

affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: "failure-domain.beta.kubernetes.io/zone"
            operator: In
            values: ["us-central1-a"]

~~~

节点的互斥可以利用否定操作符来实现。所以如果让 Pod 避免运行在`us-central1-a`，可以这样实现：

~~~yaml

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 定义，才能调度到该节点：

~~~yaml

tolerations: 
- key: "key"
  operator: "Equal"
  value: "value"
  effect: "NoSchedule"

~~~

effect 除了 NoSchedule 这个值之外，还有一个 Prefer 版本的 `PreferNoSchedule`，另外还有一个 `NoExecute` 选项，这个选项意味着这一 Taint 生效之时，如果该节点内正在运行的 Pod 没有对应的 Tolerate 设置，会被直接逐出。

目前这一特性在 Kubernetes 1.6 升级为 Beta，我们加入了一个 Alpha 特性，可以指定在节点遇到问题的时候，该节点之上的 Pod 可以保持该绑定的时间长度（缺省五分钟）。

> 参考：
> https://kubernetes.io/docs/user-guide/node-selection/#per-pod-configurable-eviction-behavior-when-there-are-node-problems-alpha-feature

## Pod 的亲和与互斥

Node 的亲和与互斥特性允许用户通过对 Pod 的定义来选择运行的 Node。但是还有一种需求就是，Pod 的相互关系，例如对同一个服务里面的 Pod 进行分布或者集中，或者和其他服务的 Pod 如何相处？Pod 的亲和与互斥就应运而生，这一特性在 Kubernetes 1.6 中也处于 Beta 阶段。

看一个例子。假设有一个叫 S1 的前端服务，会经常和一个叫 S2 的后端服务进行通信（南北通信模式），所以我们希望这两个服务能够被安排在同一个云服务区域，但是我们也不想做手工选择——一旦某个区域出了问题，我们希望这些 Pod 能够再次迁移到同一个区域。这里就可以定义 Pod 亲和性来达成这一目的了（假设我们给两组服务都设置 Label，service=s1/s2）：

~~~yaml

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`字段：

~~~yaml

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`来支持这一脚本的运行。

~~~bash
#!/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 等公司的工程师的努力成果。
