文章目录
- 01 概述
- 1.1 Pod调度控制器分类
- 1.2 RC到Deployment的发展
- 1.2.1 ReplicaSet
- 1.3 Pod调度
- 1.3.1 情景
- 1.3.2 存在的问题
- 1.3.3 解决方式
- 02 全自动调度
- 2.1 功能
- 2.2 举例
- 03 定向调度
- 3.1 Step1- 给Node打上标签
- 3.2 Step2- Pod指定NodeSelector
- 3.3 Step3- 验证
- 3.4 预定义的标签
- 04 node亲和性调度
- 4.1 亲和性调度分类
- 4.2 举例
- 4.3 注意事项
- 05 亲和性与互斥性调度
- 5.1 拓扑域
- 5.2 举例
- 5.2.1 参照目标pod
- 5.2.2 pod的亲和性调度
- 5.2.3 pod的互斥性调度
- 5.4 其它
- 06 污点与容忍
- 6.1.1 污点与容忍设置
- 6.1.1.1 Node设置污点
- 6.1.1.2 Pod声明容忍
- 6.1.1.3 小结
- 6.1.2 特殊情况
- 6.2 应用场景
- 6.2.1 独占节点
- 6.2.2 具有特殊硬件设备的节点
- 6.2.3 定义Pod驱逐行为,以应对节点故障
- 07 优先级调度
- 7.1 案例
- 7.1.1 创建PriorityClass
- 7.1.2 Pod声明优先级类别
- 7.1.3 注意事项
- 08 DaemonSet(每个node上只调度一个pod)
- 8.1 DaemonSet
- 8.1.1 应用场景
- 8.1.2 举例
- 8.1.3 注意事项
- 09 批处理调度
- 9.1 批处理调度
- 9.1.1 任务模式分类
- 9.1.1.1 按实现方式分类
- 9.1.1.2 按批处理并行分类
- 9.1.2 案例
- 9.1.2.1 Job Template Expansion案例
- 9.1.2.2 Queue with Pod Per Work Item案例
- 9.1.2.3 Queue with Variable Pod Count案例
- 10 定时任务
- 10.1 基本语法
- 10.2 案例
- 11 容灾调度
- 11.1 如何实现?
- 11.2 举例
01 概述
1.1 Pod调度控制器分类
在Kubernetes平台上,我们很少会直接创建一个Pod
,在大多数情况下会通过如下控制器完成对一组Pod
副本的创建、调度 及全生命周期的自动控制任务:
- RC
- Deployment
- DaemonSet
- Job等
1.2 RC到Deployment的发展
在最早的Kubernetes,版本里是没有这么多Pod
副本控制器的,只有一个Pod
副本控制器RC(Replication Controller),这个控制器是这样设计实现的:
RC独立于所控制的Pod,并通过Label标签这个松耦合关联关系控制目标Pod实例的创建和销毁。
随着Kubernetes的发展,RC
也出现了新的继任者-Deployment
,用于更加自动地完成Pod副本的部署、版本更新、回滚等功能。
严谨地说,RC
的继任者其实并不是Deployment
,而是ReplicaSet,因为
ReplicaSet进一步增强了RC
标签选择器的灵活性(之前RC的标签选择器只能选择一个标签而ReplicaSet拥有集合式的标签选择器,可以选择多个Pod标签),如下所示:
1.2.1 ReplicaSet
与RC
不同,ReplicaSet被设计成能控制多个不同标签的Pod副本
举例: 应用MyApp
目前发布了v1
与v2
两个版本,用户希望MyApp
的Pod
副本数保持为3个,可以同时包含v1
和v2
版本的Pod
,就可以用ReplicaSet来实现这种控制,写法如下:
其实,Kubernetes
的滚动升级就是巧妙运用ReplicaSet
的这个特性来实现的, 同时,Deployment
也是通过ReplicaSet
来实现Pod
副本自动控制功能的。
我们不应该直接使用底层的ReplicaSet来控制Pod副本,而应该通过管理ReplicaSet的Deployment对象来控制副本,这是来自官方的建议。
1.3 Pod调度
在大多数情况下,我们希望Deployment
创建的Pod
副本被成功调度到集群中的任何一个可用节点,而不关心具体会调度到哪个节点。
1.3.1 情景
但是,在真实的生产环境中的确也存在一种需求:希望某种Pod
的副本全部在指定的一个或者一些节点上运行,比如希望将MySQL
数据库调度到一个具有SSD
磁盘的目标节点上。
此时Pod
模板中的NodeSelector属性就开始发挥作用了,上述MySQL定向调度案例的实现方式可分为以下两步:
- 把具有SSD磁盘的Node都打上自定义标签disk=ssd
- 在Pod模板中设定NodeSelector的值为“disk:ssd”
如此一来,Kubernetes
在调度Pod
副本的时候,就会先按照Node
的标签过滤出合适的目标节点,然后选择一个最佳节点进行调度。
1.3.2 存在的问题
上述逻辑看起来既简单又完美,但在真实的生产环境中可能面临以下令人尴尬的问题:
- 如果
NodeSelector
选择的Label
不存在或者不符合条件(比如:这些目标节点此时宕机或者资源不足,该怎么办?) - 如果要选择多种合适的目标节点,比如
SSD
磁盘的节点或者超高速硬盘的节点,该怎么办?
备注:Kubernetes
引入了NodeAffinity
(节点亲和性设置)来解决该需求。
1.3.3 解决方式
在真实的生产环境中还存在如下所述的特殊需求:
需求 | 举例描述 | 解决方式 |
不同 | 比如MySQL数据库与Redis中间件 不能被调度到同一个目标节点上,或者两种不同的Pod必须被调度到同一个Node 上,以实现本地文件共享或本地网络通信等特殊需求 | PodAffinity来解决该问题 |
有状态集群的调度 | 对于ZooKeeper、Elasticsearch、MongoDB、Kafka等有状态集群,虽然集群中的每个Worker节点看起来都是相同的,但每个Worker节点都必须有明确的、不变的唯一ID(主机名或IP地址),这些节点的启动和停止次序通常有严格的顺序。此外,由于集群需要持久化保存状态数据所以集群中的Worker节点对应的Pod不管在哪个Node上恢复,都需要挂载原来的Volume,因此这些Pod还需要捆绑具体的PV | 针对这种复杂的需求, |
在每个Node上调度并且仅仅创建一个Pod副本 | 这种调度通常用于系统监控相关的Pod,比如主机上的日志采集、主机性能采集等进程需要被部署到集群中的每个节点,并且只能部署一个副本 | DaemonSet来解决这种特殊Pod副本控制器 |
对于批处理作业,需要创建多个Pod副本来协同工作,当这些Pod副本都完成自己的任务时,整个批处理作业就结束了 | 这种Pod运行且仅运行一次的特殊调度,用常规的RC或者Deployment都无法解决 | 引入了新的Pod调度控制器Job来解决问题,并继续延伸了定时作业的调度控制器CronJob |
与单独的Pod实例不同,由RC、ReplicaSet、Deployment、DaemonSet等控制器创建的Pod副本实例都是归属于这些控制器的,这就产生了一个问题:控制器被删除后,归属于控制器的Pod副本该何去何从?
kubernetes版本 | 操作 |
1.9之前 | 在RC等对象被删除后,它们所创建的Pod副本都不会被删除 |
1.9以后 | 这些Pod副本会被一并删除。如果不希望这样做,则可以通过kubectl命令的- cascade=false参数(例如: |
02 全自动调度
2.1 功能
Deployment或RC的主要功能之一就是自动部署一个容器应用的多份副本,以及持续监控副本的数量,在集群内始终维持用户指定的副本数量,。
2.2 举例
举例:使用配置文件可以创建一个ReplicaSet
,这个ReplicaSet
会创建3个Nginx
应用的Pod
:
使用create
命令创建之后,查看Deployment
的状态:
kubectl get deployments
该状态说明
Deployment
已创建好所有3个副本,并且所有副本都是最新的可用的。
通过运行kubectl get rs
和kubectl get pods
可以查看已创建的ReplicaSet (RS)
和Pod
的信息。
从调度策略上来说,这3个Nginx Pod
由系统全自动完成调度。它们各自最终运行在哪个节点上,完全由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。
03 定向调度
3.1 Step1- 给Node打上标签
如果要实现定向调度,首先的第一步就是要为Node节点搭上标签(Label),可以使用kubectl label
命令:
kubectl label nodes <node-name><label-key>=<label-value>
例如这里为k8s-node-1
节点打上一个zone=north
标签,表明它是“北方”的一个节点:
3.2 Step2- Pod指定NodeSelector
然后,在Pod
的定义中加上nodeSelector
的设置,以redis-master- controller.yaml
为例:
3.3 Step3- 验证
运行kubectl create -f
命令创建Pod
,scheduler
就会将该Pod
调度到拥有 “zone=north” 标签的Node
上。
使用kubectl get pods-o wide
命令可以验证Pod
所在的Node
:
需要注意的是,如果我们指定了Pod
的nodeSelector
条件,且在集群中不存在包含相应标签的Node
,则即使在集群中还有其他可供使用的Node
,这个Pod也无法被成功调度。
3.4 预定义的标签
除了用户可以自行给Node
添加标签,Kubernetes
也会给Node
预定义一些标签,包括:
- kubernetes.io/hostname;
- beta.kubernetes.io/os(从1.14版本开始更新为稳定版,到1.18版本删除);
- beta.kubernetes.io/arch(从1.14版本开始更新为稳定版,到1.18版本删除);
- kubernetes.io/os(从1.14版本开始启用);
- kubernetes.io/arch(从1.14版本开始启用)。
04 node亲和性调度
4.1 亲和性调度分类
目前有两种节点亲和性表达:
表达式 | 含义 |
| 必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于限制 |
| 强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制 |
多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序。
IgnoredDuringExecution的意思是:如果一个Pod
所在的节点在Pod
运行期间 标签发生了变更,不再符合该Pod
的节点亲和性需求,则系统将忽略Node
上Label
的变化,该Pod
能继续在该节点上运行。
4.2 举例
有如下要求:
- requiredDuringSchedulingIgnoredDuringExecution:要求只运行在amd64的节点上(beta.kubernetes.io/arch In amd64);
- preferredDuringSchedulingIgnoredDuringExecution:要求尽量运行在磁盘类型为ssd(disk-type In ssd)的节点上;
则资源文件的定义如下:
apiVersion:vl
kind:Pod
metadata:
name:with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms
- matchExpressions:
- key:beta.kubernetes.io/arch
operator:In
values:
- amd64
preferredDuringSchedulingIgnoredDuringExecution:
- weight:1
preference:
matchExpressions:
- key:disk-type
operator:In
values:
- ssd
containers:
- name:with-node-affinity
image:gcr.io/google containers/pause:2.0
从上面的配置中可以看到In操作符,NodeAffinity
语法支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt
。虽然没有节点排斥功能,但是用NotIn
和DoesNotExist
就可以实现排斥的功能了。
4.3 注意事项
- 如果同时定义了
nodeSelector
和nodeAffinity
,那么必须两个条件都得到满足,Pod
才能最终运行在指定的Node上; - 如果
nodeAffinity
指定了多个nodeSelectorTerms
,那么其中一个能匹配成功即可; - 如果在
nodeSelectorTerms
中有多个matchExpressions
,则一个节点必须满足所有matchExpressions
才能运行该Pod。
05 亲和性与互斥性调度
亲和性与互斥性可以理解为就是相关联的两种或多种Pod是否可以在同一个拓扑域中共存或者互斥。
那么什么是拓扑域?
5.1 拓扑域
拓扑域的概念:
- 一个拓扑域由一些
Node
节点组成,这些Node
节点通常有相同的地理空间坐标,比如在同一个机架、机房或地区; - 一般用
region
表示机架、 机房等的拓扑区域,用Zone
表示地区这样跨度更大的拓扑区域; - 极端情况下, 我们也可以认为一个Node就是一个拓扑区域。
k8s
内置了如下一些常用的默认拓扑域,主要是为了确定各个节点所属的拓扑域:
默认拓扑域 | 描述 |
kubernetes.io/hostname | 在Node节点初始化时,controller–manager会为Node打上该标签 |
topology.kubernetes.io/region | 公有云厂商提供的Kubernetes服务或者使用cloud-controller-manager创建的集群,会给Node打上该标签 |
topology.kubernetes.io/zone | 同上 |
5.2 举例
Pod
亲和与互斥的调度是通过在Pod
的定义上增加topologyKey 属性来声明对应的目标拓扑区域内几种相关联的Pod
要 “在一起或不在一起”。
与节点亲和相同,Pod亲和与互斥的条件设置也是requiredDuringSchedulingIgnoredDuringExecution
和
preferredDuringSchedulingIgnoredDuringExecution
:
- Pod的亲和性被定义于PodSpec的affinity字段的podAffinity子字段中;
- Pod间的互斥性则被定义于同一层次的podAntiAffinity子字段中.
下面通过实例来说明Pod间的亲和性和互斥性策略设置。
5.2.1 参照目标pod
首先,创建一个名为pod-flag
的Pod
,带有标签security=S1
和app=nginx
,后面的例子将使用pod-flag
作为Pod
亲和与互斥的目标Pod
:
apiversion:v1
kind:Pod
metadata:
name:pod-flag
labels:
security:"S1"
app:"nginx"
spec:
containers:
-name:nginx
image:nginx
5.2.2 pod的亲和性调度
下面创建第2个Pod来说明Pod的亲和性调度,这里定义的亲和标签是 “security=S1”,对应上面的Pod “pod-flag”,topologyKey的值被设置为 “kubernetes.io/hostname“:
apiVersion:vl
kind:Pod
metadata:
name:pod-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key:security
operator:In
values:
-S1
topologyKey:kubernetes.io/hostname
containers:
- name:with-pod-affinity
image:gcr.io/google_containers/pause:2.0
创建Pod
之后,使用kubectl get pods -o wide
命令可以看到,这两个Pod
在同
一个Node
上运行。
在创建这个
Pod
之前,删掉这个节点的 kubernetes.io/hostname
标签,重复上面的创建步骤,将会发现Pod
一直处于Pending
状态,这是因为找不到满足条件的Node
了。
5.2.3 pod的互斥性调度
创建第3个Pod
,我们希望它不与目标Pod
运行在同一个Node
上:
apiversion:v1
kind:Pod
metadata:
name:anti-affinity
spec:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key:security
operator:In
values:
-S1
topologyKey:topology.kubernetes.io/zone
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key:app
operator:In
values:
-nginx
topologyKey:kubernetes.io/hostname
containers:
- name:anti-affinity
image:gcr.io/google_containers/pause:2.0
这里要求这个新Pod
与security=S1
的Pod为同一个zone
,但是不与app=nginx
的Pod
为同一个Node
。
创建Pod
之后,同样用kubectl get pods -o wide
来查看,会看到新的Pod
被调度到了同一Zone
内的不同Node
上。
5.4 其它
与节点亲和性类似,Pod
亲和性的操作符也包括In、NotIn、Exists、 DoesNotExist、Gt、Lt
。
原则上,topologyKey
可以使用任意合法的标签Key
赋值,但是出于性能和安全方面的考虑,对topologyKey
有如下限制:
- 在Pod亲和性和RequiredDuringScheduling的Pod互斥性的定义中,不允许使用空的topologyKey;
- 如果Admission controller包含了LimitPodHardAntiAffinityTopology,那么针对Required DuringScheduling的Pod互斥性定义就被限制为kubernetes.io/hostname,要使用自定义的topologyKey,就要改写或禁用该控制器。
- 在PreferredDuringScheduling类型的Pod互斥性定义中,空的
topologyKey会被解释为kubernetes.io/hostname、failure-
domain.beta.kubernetes.io/zone 及 failure-domain.beta.kubernetes.io/region的组合。 - 如果不是上述情况,就可以采用任意合法的topologyKey了。
PodAffinity规则设置的注意事项如下:
- 除了设置Label Selector和topologyKey,用户还可以指定Namespace列表
进行限制。同样,使用Label Selector对Namespace进行选择,Namespace的定义 和Label Selector及topologyKey同级,省略Namespace的设置,表示使用定义了 affinity/anti-affinity的Pod所在的命名空间。如果Namespace被设置为空值 (“”),则表示所有命名空间. - 在所有关联requiredDuringSchedulingIgnoredDuringExecution的
matchExpressions 全都满足之后 ,系统才能将Pod调度到某个Node上。
06 污点与容忍
Taint(污点) 则正好相反,它让Node
拒绝Pod
的运行。简单地说,被标记为Taint
的节点就是存在问题的节点,比 如磁盘要满、资源不足、存在安全隐患要进行升级维护,希望新的Pod
不会被调度过来。
但被标记为Taint
的节点并非故障节点,仍是有效的工作节点,所以仍需将某些Pod
调度到这些节点上时,可以通过使用Toleration
属性来实现。
6.1.1 污点与容忍设置
在默认情况下,在Node
上设置一个或多个Taint
之后,除非Pod
明确声明能够容忍这些污点,否则无法在这些Node
上运行。
6.1.1.1 Node设置污点
可以用kubectl taint
命令为Node
设置Taint
信息:
kubectl taint nodes node1 key=value:NoSchedule
描述:这个设置为node1
加上了一个Taint
,该Taint
的键为key
,值为value
,Taint
的效果是NoSchedule
,这意味着除非Pod
明确声明可以容忍这个Taint
,否则不会被调度到node1
上。
6.1.1.2 Pod声明容忍
在Pod
上声明容忍的例子如下,下面的两个Toleration
都被设置为可以容忍(Tolerate
)具有该Taint
的Node
,使得Pod
能够被调度到node1
上:
tolerations:
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"
或者
tolerations:
- key: "key"
operator: "Exists"
effect: "NoSchedule"
6.1.1.3 小结
Pod的Toleration声明中的key
和effect
需要与Taint
的设置保持一致,并且满足以下条件之一:
值 | 条件 |
key | 空的 |
operator | 值是 |
effect | 空的 |
系统允许在同一个Node上设置多个Taint,也可以在Pod上设置多个Toleration。
Kubernetes调度器处理多个Taint和Toleration的逻辑顺序为:首先列出节点中所有的Taint,然后忽略Pod的Toleration能够匹配的部分,剩下的没被忽略的Taint就是对Pod的效果了。
6.1.2 特殊情况
下面是几种特殊情况:
- 如果在剩余的
Taint
中存在effect=NoSchedule
,则调度器不会把该Pod
调度到这一节点上; - 如果在剩余的
Taint
中没有NoSchedule
效果,但是有PreferNoSchedule
效果,则调度器会尝试不把这个Pod
指派给这个节点; - 如果在剩余的
Taint
中有NoExecute
效果,并且这个Pod
已经在该节点上运行,则会被驱逐; - 如果没有在该节点上运行,则也不会再被调度到该节点上。
例如,我们这样对一个节点进行Taint设置:
kubectl taint nodes node1 keyl=valuel:NoSchedule
kubectl taint nodes node1 keyl=valuel:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule
然后在Pod
上设置两个Toleration
:
tolerations:
- key: "key1"
operator: "Equal"
value: "valuel"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "valuel"
effect: "NoExecute"
结果:
- 这样的结果是该
Pod
无法被调度到node1
上,这是因为第3个Taint
没有匹配的Toleration
。 - 但是如果该
Pod
已经在node1
上运行了,那么在运行时设置第3个Taint
,它还能继续在node1
上运行,这是因为Pod
可以容忍前两个Taint。
一般来说,如果给Node
加上effect=NoExecute
的Taint
,那么在该Node
上正在运行的所有无对应Toleration
的Pod
都会被立刻驱逐,而具有相应Toleration
的Pod
永远不会被驱逐。不过,系统允许给具有NoExecute
效果的Toleration
加入一 个可选tolerationSeconds
“字段,这个设置表明Pod
可以在Taint
添加到Node
之后还能在这个Node
上运行多久(单位为s
):
tolerations:
- key: "key1"
operator: "Equal"
value: "valuel"
effect: "NoExecute"
tolerationSeconds: 3600
上述定义的意思是,如果Pod
正在运行,所在节点都被加入一个匹配的Taint
,则这个Pod
会持续在这个节点上存活3600s
后被逐出。如果在这个宽限期内Taint
被移除,则不会触发驱逐事件。
6.2 应用场景
Taint和Toleration是一种处理节点并且让Pod进行规避或者驱逐Pod的弹性处理方式,下面列举一些常见的用例。
6.2.1 独占节点
如果想要拿出一部分节点专门给一些特定应用使用,则可以为节点添加这样Taint
:
kubectl taint nodes nodename dedicated=groupName:NoSchedule
然后给这些应用的Pod
加入对应的Toleration
,这样,带有合适Toleration
的Pod
就会被允许同使用其他节点一样使用有Taint
的节点。
通过自定义Admission Controller
也可以实现这一目标。如果希望让这些应用独占一批节点,并且确保它们只能使用这些节点,则还可以给这些Taint
节点加入类似的标签dedicated=groupName
,然后Admission Controller
需要加入节点亲和 性设置,要求Pod
只会被调度到具有这一标签的节点上。
6.2.2 具有特殊硬件设备的节点
在集群里可能有一小部分节点安装了特殊的硬件设备(如GPU
芯片),用户自然会希望把不需要占用这类硬件的Pod
排除在外,以确保对这类硬件有需求的Pod
能够被顺利调度到这些节点上。
可以用下面的命令为节点设置Taint
:
kubectl taint nodes nodename special=true:NoSchedule
kubectl taint nodes nodename special=true:PreferNoSchedule
然后在Pod
中利用对应的Toleration
来保障特定的Pod
能够使用特定的硬件。
和上面独占节点的示例类似,使用Admission Controller
来完成这一任务会更方便,例如:
- Admission Controller使用Pod的一些特征来判断这些Pod,如果可以使用这些件,就添加Toleration来完成这一工作;
- 要保障需要使用特殊硬件的Pod只被调度到安装这些硬件的节点上,则还需要一些额外的工作,比如将这些特殊资源使用opaque-int-resource的方式对自定义资源进行量化,然后在PodSpec中进行请求;
- 也可以使用标签的方式来标注这些安装有特别硬件的节点,然后在Pod 中定义节点亲和性来实现这个目标。
6.2.3 定义Pod驱逐行为,以应对节点故障
前面提到的NoExecute
这个Taint
效果对节点上正在运行的Pod
有以下影响:
- 没有设置Toleration的Pod会被立刻驱逐;
- 配置了对应Toleration的Pod,如果没有为tolerationSeconds赋值,则会一直留在这一节点中;
- 配置了对应Toleration的Pod且指定了tolerationSeconds值,则会在指定的时间后驱逐(注意,在节点发生故障的情况下,系统将会以限速(rte- limiting)模式逐步给Node设置Taint,这样就能避免在一些特定情况下(比如
Master暂时失联)有大量的Pod被驱逐)。
注意,Kubernetes会自动给Pod添加下面几种Toleration:
- key为node.kubernetes.io/not-ready,并配置tolerationSeconds=300;
- key 为node.kubernetes.io/unreachable,并配置tolerationSeconds=300。
以上添加的这种自动机制保证了在某些节点发生一些临时性问题时,Pod默认能够继续停留在当前节点运行5min等待节点恢复,而不是立即被驱逐,从而避免系统的异常波动。
另外,Kubernetes从1.6版本开始引入两个与Taint相关的新特性,TaintNodesByCondition及TaintBasedEvictions,用来改善异常情况下的Pod调度与驱逐问题,比如在节点内存吃紧、节点磁盘空间已满、节点失联等情况下,是 否自动驱逐某些Pod或者暂时保留这些Pod等待节点恢复正常。这个过程的完整逻 辑基本如下。
- 不断地检查所有Node状态,设置对应的Condition;
- 不断地根据Node Condition设置对应的Taint;
- 不断地根据Taint驱逐Node上的Pod。
其中,检查Node
的状态并设置Node
的Taint
就是TaintNodesByCondition
特性,即在Node满足某些特定的条件时,自动为Node节点添加Taint,目前主要有以下几种条件:
条件 | 描述 |
node.kubernetes.io/not-ready:节点未就绪 | 对应NodeCondition Ready为False的情况 |
node.kubernetes.io/unreachable:节点不可触达 | 对应NodeCondition Ready.为Unknown的情况 |
node.kubernetes.io/out-of-disk | 节点磁盘空间已满 |
node.kubernetes.io/network-unavailable | 节点网络不可用 |
node.kubernetes.io/unschedulable | 节点不可调度 |
node.cloudprovider,kubernetes.io/uninitialized | 如果kubelet是由"外部"云服务商启动的,则该污点用来标识某个节点当前为不可用状态。在云控制器 (cloud-controller-manager)初始化这个节点以后,kubelet会将此污点移除 |
自Kubernetes 1.13开始,上述两个特性被默认启用,TaintNodesByCondition 这个特性只会为节点添加NoSchedule效果的污点,TaintBasedEviction则为节点添加NoExecute效果的污点。
在TaintBasedEvictions特性被开启之后,kubelet会在有资源压力时对相应的Node节点自动加上对应的NoExecute效果的Taint,例如 node.kubernetes.io/memory-pressure、node.kubernetes.io/disk-pressure。
如果Pod没有设置对应的Toleration,则这部分Pod将被驱逐,以确保节点不会崩溃。
07 优先级调度
对于运行各种负载(如:Service
、Job
)的中等规模或者大规模的集群来说,出于各种原因,我们需要尽可能提高集群的资源利用率。
提高资源利用率的常规做法是采用优先级方案,即不同类型的负载对应不同的优先级,同时允许集群中的所有负载所需的资源总量超过集群可提供的资源,在这种情况下,当发生资源不足的情况时,系统可以选择释放一些不重要的负载(优先级最低的),保障最重要的负载能够获取足够的资源稳定运行。
7.1 案例
7.1.1 创建PriorityClass
首先,由集群管理员创建PriorityClass
(PriorityClass
不属于任何命名空间):
apiversion:scheduling.k8s.io/vlbetal kind:Priorityclass
metadata:
name:high-priority
va1ue:1000000
globalDefault:false
description:"This priority class should be used for XYZ service pods only."
上述YAML
文件定义了一个名为high-priority
的优先级类别,优先级为 100000
,数字越大,优先级越高,超过一亿的数字被系统保留,用于指派给系统组件。
7.1.2 Pod声明优先级类别
可以在任意Pod
上引用上述Pod
优先级类别:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
env: test
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
priorityclassName: high-priority
如果发生了需要抢占的调度,高优先级Pod
就可能抢占节点N
,并将其低优先级Pod
驱逐出节点N
,高优先级Pod
的status
信息中的nominatedNodeName
字段会记录目标节点的名称。
需要注意,高优先级Pod
仍然无法保证最终被调度到节点N
上,在节点N
上低优先级Pod
被驱逐的过程中,如果有新的节点满足高优先级Pod
的需求,就会把它调度到新的Node
上。
而如果在等待低优先级的Pod
退出的过程中,又出现了优先级更高的Pod
,调度器就会调度这个更高优先级的Pod
到节点N
上,并重新调度之前等待的高优先级Pod
。
7.1.3 注意事项
优先级抢占的调度方式可能会导致调度陷入“死循环”状态。当Kubernetes
集群配置了多个调度器(Scheduler
)时,这一行为可能就会发生,比如下面这个例子:
Scheduler A
为了调度一个(批)Pod
,特地驱逐了一些Pod
,因此在集群中有了空余的空间可以用来调度,此时Scheduler B
恰好抢在Scheduler A
之前调度了一个新的Pod
,消耗了相应的资源,因此,当Scheduler A
清理完资源后正式发起Pod
的调度时,却发现资源不足,被目标节点的kubelet
进程拒绝了调度请求! 这种情况的确无解,因此最好的做法是让多个Scheduler相互协作来共同实现一个目标。
高优先级Pod
抢占节点并驱逐低优先级的Pod
,这个问题对于普通的服务型的
Pod
来说问题不大,但对于执行批处理任务的Pod
来说就可能是个灾难,当一个高 优先级的批处理任务的Pod
创建后,正在执行批处理任务的某个低优先级的Pod
可 能因为资源不足而被驱逐,从而导致对应的批处理任务被搁置。
为了避免这个问题发生,PriorityClass
增加了一个新的属性一preemptionPolicy
,当它的值为 preemptionLowerPriorty
(默认)时,就执行抢占功能,当它的值被设置为Never
时,就默认不抢占资源,而是静静地排队,等待自己的调度机会。
08 DaemonSet(每个node上只调度一个pod)
DaemonSet是 Kubernetes1.2
版本新增的一种资源对象,用于管理在集群中的每个Node
上仅运行一份Pod
的副本实例,如下图所示:
8.1 DaemonSet
8.1.1 应用场景
下面举例DaemonSet的一些使用场景:
- 在每个
Node
上都运行一个GlusterFS
存储或者Ceph
存储的Daemon
进程; - 在每个
Node
上都运行一个日志采集程序,例如Fluentd
或者Logstach
; - 在每个
Node
上都运行一个性能监控程序,采集该Node
的运行性能数据, 例如Prometheus Node Exporter
、collectd
、New Relic agent
或者Ganglia gmond
等。
DaemonSet的Pod调度策略与RC类似,除了使用系统内置的算法在每个Node上进行调度,也可以在Pod的定义中使用NodeSelector或NodeAffinity来指定满足条件的Node范围进行调度。
8.1.2 举例
下面的例子定义了为在每个Node上都启动一个fluentd
容器,配置文件 fluentd-ds.yaml
的内容如下 (其中挂载了物理机的两个目录"/var/log
"和 “/var/lib/docker/containers
”):
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-cloud-logging
namespace: kube-system
labels:
k8s-app: fluentd-cloud-logging
spec:
template:
metadata:
namespace: kube-system
labels:
k8s-app: fluentd-cloud-logging
spec:
containers:
- name: fluentd-cloud-logging
image: gcr.io/google containers/fluentd-elasticsearch:1.17
resources:
limits:
cpu: 100m
memory: 200Mi
env:
- name: FLUENTD ARGS
value: -q
volumeMounts
- name: varlog
mountPath: /var/log
readOnly: false
- name: containers
mountPath: /var/lib/docker/containers
readonly: false
volumes:
- name: containers
hostPath:
path: /var/lib/docker/containers
- name: varlog
hostPath:
path: /var/log
使用kubectl create
命令创建该DaemonSet
:
kubectl create -f fluentd-ds.yaml
daemonset "fluentd-cloud-logging"created
查看创建好的DaemonSet
和Pod
,可以看到在每个Node
上都创建了一个
Pod
:
8.1.3 注意事项
DaemonSet调度不同于普通的Pod调度,所以没有用默认的Kubernetes Scheduler
进行调度,而是通过专有的DaemonSet Controller
进行调度。但是随着Kubernetes
版本的改进和调度特性不断丰富,产生了一些难以解决的矛盾,最主要的两个矛盾如下:
- 普通的Pod是在Pending状态触发调度并被实例化的,DaemonSet Controller并不是在这个状态调度Pod的,这种不一致容易误导和迷惑用户;
- Pod优先级调度是被Kubernetes Scheduler执行的,而DaemonSet
Controller并没有考虑到Pod优先级调度的问题,也产生了不一致的结果。
从Kubernetes 1.8开始,DaemonSet
的调度默认切换到Kubernetes Scheduler
进行,从而一劳永逸地解决了以上问题及未来可能的新问题,因为默认切换到了Kubernetes Scheduler
统一调度Pod
,因此DaemonSet
也能正确处理Taints
和Tolerations
的问题。
09 批处理调度
Kubernetes从1.2版本开始支持批处理类型的应用,我们可以通过Kubernetes Job
资源对象来定义并启动一个批处理任务。
9.1 批处理调度
批处理任务通常并行(或者串行) 启动多个计算进程去处理一批工作项(Work item
),处理完成后,整个批处理任务结束。
9.1.1 任务模式分类
9.1.1.1 按实现方式分类
按照批处理任务实现方式的不同,批处理任务可以分为如图所示的几种模式:
模式分类:
模式名称 | 描述 |
Job Template Expansion | 一个 |
Queue with Pod Per Work Item | 采用一个任务队列存放 |
Queue with Variable Pod Count | 也是采用一个任务队列存放 |
Single Job with Static Work Assignment | 也是一个 |
模式对比:
模式名称 | 是否是一个job | pod的数量少于work item | 用户程序是否要做相应的修改 | kubernetes是否支持 |
Job Template Expansion | / | / | 是 | 是 |
Queue with Pod Per Work Item | 是 | / | 有时候需要 | 是 |
Queue with Variable Pod Count | 是 | / | / | 是 |
Single Job with Static Work Assignment | 是 | / | 是 | / |
9.1.1.2 按批处理并行分类
考虑到批处理的并行问题,Kubernetes
将Job
分以下三种类型:
类型 | 描述 |
Non-parallel Jobs | 通常一个Job只启动一个Pod,除非Pod异常,才会重启该Pod,一旦此Pod正常结束,Job将结束 |
Parallel Jobs with a fixed completion count | 并行Job会启动多个Pod,此时需要设定Job的 |
Parallel Jobs with a work queue | 任务队列方式的并行Job需要一个独 立的Queue,Work item都在一个Queue中存放,不能设置Job 的 |
9.1.2 案例
9.1.2.1 Job Template Expansion案例
首先是Job Template Expansion模式,由于在这种模式下每个Work item
都对应一个Job
实例,所以这种模式首先定义一个Job
模板,模板里的主要参数是Work item
的标识,因为每个Job
都处理不同的Work item
。
如下所示的Job
模板(文件名为job.yaml.txt
)中的 $ITEM 可以作为任务项的标识:
apiVersion: batch/v1
kind: Job
metadata:
name: process-item-$ITEM
labels:
jobgroup: jobexample
spec:
template:
metadata:
name: jobexample
labels:
jobgroup: jobexample
spec:
containers:
- name: c
image: busybox
command: ["sh","-c","echo Processing item $ITEM &sleep 5"]
restartPolicy: Never
通过下面的操作,生成了3个对应的Job
定义文件并创建Job
:
> for i in apple banana cherry
> do
> cat job.yaml.txt | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
> done
# ls jobs
job-apple.yaml job-banana.yaml job-cherry.yaml
# kubectl create -f jobs
job "process-item-apple"created
job "process-item-banana"created
job "process-item-cherry"created
观察Job的运行情况:
$ kubect1 get jobs -l jobgroup=jobexample
NAME DESIRED SUCCESSFUL AGE
process-item-apple 1 1 4m
process-item-banana 1 1 4m
process-item-cherry 1 1 4m
9.1.2.2 Queue with Pod Per Work Item案例
在这种模式下需要一个任务队列存放Work item
,比如RabbitMQ
客户端程序先把要处理的任务变成Work item
放入任务队列,然后编写Worker
程序、打包镜像并定义成为Job
中的Work Pod
。
Worker
程序的实现逻辑是从任务队列中拉取一个Work item
并处理, 在处理完成后结束进程。并行度为2的Demo如下图所示:
9.1.2.3 Queue with Variable Pod Count案例
由于这种模式下,Worker
程序需要知道队列中是否还有等待处理的Work item
,如果有就取出来处理,否则就认为所有工作完成并结束进程,所以任务队列通常要采用Redis或者数据库来实现:
10 定时任务
Kubernetes从1.5版本开始增加了一种新类型的Job,即类似Linux Cron的定时任务Cron Job
,下面看看如何定义和使用这种类型的Job
。
10.1 基本语法
首先,确保
Kubernetes
的版本为1.8
及以上。
Cron Job的定时表达式基本上照搬了Linux Cron的表达式,格式如下:
Minutes Hours DayofMonth Month DayofWeek
其中每个域都可出现的字符如下。
域 | 描述 |
Minutes | 可出现 |
Hours | 可出现 |
DayofMonth | 可出现 |
Month | 可出现 |
DayofWeek | 可出现 |
表达式中的特殊字符“*”
与“/”
的含义如下:
-
*
:表示匹配该域的任意值,假如在Minutes
域使用“*”
,则表示每分钟都会触发事件; -
/
:表示从起始时间开始触发,然后每隔固定时间触发一次,例如在
Minutes域设置为5/20,则意味着第1次触发在第5min时,接下来每20min触发一 次,将在第25min、第45min等时刻分别触发。
10.2 案例
比如,我们要每隔1min
执行一次任务,则Cron表达式如下:
*/1 * * * *
编写一个Cron Job的配置文件(cron.yaml
):
apiversion: batch/vl beta
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name:hello
image:busybox
args:
- /bin/sh
- -C
- date;echo Hello from the Kubernetes cluster restartPolicy:OnFailure
该例子定义了一个名为hello
的Cron Job
,任务每隔1min执行一次,运行的镜像是busybox
,运行的命令是Shell
脚本,脚本运行时会在控制台输出当前时间和字符串"Hello from the Kubernetes cluster".
接下来运行kubectl create
命令完成创建:
$ kubectl create -f cron.yaml
cronjob "hello"created
然后每隔1min
运行kubectl get cronjob hello
查看任务状态,发现的确每分钟调度了一次:
还可以通过查找Cron Job对应的容器,验证每隔1min产生一个容器的事实:
查看任意一个容器的日志,结果如下:
运行下面的命令,可以更直观地了解Cron Job定期触发任务执行的历史和现状:
在Kubernetes1.9
版本后,kubectl
命令增加了别名cj
来表示cronjob
,同时 kubectl set image/env
命令也可以作用在CronJob
对象上。
11 容灾调度
我们可以将Pod
的各种常规调度策略认为是将整个集群视为一个整体,然后进行 “打散或聚合” 的调度。
当我们的集群是为了容灾而建设的跨区域的多中心(多个Zone
)集群,即集群中的节点位于不同区域的机房时,比如:
北京、上海、广 州、武汉,要求每个中心的应用相互容灾备份,又能同时提供服务,此时最好的调度策略就是将需要容灾的应用均匀调度到各个中心,当某个中心出现问题时, 又自动调度到其他中心均匀分布,
Pod
的多中心均匀分布调度效果图如下所示(不管每个中心的Nod节点数量如何):
11.1 如何实现?
用普通的基于Node
标签选择的调度方式也可以实现上述效果,比如为每个
Zone都建立一个Deployment,Pod的副本总数除以Zone的数量就是每个分区的
Pod副本数量。但这样做有个问题:如果某个Zone失效,那么这个Zone的Pod就无法迁移到其他Zone。
另外,topology.kubernetes.io/zone就是Kubernetes默认支持的重要拓扑域之
一,那是否可以用Pod的亲和性调度来解决这个问题呢?不能,因为Pod的亲和性 调度用于解决相关联的Pod的调度问题,不能保证被依赖的Pod被均匀调度到多个Zone。
为了满足这种容灾场景下的特殊调度需求,在Kubernetes1.16版本中首次引入Even Pod Spreading特性,用于通过topologyKey
属性识别Zone,并通过设置新 的参数topologySpreadConstraints来将Pod均匀调度到不同的Zone。
11.2 举例
举个例子, 假如我们的集群被划分为多个Zone
,我们有一个应用(对应的Pod
标签为 app=foo
)需要在每个Zone
均匀调度以实现容灾,则可以定义YAML
文件如下:
spec:
topologySpreadConstraints:
- maxSkew: 1
whenUnsatisfiable: DoNotSchedule
topologyKey: topology.kubernetes.io/zone
selector:
matchLabels:
app: foo
在以上YAML定义中,关键的参数是maxSkew
,用于指定Pod
在各个Zone
上调度时能容忍的最大不均衡数:
- 值越大,表示能接受的不均衡调度越大;
- 值越小,表示各个Zone的Pod数量分布越均匀。
为了理解maxSkew
,我们需要先理解skew
参数的计算公式:
skew[topo]=count[topo]-min(count[topo])
即每个拓扑区域的skew值都为该区域包括的目标Pod数量与整个拓扑区域最少Pod数量的差,而naxSkew就是最大的skew值。
假如在上面的例子中有3个拓扑区域,分别为Zone A、Zone B及Zone C,有3个目标Pod需要调度到这些拓扑区域,那么前两个毫无疑问会被调度到Zone A和Zone B,Even Pod Spreading调度效果如图所示:
那么,第3个Pod
会被调度到哪里呢?我们可以手动计算每个Zone的skew:
- 首先计算出
min(count[topo])
是0,对应Zone C; - 于是
Zone A
的skew=1-0=1,Zone B
的skew=1-0=0,Zone C
的skew=0-0=0,于是第3个Pod应该被放在Zone C
,此时min(count[topo])的值就变成了1,而实际的maxSkew的值为0,符合预期设置; - 如果我们把maxSkew设置为2,则在这种情况下,第3个Pod被放在
Zone A
或Zone B
都是符合要求的。
有了新的Even Pod Spreading调度特性的加持,再加上之前就已成熟的Pod亲和性调度,Kubernetes就可以完美实现特定应用的容灾部署目标了。
具体做法也很简单:将一个应用中需要部署在一起的几个Pod用亲和性调度声明捆绑,然后选 择其中一个Pod,加持Even Pod Spreading调度规则即可。最终的部署效果图如下: