在k8s集群中kube-scheduler组件负责为Pod选择运行节点,并由对应节点上的kubelet创建Pod。对于每个未绑定至任何节点的Pod对象,无论是新建、被驱逐等,kube-scheduler都要使用调度算法从集群中挑选一个最佳节点来运行它。
kubs-schedulerd调度方式的发展主要分为两个阶段:在1.15版本之前使用经典调度器架构,1.15版本之后使用调度器框架(Scheduler Framework)
在经典调度架构中Pod的调度流程如下图所示,可以分为3个步骤:节点预选、节点优选和节点绑定。其中节点预选和节点优选是通过预选函数和优选函数完成。
k8s自1.1.5版本引入的调度框架重构了此前的经典调度器架构,它以插件化的方式在多个扩展点实现了调度器的绝大多数功能,替代了经典调度器中以预选函数(predicate)和优选函数(priority)为核心的调度载体。
如下图所示,调度框架将每次调度一个Pod的过程分为调度周期和绑定周期两个阶段,前者负责为Pod选择一个最佳运行节点,相当于之前的节点预选和优选;后者为完成Pod到节点的绑定执行必要的检测或初始化操作等。
调度器框架提供了多个扩展点,事实上其中的Filter相当于传统调度器上的Predicate(预选),Score相当于Priority(优选),Bind则保持原有的名称调度器插件可以根据自身的功能注册到一个或多个扩展点并由调度器进行调用。
在调度框架下,大部分的调度功能都以插件方式实现,便于扩展,还能让调度器核心程序保持简单且易于维护。因此,传统调度器中的节点预选、优选和绑定等相关的函数代码也都转而实现为新的调度框架下的插件。
Pod资源可以使用spec.nodeName直接指定要运行的目标节点,也可以基于spec.nodeSelector指定的标签选择器筛选符合条件的节点作为运行节点,最终选择则基于打分机制完成。nodeSeclctor也称为节点选择器,用户可以提前给节点打上不同的标签,然后通过节点选择器来选择想要运行Pod的节点,比如集群中的节点分属不同项目、节点不同硬件等情况可以使用节点选择器
如下图,目前集群中有3个node,下面分别演示一下nodeName和nodeSelector的效果
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
然后创建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
节点亲和是调度程序用来确定Pod对象调度位置的调度规则,这些规则基于节点上的自定义标签和Pod对象上指定的标签选择器进行定义。简单来说,节点亲和调度支持Pod资源定义自身对期望运行的某类节点的倾向性,倾向于运行的指定类型的节点即为亲和关系,否则即为反亲和关系
在Pod上定义节点亲和条件时有两种类型的亲和关系:强制(required)亲和首选(preferred)亲和,或者成为硬亲和和软亲和。强制亲和定义的规则在Pod调度时必选满足,无可用节点时Pod对象会被至于Pending状态,直到满足亲和条件的节点出现。首选亲和是非强制性的调度限制,它同样倾向于将Pod运行在符合亲和条件定义的节点上,但无法满足调度需求时,调度器会选择一个无法匹配规则的节点,而不是将Pod至于Pending状态
在Pod上定义亲和条件的关键点有两个:一是给节点规划并配置符合期望的标签;二是给Pod对象定义合理的标签选择器。需要注意的是,在Pod资源基于亲和条件调度到某节点之后,如果节点标签发生变动而不再符合Pod定义的亲和性规则时,调度器不会将Pod从此节点移出,因而亲和调度仅在调度执行的过程中进行一次即时的判断,而不是持续的监视亲和条件是否满足
虽然节点亲和和nodeSelector的目的都是控制Pod的调度结果,但是相对于nodeSelector,节点亲和的功能更加强大,具有以下优势:
Pod规范中的spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution用于定义Pod和节点的强制亲和关系,它可以嵌套使用nodeSelectorTerm字段。nodeSelectorTerms用于定义节点选择器,其值是一个对象列表,支持使用matchExpressions和matchFields两种表达机制
每个匹配条件下可以有一到多个匹配规则,例如一个matchExpressions条件下可以同时存在多个标签匹配规则,这些匹配规则之间是逻辑与关系。举例来说,如果一个nodeSelectorTerms下存在两个matchExpressions条件,只要满足其中一个即可,但满足指的是matchExpressions下的匹配规则要全部匹配成功
下面是一个强制亲和示例,它定义了Pod只能运行在具有project=project1 和ssd=true的节点上
此前已经为集群中的节点都打了project标签,现在再为其中一个属于projec1的节点打上ssd=true的标签
kubectl label node 192.168.122.21 ssd=true
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"]
通过kubectl descibe输出可以看到所有节点都不能满足node affinity规则所以调度失败
节点首选亲和为节点选择机制提供了一种柔性控制逻辑,被调度的Pod应该尽量放置在满足亲和条件的节点上,但亲和条件不满足时,该Pod也能接受被调度到其它不满足亲和条件的节点上。另外,多个软亲和条件并存时,还支持为亲和条件定义weight属性以区别它们的优先级,取值范围1-100,数字越大优先级越高,Pod越优先被调度到此节点上。
Pod规范中的spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution字段用于定义Pod和节点的首选亲和关系,它可以嵌套使用preference和weight字段。
下面是一个软亲和示例:
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类:
Pod在调度时会优先选择第一类节点,直到第一类节点资源不足时再使用第二类节点,以此类推。
创建之后查看Pod,如下图,3个pod都运行在192.168.122.22节点,它具有project=project标签;剩余一个Pod运行在192.168.122.21节点,它具有ssd=true标签
假如修改示例中的软亲和条件,将两个标签匹配器都修改为不存在的标签,然后删除重建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"]
下面是一个强制亲和&首选亲和的示例,定义了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标签
Pod的亲和与反亲和就能实现这类需求,它可以基于已经在node节点上运行的Pod来约束新创建的Pod可以调度到的目的节点。Pod的亲和/反亲和也都支持强制和首选两种形式
Pod亲和调度的目的在于确保相关的Pod对象运行在同一位置,而反亲和调度则要求它们不能运行在同一位置。如何判断节点是否处于同一位置,取决于通过节点上的哪个标签来判断。
假设集群中有4个节点,如下图所示:
假如以kubernetes.io/hostname标签来判断,同一位置表示同一个节点,不同的节点表示不同的位置;假如以Kubernetes.io/rack标签来判断,node1和node2属于同一位置,node3和node4属于同一位置
因此,定义Pod的亲和/反亲和关系时,需要先借助标签选择器来选择出要参照的Pod对象,而后根据筛选出的Pod对象所在节点的标签来判定同一位置所指,而后针对亲和关系将新创建的Pod放置在同一位置优先级最高的节点,或根据反亲和关系将新创建的Pod放置在不同位置优先级最高的节点
Pod间的亲和关系通过spec.affinity.podAffinity字段定义,反亲和关系通过spec.affinity.podAntiAffinity字段定义,它们都支持强制和首选两种约束关系,都支持使用如下字段:
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字段
下面是一个示例,它同样先定义一个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的强制反亲和定义在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