k8s调度之亲和/反亲和

Pod的调度流程

在k8s集群中kube-scheduler组件负责为Pod选择运行节点,并由对应节点上的kubelet创建Pod。对于每个未绑定至任何节点的Pod对象,无论是新建、被驱逐等,kube-scheduler都要使用调度算法从集群中挑选一个最佳节点来运行它。

kubs-schedulerd调度方式的发展主要分为两个阶段:在1.15版本之前使用经典调度器架构,1.15版本之后使用调度器框架(Scheduler Framework)

在经典调度架构中Pod的调度流程如下图所示,可以分为3个步骤:节点预选、节点优选和节点绑定。其中节点预选和节点优选是通过预选函数和优选函数完成。
k8s调度之亲和/反亲和_第1张图片

  1. 节点预选:基于一系列预选规则对每个节点进行检查,将那些不符合筛选条件的节点过滤掉;没有节点满足Pod的资源需求时,该Pod被至于Pending状态,直到出现至少一个能满足条件的节点为止
  2. 节点优选:根据优选算法对预选出的节点进行打分,并根据最终得分进行优先级排序
  3. 节点选定:从优先级排序结果中挑选出优先级最高的节点运行Pod对象,最高优先级的节点数量多于一个时,从中随机选取一个运行Pod

k8s自1.1.5版本引入的调度框架重构了此前的经典调度器架构,它以插件化的方式在多个扩展点实现了调度器的绝大多数功能,替代了经典调度器中以预选函数(predicate)和优选函数(priority)为核心的调度载体。

如下图所示,调度框架将每次调度一个Pod的过程分为调度周期和绑定周期两个阶段,前者负责为Pod选择一个最佳运行节点,相当于之前的节点预选和优选;后者为完成Pod到节点的绑定执行必要的检测或初始化操作等。
k8s调度之亲和/反亲和_第2张图片
调度器框架提供了多个扩展点,事实上其中的Filter相当于传统调度器上的Predicate(预选),Score相当于Priority(优选),Bind则保持原有的名称调度器插件可以根据自身的功能注册到一个或多个扩展点并由调度器进行调用。

在调度框架下,大部分的调度功能都以插件方式实现,便于扩展,还能让调度器核心程序保持简单且易于维护。因此,传统调度器中的节点预选、优选和绑定等相关的函数代码也都转而实现为新的调度框架下的插件。

节点选择器

Pod资源可以使用spec.nodeName直接指定要运行的目标节点,也可以基于spec.nodeSelector指定的标签选择器筛选符合条件的节点作为运行节点,最终选择则基于打分机制完成。nodeSeclctor也称为节点选择器,用户可以提前给节点打上不同的标签,然后通过节点选择器来选择想要运行Pod的节点,比如集群中的节点分属不同项目、节点不同硬件等情况可以使用节点选择器

如下图,目前集群中有3个node,下面分别演示一下nodeName和nodeSelector的效果
k8s调度之亲和/反亲和_第3张图片

nodeName示例
创建2个Pod指定其运行在192.168.122.20这个node上,部署文件如下

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-with-nodeName
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pod-with-nodeName
  template:
    metadata:
      lables:
        app: pod-with-nodeName
    spec:
      nodeName: 192.168.122.20
      containers:
      - name: nginx
        image: nginx
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 80
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 256Mi

创建之后,查看Pod运行节点。如下图所示,两个Pod都被调度到了192.168.122.20这个node上
在这里插入图片描述

nodeSelector示例
假设集群中存在两个项目project1和project2,节点192.168.122.20和21属于project1,节点192.168.122.22属于project2,创建两个pod让其运行在project2拥有的节点上。

先为所有节点打上project标签

kubectl label node 192.168.122.20 project=project1
kubectl label node 192.168.122.21 project=project1
kubectl label node 192.168.122.22 project=project2

k8s调度之亲和/反亲和_第4张图片

然后创建pod,部署文件如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-with-nodeselector
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pod-with-nodeselector
  template:
    metadata:
      labels:
        app: pod-with-nodeselector
    spec:
      nodeSelector:
        project: project2
      containers:
      - name: nginx
        image: nginx
        imagePullPolicy: IfNotPresent
        ports:
        - name: http
          containerPort: 80
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 256Mi

创建之后,查看Pod运行节点,如下图,Pod都被调度到192.168.122.22这个node
在这里插入图片描述

node亲和

节点亲和是调度程序用来确定Pod对象调度位置的调度规则,这些规则基于节点上的自定义标签和Pod对象上指定的标签选择器进行定义。简单来说,节点亲和调度支持Pod资源定义自身对期望运行的某类节点的倾向性,倾向于运行的指定类型的节点即为亲和关系,否则即为反亲和关系

在Pod上定义节点亲和条件时有两种类型的亲和关系:强制(required)亲和首选(preferred)亲和,或者成为硬亲和和软亲和。强制亲和定义的规则在Pod调度时必选满足,无可用节点时Pod对象会被至于Pending状态,直到满足亲和条件的节点出现。首选亲和是非强制性的调度限制,它同样倾向于将Pod运行在符合亲和条件定义的节点上,但无法满足调度需求时,调度器会选择一个无法匹配规则的节点,而不是将Pod至于Pending状态

在Pod上定义亲和条件的关键点有两个:一是给节点规划并配置符合期望的标签;二是给Pod对象定义合理的标签选择器。需要注意的是,在Pod资源基于亲和条件调度到某节点之后,如果节点标签发生变动而不再符合Pod定义的亲和性规则时,调度器不会将Pod从此节点移出,因而亲和调度仅在调度执行的过程中进行一次即时的判断,而不是持续的监视亲和条件是否满足

虽然节点亲和和nodeSelector的目的都是控制Pod的调度结果,但是相对于nodeSelector,节点亲和的功能更加强大,具有以下优势:

  1. 节点亲和对目的标签的选择匹配不仅支持and,还支持In、NotIn、Exists、DoseNotExists、Gt(标签的值大于某个值)和Lt(标签的值小于某个值)
  2. 支持软亲和限制,在软亲和下,即使亲和条件不满足Pod也可以被调度到其它节点运行
node强制亲和

Pod规范中的spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution用于定义Pod和节点的强制亲和关系,它可以嵌套使用nodeSelectorTerm字段。nodeSelectorTerms用于定义节点选择器,其值是一个对象列表,支持使用matchExpressions和matchFields两种表达机制

  • matchExpressions:标签选择器表达式,根据节点标签值过滤节点;可以写多个matchExpressions以表示不同的匹配条件,它们之间为逻辑或关系
  • matchFields:以字段选择器表达的节点选择器,同样可以使用多个matchFields表示不同的匹配条件,它们之间为逻辑或关系

每个匹配条件下可以有一到多个匹配规则,例如一个matchExpressions条件下可以同时存在多个标签匹配规则,这些匹配规则之间是逻辑与关系。举例来说,如果一个nodeSelectorTerms下存在两个matchExpressions条件,只要满足其中一个即可,但满足指的是matchExpressions下的匹配规则要全部匹配成功

下面是一个强制亲和示例,它定义了Pod只能运行在具有project=project1 和ssd=true的节点上

此前已经为集群中的节点都打了project标签,现在再为其中一个属于projec1的节点打上ssd=true的标签

kubectl label node 192.168.122.21 ssd=true

k8s调度之亲和/反亲和_第5张图片

pod部署文件如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-with-nodeaffinity
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pod-with-nodeaffinity
  template:
    metadata:
      labels:
        app: pod-with-nodeaffinity
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 256Mi
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:	#此matchExpressions下的两个标签选择器都满足Pod才能调度成功
              - key: project
                operator: In
                values: ["project1"]
              - key: ssd
                operator: In
                values: ["true"]

查看Pod,如下图,它们都调度到了192.168.122.21这个节点,符合亲和条件约束
在这里插入图片描述

假如修改部署文件中亲和条件定义,将一个标签匹配规则改为不存在的标签,然后删除重建Pod,那么此时Pod将无法被调度成功,一直处于Pending状态。如下所示:

      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: project
                operator: In
                values: ["project1"]
              - key: gpu	#修改为匹配gpu=true的标签
                operator: In
                values: ["true"]

k8s调度之亲和/反亲和_第6张图片
通过kubectl descibe输出可以看到所有节点都不能满足node affinity规则所以调度失败
k8s调度之亲和/反亲和_第7张图片

node首选亲和

节点首选亲和为节点选择机制提供了一种柔性控制逻辑,被调度的Pod应该尽量放置在满足亲和条件的节点上,但亲和条件不满足时,该Pod也能接受被调度到其它不满足亲和条件的节点上。另外,多个软亲和条件并存时,还支持为亲和条件定义weight属性以区别它们的优先级,取值范围1-100,数字越大优先级越高,Pod越优先被调度到此节点上。

Pod规范中的spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution字段用于定义Pod和节点的首选亲和关系,它可以嵌套使用preference和weight字段。

  • weight:指定软亲和条件的优先级,取值范围1-100,数字越大优先级越高
  • preference:用于定义节点选择器,值是一个对象,支持matchExpressions和matchFields两种表达机制,它们的使用方式和逻辑与强制亲和一样

下面是一个软亲和示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-with-nodeaffinity-preferred
spec:
  replicas: 4
  selector:
    matchLabels:
      app: pod-with-nodeaffinity-preferred
  template:
    metadata:
      labels:
        app: pod-with-nodeaffinity-preferred
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 256Mi
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 60
            preference:
              matchExpressions:
              - key: project
                operator: In
                values: ["project2"]
          - weight: 30
            preference:
              matchExpressions:
              - key: ssd
                operator: In
                values: ["true"]

在上面的示例中,定义了两个软亲和条件,第一个用于选择具有project=project2标签的节点,优先级为60;第二个用于选择具有ssd=true标签的节点,优先级为30。此时可以将集群中的节点分为4类:

  1. 第一类,同时具有project=project2和ssd=true标签的节点,优先级最高为60+30=90
  2. 第二类,只具有project=project2标签的节点,优先级为60
  3. 第三类,只具有ssd=true标签的节点,优先级为30
  4. 第四类,不具有project=project2和ssd=true标签的节点,优先级为0

Pod在调度时会优先选择第一类节点,直到第一类节点资源不足时再使用第二类节点,以此类推。

创建之后查看Pod,如下图,3个pod都运行在192.168.122.22节点,它具有project=project标签;剩余一个Pod运行在192.168.122.21节点,它具有ssd=true标签
k8s调度之亲和/反亲和_第8张图片

假如修改示例中的软亲和条件,将两个标签匹配器都修改为不存在的标签,然后删除重建Pod,此时Pod也可以被调度运行,而不会被置于Pending状态,这就是和硬亲和的不同之处。如下所示:

      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 60
            preference:
              matchExpressions:
              - key: gpu
                operator: In
                values: [""]
          - weight: 30
            preference:
              matchExpressions:
              - key: disktype
                operator: In
                values: ["hdd"]

k8s调度之亲和/反亲和_第9张图片

综合示例

下面是一个强制亲和&首选亲和的示例,定义了Pod只能运行在具有project=project1标签的机器上,并且尽量运行在具有ssd=true标签的节点上

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-nodeaffinity-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pod-nodeaffinity-demo
  template:
    metadata:
      labels:
        app: pod-nodeaffinity-demo
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 200m
            memory: 256Mi
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:	
            nodeSelectorTerms:
            - matchExpressions:	#硬亲和条件1,Pod只能运行在属于project1的节点上
              - key: project
                operator: NotIn
                values: ["project2"]
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 50		#软亲和条件1,Pod尽量运行在具有ssd的节点上
            preference:		
              matchExpressions:
              - key: ssd
                operator: In
                values: ["true"]

创建之后查看Pod,如下图,3个Pod都运行在192.168.122.21节点上,它同时具有project=project1和ssd=true标签
k8s调度之亲和/反亲和_第10张图片

Pod亲和/反亲和

Pod的亲和与反亲和就能实现这类需求,它可以基于已经在node节点上运行的Pod来约束新创建的Pod可以调度到的目的节点。Pod的亲和/反亲和也都支持强制和首选两种形式

位置拓扑

Pod亲和调度的目的在于确保相关的Pod对象运行在同一位置,而反亲和调度则要求它们不能运行在同一位置。如何判断节点是否处于同一位置,取决于通过节点上的哪个标签来判断。

假设集群中有4个节点,如下图所示:
k8s调度之亲和/反亲和_第11张图片
假如以kubernetes.io/hostname标签来判断,同一位置表示同一个节点,不同的节点表示不同的位置;假如以Kubernetes.io/rack标签来判断,node1和node2属于同一位置,node3和node4属于同一位置

因此,定义Pod的亲和/反亲和关系时,需要先借助标签选择器来选择出要参照的Pod对象,而后根据筛选出的Pod对象所在节点的标签来判定同一位置所指,而后针对亲和关系将新创建的Pod放置在同一位置优先级最高的节点,或根据反亲和关系将新创建的Pod放置在不同位置优先级最高的节点

Pod间的亲和关系通过spec.affinity.podAffinity字段定义,反亲和关系通过spec.affinity.podAntiAffinity字段定义,它们都支持强制和首选两种约束关系,都支持使用如下字段:

  • topologyKey:拓扑键,用来判断节点是否处于同一拓扑位置的标签,在指定的键上具有相同值的节点属于同一拓扑位置
  • labelSelector:Pod标签选择器,用于指定当前调度的Pod应该参照哪类现有的Pod来确定运行位置
  • namespace:指定的labelSelector生效的名称空间,默认为当前调度Pod所属的名称空间,可用来跨名称空间筛选Pod
Pod亲和
强制亲和

Pod间的强制亲和关系定义在spec.affinity.podAffinity.requiredSchedulingIgnoredDuringExecution字段中,其值是一个对象列表,支持嵌套使用labelSelector、namespaces和topologyKey字段。

下面是一个示例,首先定义了一个mysql应用,然后定义了一个依赖mysql的tomcat应用,tomcat上定义了Pod强制亲和约束,期望与mysql运行在同一位置,以project作为拓扑键。也就是说tomcat Pod与mysql Pod要运行在具有project标签且标签值相同的节点上。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: harbor-server.linux.io/n70/mysql:5.7.39
        imagePullPolicy: IfNotPresent
        ports:
        - name: mysql
          containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: Passw0rd

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat-deploy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: tomcat-app1
  template:
    metadata:
      labels:
        app: tomcat-app1
    spec:
      containers:
      - name: tomcat
        image: harbor-server.linux.io/n70/tomcat-myapp:v1
      affinity:
        podAffinity:	#Pod亲和定义
          requiredDuringSchedulingIgnoredDuringExecution:	#pod强制亲和条件定义,多个列表项之间是逻辑与关系
          - labelSelector:	#Pod对象标签选择器,用于筛选放置当前Pod时要参考的Pod
              matchExpressions:
              - key: app
                operator: In
                values: ["mysql"]
            namespaces:		#指定名称空间,表示在哪些名称空间下筛选Pod
            - default
            topologyKey: project	#拓扑键,用于确定节点拓扑位置

创建之后,查看Pod运行位置,如下图,mysql和tomcat都运行在节点192.168.122.22上,符合亲和规则约束
在这里插入图片描述
如果Pod的强制亲和规则不满足,Pod也会被置于Pending状态,这和node强制亲和的行为是一样的

首选亲和

Pod间的首选亲和通过spec.affinity.podAffinity.preferredDuringSchedulingIgnoredDuringExecution字段中,其值是一个对象列表,支持嵌套使用weight和podAffinityTerm字段

  • weight:数字,取值范围1-100,定义软亲和条件的权重
  • podAffinityTerm:Pod标签选择器定义,可以嵌套使用labelSelector、namespaces和topologyKey字段

下面是一个示例,它同样先定义一个mysql应用,之后定义的tomcat应用定义了Pod首选亲和约束,tomcat Pod期望尽量与mysql Pod运行在同一节点,但当条件无法满足时,则期望运行在同一project的节点上。如果都无法满足,也能接受运行在集群其他节点上

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: harbor-server.linux.io/n70/mysql:5.7.39
        imagePullPolicy: IfNotPresent
        ports:
        - name: mysql
          containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: Passw0rd

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat-myapp-deploy
spec:
  replicas: 4
  selector:
    matchLabels:
      app: tomcat-myapp
  template:
    metadata:
      labels:
        app: tomcat-myapp
    spec:
      containers:
      - name: tomcat
        image: harbor-server.linux.io/n70/tomcat-myapp:v1
        resources:
          requests:
            cpu: 500m
            memory: 512Mi
          limits:
            cpu: 500m
            memory: 512Mi
      affinity:
        podAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:	#Pod软亲和定义
          - weight: 80		#软亲和条件1,权重为80
            podAffinityTerm:	#Pod标签选择器定义
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["mysql"]
              namespaces:
              - default
              topologyKey: kubernetes.io/hostname
          - weight: 40		#软亲和条件2,权重为40
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["mysql"]
              namespaces:
              - default
              topologyKey: project

创建之后查看Pod运行位置:
在这里插入图片描述
如上图,mysql运行在192.168.122.22节点,有两个tomcat Pod和mysql运行在同一节点,另外两个Pod由于192.168.122.22节点资源不足会转而选择带project=project2标签的节点,但是只有192.168.122.22节点属于project2(关于节点上的标签设置,可以查看前面node亲和部分),所以它们被调度到其他节点

Pod反亲和

Pod的反亲和关系要实现的调度目标与亲和关系相反,它需要确保存在互斥关系的Pod不会运行在同一位置,因此反亲和调度一般用于分散同一类应用的Pod对象等,也包括把不同安全级别的Pod调度到不同的区域。同样的,Pod的反亲和也支持强制和首选两种形式。

强制反亲和

Pod的强制反亲和定义在spec.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution字段中,可嵌套使用的字段和强制亲和定义完全一致。

下面是一个示例,定义了属于同一Deployment但彼此互斥的Pod对象,它们必须运行在不同的节点上

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fluentd
spec:
  replicas: 4
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      containers:
      - name: fluentd
        image: harbor-server.linux.io/n70/fluentd:v1.14-1
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values: ["fluentd"]
            namespaces:
            - default
            topologyKey: kubernetes.io/hostname

如下图,创建之后只有3个Pod被调度成功,剩余一个Pod处于Pending状态,因为集群中只有3个节点
在这里插入图片描述

首选反亲和

Pod的首选反亲和定义在spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution字段中,调度器尽量不会把互斥的Pod调度到同一位置,但约束条件无法满足时,也会将Pod放在同一位置,而不是将Pod至于Pending状态。

下面是一个示例,将上面的强制反亲和示例改为了首选反亲和:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fluentd
spec:
  replicas: 4
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      containers:
      - name: fluentd
        image: harbor-server.linux.io/n70/fluentd:v1.14-1
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 60
            podAffinityTerm:
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values: ["fluentd"]
              namespaces:
              - default
              topologyKey: kubernetes.io/hostname

如下图,首选反亲和下Pod也可以运行在同一节点,而不是被至于Pending状态:
在这里插入图片描述

你可能感兴趣的:(kubernetes,docker)