Kubernetes中的高级调度策略

在大多数业务场景下Kubernetes的默认调度策略都工作得很好,如 :在将一个Pod调度到节点之前,会首选确保改节点有足够的资源(如内存),并且它会试图平衡所有节点资源的使用率。

但是有时候我们会试图控制Pod的调度方式,比如:我们希望一些pod只会被调度到特定硬件的节点上,或者我们希望将通信比较频繁的service(被这些service选择到的pod)被调度到一个节点或者一个机房到节点上,又或者我们希望能指定一组节点提供给特定的用户的业务来使用。总结起来,也就是说我们希望我们能更清楚的知道应用在Kubernetes集群上到底是如何被调度或者部署的。所以从Kubernetes1.6开始,给我们提供了四种高级调度策略:node affinity/anti-affinity, taints and tolerations, pod affinity/anti-affinity, custom schedulers,这些特性在Kubernetes1.6中处于beta阶段。

Node Affinity/Anti-Affinity

Node Affinity/Anti-Affinity提供了一种方法,给一些节点设置一些规则,以确保这些节点被调度选中。这个特定是nodeSelector的一种泛化。给节点设置规则和给节点设置标签的方法是一模一样的,并且需要在pod中设置相应的选择器(类似nodeSelector),选择器的匹配规则可以是required或者preferred

required规则必须给严格匹配,才能将pod调度到该节点。如果没有被匹配(这里也会考虑到pod资源的使用量问题),则不会调度。在nodeAddinity中使用requiredDuringSchedulingIgnoredDuringExecution来定义requeired规则。

如,加入需要将pod调度到指定的节点上,在pod Spec中定义:

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
      affinity:
       nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
         nodeSelectorTerms:
          - matchExpressions:
            - key: "region"
              operator: In
              values: ["aws"]

IgnoredDuringExecution 表示如果该节点的标签被改变了,pod还是可以继续运行,并不会重新调度到其他节点去。未来会有一个新特性requiredDuringSchedulingRequiredDuringExecution,表示一旦节点到标签被修改,即pod的 node affinity无法再匹配的时候,pod会马上被驱逐出该节点,重新调度到匹配到节点上(如果有匹配到的节点的)。

preferred规则表示如果有可以匹配到的节点,则会优先将pod调度到该节点上,如果没有人和匹配到的节点,则会随机选择一个节点进行调度。

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx-deployment
  annotations:
    scheduler.alpha.kubernetes.io/affinity: >
      {
         "nodeAffinity": {
            "preferredDuringSchedulingIgnoredDuringExecution": [
               {
                 "weight" : 10,
                 "preference": {
                    "nodeSelectorTerms": [
                       {
                          "matchExpressions": [
                             {
                               "key": "region",
                               "operator": "In",
                               "values": ["aws"]
                             }
                          ]
                       }
                     ]
                 }
              }
           ]
         }
       }
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

Node anti-affinity还可以执行取反操作,如:

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
        - matchExpressions:
          - key: "region"
            operator: NotIn
            values: ["aws"]

可用的运算操作符有:In, NotIn, Exists, DoesNotExist. Gt, 和 Lt

Taints and Tolerations

taints and tolerations允许将某个节点做标记,以使得所有的pod都不会被调度到该节点上。但是如果某个pod明确制定了tolerates, 则可以正常调度到被标记的节点上。如,一般情况下我闷可以将master节点标记位不可以调度;或者将某些节点制定给特定的一组用户使用。

kubectl可以标记节点:

kubectl taint nodes node01 key=value:NoSchedule

所有的pod都不会调度到该节点上,除非pod里面制定了toleration,这里指定的effectNoSchedule,其他可用的effect有:PreferNoScheduleNoSchedulePrefer版本,NoExecute表示如果节点被taint,则运行在该节点上的所有pod的都会被驱逐出去,除非pod指定了相应的toerate。可以在PodSpec中指定tolerations

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

这个特性在kubernetes1.6处在beta阶段,在1.6版本中还有一个alpha的特性,使用taintstolerations指定pod在节点出现问题之后还可以绑定在该节点多长时间:

tolerations: 
- key: "node.alpha.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

(如果来匹配not ready的节点,则将key修改为node.alpha.kubernetes.io/notReady 即可).

Pod Affinity/Anti-Affinity

节点的affinity/anti-affinity允许pod基于标签选择调度到哪个节点上。但是,有时候我们希望可以通过已定的规则确定pod与pod之间的亲密关系,如:我们有一些 front-ends pod在service s1中,这些pod需要和后段的pod进行频繁的通信,后端的pod运行在另外一个service s2中。这个时候我们就希望两个service的pod会被调度到同一个region的节点上,但是我们要是手动指定了调度的节点region,当region的网络出问题的时候,这些service将不能正常运行,此时我们希望这些pod能被重新调度到其他region的节点上。我们可以定义一些pod之间亲和性的规则来实现这个目的,假如我们给s1的pod设置标签为service=s1,为s2的pod设置标签为service=s2:

affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: service
            operator: In
            values: [“s1”]
        topologyKey: failure-domain.beta.kubernetes.io/zone

同样也支持preferredDuringSchedulingIgnoredDuringExecution

我们也可以只一些标签规则,使两个service的pod不会调度到同一个节点上:

affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: service
            operator: In
            values: [“s1”]
        topologyKey: kubernetes.io/hostname

Custom Schedulers

如果kubernetes提供的多种调度策略均满足不了我们的业务需求,我们可以自定义调度器,实现pod的调度功能。每个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

下面是一个经过测试的Python自定义调度器:

#!/usr/bin/env python
import requests
import time
import json

SCHEDULER_NAME = "my-scheduler"
API_SERVER = "http://127.0.0.1:8080"
API_URL = {
    "pods": "/api/v1/pods",
    "nodes": "/api/v1/nodes",
    "binding": "/api/v1/namespaces/{0}/pods/{1}/binding"
}


def get_pods(url):

    pods = requests.get(url)
    pods_list = list()
    if pods.status_code == 200:
        pods_list = [{"name": x["metadata"]["name"], "namespace":x["metadata"]["namespace"]} for x in pods.json()["items"] if x["status"][
            "phase"] == "Pending" and x["spec"]["schedulerName"] == SCHEDULER_NAME]
    return pods_list


def get_nodes(url):
    nodes = requests.get(url)
    nodes_list = list()
    if nodes.status_code == 200:
        nodes_list = [x["metadata"]["name"] for x in nodes.json()["items"]]

    return nodes_list


def chose_node(nodes):
    '''scheduler'''
    chosen = None
    for node in nodes:
        if node.endswith("179"): #测试用,只是选择了ip以179结尾的节点
            chosen = node
            break
    return chosen


def main():
    pods_list = get_pods(API_SERVER + API_URL["pods"])
    nodes_list = get_nodes(API_SERVER + API_URL["nodes"])
    if pods_list == []:
        print "There is not pod need to be scheduled."
        return True

    for pod in pods_list:
        chosen = chose_node(nodes_list)
        if chosen == None:
            print "There is no node be chosen."
            return True

        data = {"apiVersion": "v1",
                "kind": "Binding",
                "metadata": {"name": pod["name"]},
                "target": {"apiVersion": "v1", "kind": "Node", "name": chosen}
                }
        bind = API_SERVER + \
            API_URL["binding"].format(pod["namespace"], pod["name"])
        headers = {"Content-type": "application/json",
                   "Accept": "application/json"}
        r = requests.post(bind, data=json.dumps(data), headers=headers)
        if r.status_code == 201:
            print "Assigned {0} to {1}.".format(pod["name"], chosen)
        else:
            print r.text
    time.sleep(3)

if __name__ == "__main__":

    while True:
        time.sleep(1)
        main()

下边这个是官方提供的shell版调度器:

 #!/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自带的默认调度器和一些高级的调度策略已经可以满足我们的需求了,但是同时kubernetes也提供了自定义调度器的功能,这使得我们可以实现一些符合特定业务需求的、经过特殊优化的调度器,kubernetes无论是直接使用,还是定制二次开发都提供了良好的支持或者接口。

3 Replies to “Kubernetes中的高级调度策略”

  1. 感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/3bdbaf 欢迎点赞支持!
    欢迎订阅《DevOps成长之路》https://toutiao.io/subject/65829

  2. 笔者可否介绍下java用fabric8调用api接口实现自定义调度绑定的功能啊?研究了很久没实现pod到node的绑定,希望笔者能给予帮助

发表评论

电子邮件地址不会被公开。 必填项已用*标注