Kubernetes-5 Pod的编排与调度
# 无状态负载(Deployment)
# 无状态负载(Deployment)
Pod是Kubernetes创建或部署的最小单位,但是Pod是被设计为相对短暂的一次性实体,Pod可以被驱逐(当节点资源不足时)、随着集群的节点崩溃而消失。Kubernetes提供了Controller(控制器)来管理Pod,Controller可以创建和管理多个Pod,提供副本管理、滚动升级和自愈能力,其中最为常用的就是Deployment。
图1 Deployment

一个Deployment可以包含一个或多个Pod副本,每个Pod副本的角色相同,所以系统会自动为Deployment的多个Pod副本分发请求。
Deployment集成了上线部署、滚动升级、创建副本、恢复上线的功能,在某种程度上,Deployment实现无人值守的上线,大大降低了上线过程的复杂性和操作风险。
# 创建Deployment
以下示例为创建一个名为nginx的Deployment负载,使用nginx:latest镜像创建两个Pod,每个Pod占用100m CPU、200Mi内存。
apiVersion: apps/v1 # 注意这里与Pod的区别,Deployment是apps/v1而不是v1
kind: Deployment # 资源类型为Deployment
metadata:
name: nginx # Deployment的名称
spec:
replicas: 2 # Pod的数量,Deployment会确保一直有2个Pod运行
selector: # Label Selector
matchLabels:
app: nginx
template: # Pod的定义,用于创建Pod,也称为Pod template
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:latest
name: container-0
resources:
limits:
cpu: 100m
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
从这个定义中可以看到Deployment的名称为nginx,spec.replicas定义了Pod的数量,即这个Deployment控制2个Pod;spec.selector是Label Selector(标签选择器),表示这个Deployment会选择Label为app=nginx的Pod;spec.template是Pod的定义,内容与Pod中的定义完全一致。
将上面Deployment的定义保存到deployment.yaml文件中,使用kubectl创建这个Deployment。
使用kubectl get查看Deployment和Pod,可以看到READY值为2/2,前一个2表示当前有2个Pod运行,后一个2表示期望有2个Pod,AVAILABLE为2表示有2个Pod是可用的。
$ kubectl create -f deployment.yaml
deployment.apps/nginx created
$ kubectl get deploy
NAME READY UP-TO-DATE AVAILABLE AGE
nginx 2/2 2 2 4m5s
2
3
4
5
6
# Deployment如何控制Pod
继续查询Pod,如下所示。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-7f98958cdf-tdmqk 1/1 Running 0 13s
nginx-7f98958cdf-txckx 1/1 Running 0 13s
2
3
4
如果删掉一个Pod,您会发现立马会有一个新的Pod被创建出来,如下所示,这就是前面所说的Deployment会确保有2个Pod在运行,如果删掉一个,Deployment会重新创建一个,如果某个Pod故障或有其他问题,Deployment会自动拉起这个Pod。
$ kubectl delete pod nginx-7f98958cdf-txckx
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-7f98958cdf-tdmqk 1/1 Running 0 21s
nginx-7f98958cdf-tesqr 1/1 Running 0 1s
2
3
4
5
6
看到有如下两个名为nginx-7f98958cdf-tdmqk和nginx-7f98958cdf-tesqr的Pod, 其中nginx是直接使用Deployment的名称,-7f98958cdf-tdmqk和-7f98958cdf-tesqr是kubernetes随机生成的后缀。
也许会发现这两个后缀中前面一部分是相同的,都是7f98958cdf,这是因为Deployment不是直接控制Pod的,Deployment是通过一种名为ReplicaSet的控制器控制Pod,通过如下命令可以查询ReplicaSet,其中rs是ReplicaSet的缩写。
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-7f98958cdf 2 2 2 1m
2
3
这个ReplicaSet的名称为nginx-7f98958cdf,后缀-7f98958cdf也是随机生成的。
Deployment控制Pod的方式如图所示,Deployment控制ReplicaSet,ReplicaSet控制Pod。
图2 Deployment通过ReplicaSet控制Pod

如果使用kubectl describe命令查看Deployment的详情,就可以看到ReplicaSet,如下所示,可以看到有一行NewReplicaSet: nginx-7f98958cdf (2/2 replicas created),而且Events里面事件确是把ReplicaSet的实例扩容到2个。在实际使用中您也许不会直接操作ReplicaSet,但了解Deployment通过控制ReplicaSet来控制Pod会有助于您定位问题。
$ kubectl describe deploy nginx
Name: nginx
Namespace: default
CreationTimestamp: Sun, 16 Dec 2018 19:21:58 +0800
Labels: app=nginx
...
NewReplicaSet: nginx-7f98958cdf (2/2 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 5m deployment-controller Scaled up replica set nginx-7f98958cdf to 2
2
3
4
5
6
7
8
9
10
11
12
13
# 升级
在实际应用中,升级是一个常见的场景,Deployment能够很方便地支撑应用升级。
Deployment可以设置不同的升级策略,有如下两种。
RollingUpdate:滚动升级,即逐步创建新Pod再删除旧Pod,为默认策略。
Recreate:替换升级,即先把当前Pod删掉再重新创建Pod。
Deployment的升级可以是声明式的,也就是说只需要修改Deployment的YAML定义即可,比如使用kubectl edit命令将上面Deployment中的镜像修改为nginx:alpine。修改完成后再查询ReplicaSet和Pod,发现创建了一个新的ReplicaSet,Pod也重新创建了。
$ kubectl edit deploy nginx
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
nginx-6f9f58dffd 2 2 2 1m
nginx-7f98958cdf 0 0 0 48m
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-6f9f58dffd-tdmqk 1/1 Running 0 1m
nginx-6f9f58dffd-tesqr 1/1 Running 0 1m
2
3
4
5
6
7
8
9
10
11
Deployment可以通过maxSurge和maxUnavailable两个参数控制升级过程中同时重新创建Pod的比例,这在很多时候是非常有用,配置如下所示。
spec:
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
type: RollingUpdate
2
3
4
5
6
maxSurge:与Deployment中spec.replicas相比,可以有多少个Pod存在,默认值是25%,比如spec.replicas为 4,那升级过程中就不能超过5个Pod存在,即按1个的步伐升级,实际升级过程中会换算成数字,且换算会向上取整。这个值也可以直接设置成数字。
maxUnavailable:与Deployment中spec.replicas相比,可以有多少个Pod失效,也就是删除的比例,默认值是25%,比如spec.replicas为4,那升级过程中就至少有3个Pod存在,即删除Pod的步伐是1。同样这个值也可以设置成数字。
在前面的例子中,由于spec.replicas是2,如果maxSurge和maxUnavailable都为默认值25%,那实际升级过程中,maxSurge允许最多3个Pod存在(向上取整,2*1.25=2.5,取整为3),而maxUnavailable则不允许有Pod Unavailable(向上取整,2*0.75=1.5,取整为2),也就是说在升级过程中,一直会有2个Pod处于运行状态,每次新建一个Pod,等这个Pod创建成功后再删掉一个旧Pod,直至Pod全部为新Pod。
# 回滚
回滚也称为回退,即当发现升级出现问题时,让应用回到老的版本。Deployment可以非常方便地回滚到老版本。
例如上面升级的新版镜像有问题,可以执行kubectl rollout undo命令进行回滚。
$ kubectl rollout undo deployment nginx
deployment.apps/nginx rolled back
2
Deployment之所以能如此容易的做到回滚,是因为Deployment是通过ReplicaSet控制Pod的,升级后之前ReplicaSet都一直存在,Deployment回滚做的就是使用之前的ReplicaSet再次把Pod创建出来。Deployment中保存ReplicaSet的数量可以使用revisionHistoryLimit参数限制,默认值为10。
# 有状态负载(StatefulSet)
# 有状态负载(StatefulSet)
Deployment控制器下的Pod都有个共同特点,那就是每个Pod除了名称和IP地址不同,其余完全相同。需要的时候,Deployment可以通过Pod模板创建新的Pod;不需要的时候,Deployment就可以删除任意一个Pod。
但是在某些场景下,这并不满足需求,比如有些分布式的场景,要求每个Pod都有自己单独的状态时,比如分布式数据库,每个Pod要求有单独的存储,这时Deployment无法满足业务需求。
分布式有状态应用的特点主要是应用中每个部分的角色不同(即分工不同),比如数据库有主备、Pod之间有依赖,在Kubernetes中部署有状态应用对Pod有如下要求:
Pod能够被别的Pod找到,要求Pod有固定的标识。
每个Pod有单独存储,Pod被删除恢复后,必须读取原来的数据,否则状态就会不一致。
Kubernetes提供了StatefulSet来解决这个问题,其具体如下:
StatefulSet给每个Pod提供固定名称,Pod名称增加从0-N的固定后缀,Pod重新调度后Pod名称和HostName不变。
StatefulSet通过Headless Service给每个Pod提供固定的访问域名。
StatefulSet通过创建固定标识的PVC保证Pod重新调度后还是能访问到相同的持久化数据。
图1 StatefulSet

# 创建Headless Service
如前所述,创建Statefulset需要一个Headless Service (opens new window)用于Pod访问。
使用如下文件描述Headless Service,其中:
spec.clusterIP:必须设置为None,表示Headless Service。
spec.ports.port:Pod间通信端口号。
spec.ports.name:Pod间通信端口名称。
apiVersion: v1
kind: Service # 对象类型为Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- name: nginx # Pod间通信的端口名称
port: 80 # Pod间通信的端口号
selector:
app: nginx # 选择标签为app:nginx的Pod
clusterIP: None # 必须设置为None,表示Headless Service
2
3
4
5
6
7
8
9
10
11
12
13
执行如下命令创建Headless Service。
# kubectl create -f headless.yaml
service/nginx created
2
创建完成后可以查询Service。
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 5s
2
3
# 创建Statefulset
Statefulset的YAML定义与其他对象基本相同,主要有两个差异点:
serviceName指定了Statefulset使用哪个Headless Service,需要填写Headless Service的名称。
volumeClaimTemplates是用来申请持久化声明PVC ,这里定义了一个名为data的模板,它会为每个Pod创建一个PVC,storageClassName指定了持久化存储的类型,在PV、PVC和StorageClass会详细介绍;volumeMounts是为Pod挂载存储。当然如果不需要存储的话可以删除volumeClaimTemplates和volumeMounts字段。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx
spec:
serviceName: nginx # headless service的名称
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: container-0
image: nginx:alpine
resources:
limits:
cpu: 100m
memory: 200Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts: # Pod挂载的存储
- name: data
mountPath: /usr/share/nginx/html # 存储挂载到/usr/share/nginx/html
imagePullSecrets:
- name: default-secret
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: csi-nas # 持久化存储的类型
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
执行如下命令创建。
# kubectl create -f statefulset.yaml
statefulset.apps/nginx created
2
命令执行后,查询一下StatefulSet和Pod,可以看到Pod的名称后缀从0开始到2,逐个递增。
# kubectl get statefulset
NAME READY AGE
nginx 3/3 107s
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-0 1/1 Running 0 112s
nginx-1 1/1 Running 0 69s
nginx-2 1/1 Running 0 39s
2
3
4
5
6
7
8
9
此时如果手动删除nginx-1这个Pod,然后再次查询Pod,可以看到StatefulSet重新创建了一个名称相同的Pod,通过创建时间5s可以看出nginx-1是刚刚创建的。
# kubectl delete pod nginx-1
pod "nginx-1" deleted
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-0 1/1 Running 0 3m4s
nginx-1 1/1 Running 0 5s
nginx-2 1/1 Running 0 1m10s
2
3
4
5
6
7
8
进入容器查看容器的hostname,可以看到同样是nginx-0、nginx-1和nginx-2。
# kubectl exec nginx-0 -- sh -c 'hostname'
nginx-0
# kubectl exec nginx-1 -- sh -c 'hostname'
nginx-1
# kubectl exec nginx-2 -- sh -c 'hostname'
nginx-2
2
3
4
5
6
同时可以看一下StatefulSet创建的PVC,可以看到这些PVC,都以“PVC名称-StatefulSet名称-编号”的方式命名,并且处于Bound状态。
# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-nginx-0 Bound pvc-f58bc1a9-6a52-4664-a587-a9a1c904ba29 1Gi RWX csi-nas 2m24s
data-nginx-1 Bound pvc-066e3a3a-fd65-4e65-87cd-6c3fd0ae6485 1Gi RWX csi-nas 101s
data-nginx-2 Bound pvc-a18cf1ce-708b-4e94-af83-766007250b0c 1Gi RWX csi-nas 71s
2
3
4
5
# StatefulSet的网络标识
StatefulSet创建后,可以看下Pod是有固定名称的,那Headless Service是如何起作用的呢,那就是使用DNS,为Pod提供固定的域名,这样Pod间就可以使用域名访问,即便Pod被重新创建而导致Pod的IP地址发生变化,这个域名也不会发生变化。
Headless Service创建后,每个Pod的IP都会有下面格式的域名。
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
例如上面的三个Pod的域名就是:
nginx-0.nginx.default.svc.cluster.local
nginx-1.nginx.default.svc.cluster.local
nginx-2.nginx.default.svc.cluster.local
实际访问时可以省略后面的.<namespace>.svc.cluster.local。
下面命令会使用tutum/dnsutils镜像创建一个Pod,进入这个Pod的容器,使用nslookup命令查看Pod对应的域名,可以发现能解析出Pod的IP地址。这里可以看到DNS服务器的地址是10.247.3.10,这是在创建CCE集群时默认安装CoreDNS插件,用于提供DNS服务,后续在Kubernetes网络会详细介绍CoreDNS的作用。
$ kubectl run -i --tty --image tutum/dnsutils dnsutils --restart=Never --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ # nslookup nginx-0.nginx
Server: 10.247.3.10
Address: 10.247.3.10#53
Name: nginx-0.nginx.default.svc.cluster.local
Address: 172.16.0.31
/ # nslookup nginx-1.nginx
Server: 10.247.3.10
Address: 10.247.3.10#53
Name: nginx-1.nginx.default.svc.cluster.local
Address: 172.16.0.18
/ # nslookup nginx-2.nginx
Server: 10.247.3.10
Address: 10.247.3.10#53
Name: nginx-2.nginx.default.svc.cluster.local
Address: 172.16.0.19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
此时如果手动删除这两个Pod,查询被StatefulSet重新创建的Pod的IP,然后使用nslookup命令解析Pod的域名,可以发现nginx-0.nginx和nginx-1.nginx仍然能解析到对应的Pod。这就保证了StatefulSet网络标识不变。
# StatefulSet存储状态
上面说了StatefulSet可以通过PVC做持久化存储,保证Pod重新调度后还是能访问到相同的持久化数据,在删除Pod时,PVC不会被删除。
图2 StatefulSet的Pod重建过程

下面将通过实际操作验证这一点是如何做到的,执行下面的命令,在nginx-1的目录/usr/share/nginx/html中写入一些内容,例如将index.html的内容修改为“hello world”。
# kubectl exec nginx-1 -- sh -c 'echo hello world > /usr/share/nginx/html/index.html'
修改完后,如果在Pod中访问“http://localhost”,那就会返回“hello world”。
# kubectl exec -it nginx-1 -- curl localhost
hello world
2
此时如果手动删除nginx-1这个Pod,然后再次查询Pod,可以看到StatefulSet重新创建了一个名称相同的Pod,通过创建时间4s可以看出nginx-1是刚刚创建的。
# kubectl delete pod nginx-1
pod "nginx-1" deleted
# kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-0 1/1 Running 0 14m
nginx-1 1/1 Running 0 4s
nginx-2 1/1 Running 0 13m
2
3
4
5
6
7
8
再次访问该Pod的index.html页面,会发现仍然返回“hello world”,这说明这个Pod仍然是访问相同的存储。
# kubectl exec -it nginx-1 -- curl localhost
hello world
2
# 普通任务(Job)和定时任务(CronJob)
# 普通任务(Job)和定时任务(CronJob)
Job和CronJob是负责批量处理短暂的一次性任务(short lived one-off tasks),即仅执行一次的任务,它保证批处理任务的一个或多个Pod成功结束。
Job:是Kubernetes用来控制批处理型任务的资源对象。批处理业务与长期伺服业务(Deployment、StatefulSet)的主要区别是批处理业务的运行有头有尾,而长期伺服业务在用户不停止的情况下永远运行。Job管理的Pod根据用户的设置把任务成功完成就自动退出(Pod自动删除)。
CronJob:是基于时间的Job,就类似于Linux系统的crontab文件中的一行,在指定的时间周期运行指定的Job。
任务负载的这种用完即停止的特性特别适合一次性任务,比如持续集成。
# 创建Job
以下是一个Job配置,其计算π到2000位并打印输出。Job结束需要运行50个Pod,这个示例中就是打印π 50次,并行运行5个Pod,Pod如果失败最多重试5次。
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-timeout
spec:
completions: 50 # 运行的次数,即Job结束需要成功运行的Pod个数
parallelism: 5 # 并行运行Pod的数量,默认为1
backoffLimit: 5 # 表示失败Pod的重试最大次数,超过这个次数不会继续重试。
activeDeadlineSeconds: 100 # 表示Pod超期时间,一旦达到这个时间,Job及其所有的Pod都会停止。
template: # Pod定义
spec:
containers:
- name: pi
image: perl
command:
- perl
- "-Mbignum=bpi"
- "-wle"
- print bpi(2000)
restartPolicy: Never
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
根据completions和parallelism的设置,可以将Job划分为以下几种类型。
| 一次性Job | 创建一个Pod直至其成功结束 | 数据库迁移 |
|---|---|---|
| 固定结束次数的Job | 依次创建一个Pod运行直至completions个成功结束 | 处理工作队列的Pod |
| 固定结束次数的并行Job | 依次创建多个Pod运行直至completions个成功结束 | 多个Pod同时处理工作队列 |
| 并行Job | 创建一个或多个Pod直至有一个成功结束 | 多个Pod同时处理工作队列 |
# 创建CronJob
相比Job,CronJob就是一个加了定时的Job,CronJob执行时是在指定的时间创建出Job,然后由Job创建出Pod。
apiVersion: batch/v1
kind: CronJob
metadata:
name: cronjob-example
spec:
schedule: "0,15,30,45 * * * *" # 定时相关配置
jobTemplate: # Job的定义
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: pi
image: perl
command:
- perl
- "-Mbignum=bpi"
- "-wle"
- print bpi(2000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CronJob的格式从前到后就是:
Minute
Hour
Day of month
Month
Day of week
如 "0,15,30,45 * * * * " ,前面逗号隔开的是分钟,后面第一个* 表示每小时,第二个 * 表示每个月的哪天,第三个表示每月,第四个表示每周的哪天。
如果您想要每个月的第一天里面每半个小时执行一次,那就可以设置为" 0,30 * 1 * * " 如果您想每个星期天的3am执行一次任务,那就可以设置为 "0 3 * * 0"。
更详细的CronJob格式说明请参见https://zh.wikipedia.org/wiki/Cron (opens new window)。
# 守护进程集(DaemonSet)
# 守护进程集(DaemonSet)
DaemonSet(守护进程集)在集群的每个节点上运行一个Pod,且保证只有一个Pod,非常适合一些系统层面的应用,例如日志收集、资源监控等,这类应用需要每个节点都运行,且不需要太多实例,一个比较好的例子就是Kubernetes的kube-proxy。
DaemonSet跟节点相关,如果节点异常,也不会在其他节点重新创建。
图1 DaemonSet

# 创建DaemonSet
下面是一个DaemonSet的示例。
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-daemonset
labels:
app: nginx-daemonset
spec:
selector:
matchLabels:
app: nginx-daemonset
template:
metadata:
labels:
app: nginx-daemonset
spec:
nodeSelector: # 节点选择,当节点拥有daemon=need时才在节点上创建Pod
daemon: need
containers:
- name: nginx-daemonset
image: nginx:alpine
resources:
limits:
cpu: 250m
memory: 512Mi
requests:
cpu: 250m
memory: 512Mi
imagePullSecrets:
- name: default-secret
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这里可以看出DaemonSet没有Deployment或StatefulSet中的replicas参数,因为DaemonSet会在每个目标节点上固定部署一个Pod。
Pod模板中有个nodeSelector,指定了只在有“daemon=need”的节点上才创建Pod,如下图所示,DaemonSet只在指定标签的节点上创建Pod。如果需要在每一个节点上创建Pod可以删除该标签。
图2 DaemonSet在指定标签的节点上创建Pod

创建DaemonSet:
$ kubectl create -f daemonset.yaml
daemonset.apps/nginx-daemonset created
2
查询发现nginx-daemonset没有Pod创建。
$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 0 0 0 0 0 daemon=need 16s
$ kubectl get pods
No resources found in default namespace.
2
3
4
5
6
这是因为节点上没有daemon=need这个标签,使用如下命令可以查询节点的标签。
$ kubectl get node --show-labels
NAME STATUS ROLES AGE VERSION LABELS
192.168.0.212 Ready <none> 83m v1.15.6-r1-20.3.0.2.B001-15.30.2 beta.kubernetes.io/arch=amd64 ...
192.168.0.94 Ready <none> 83m v1.15.6-r1-20.3.0.2.B001-15.30.2 beta.kubernetes.io/arch=amd64 ...
192.168.0.97 Ready <none> 83m v1.15.6-r1-20.3.0.2.B001-15.30.2 beta.kubernetes.io/arch=amd64 ...
2
3
4
5
给192.168.0.212这个节点打上标签,然后再查询,发现已经创建了一个Pod,并且这个Pod是在192.168.0.212这个节点上。
$ kubectl label node 192.168.0.212 daemon=need
node/192.168.0.212 labeled
$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 1 1 0 1 0 daemon=need 116s
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-daemonset-g9b7j 1/1 Running 0 18s 172.16.3.0 192.168.0.212
2
3
4
5
6
7
8
9
10
再给192.168.0.94这个节点打上标签,发现又创建了一个Pod:
$ kubectl label node 192.168.0.94 daemon=need
node/192.168.0.94 labeled
$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 2 2 1 2 1 daemon=need 2m29s
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-daemonset-6jjxz 0/1 ContainerCreating 0 8s <none> 192.168.0.94
nginx-daemonset-g9b7j 1/1 Running 0 42s 172.16.3.0 192.168.0.212
2
3
4
5
6
7
8
9
10
11
如果修改掉192.168.0.94节点的标签,可以发现DaemonSet会删除这个节点上的Pod。
$ kubectl label node 192.168.0.94 daemon=no --overwrite
node/192.168.0.94 labeled
$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 1 1 1 1 1 daemon=need 4m5s
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-daemonset-g9b7j 1/1 Running 0 2m23s 172.16.3.0 192.168.0.212
2
3
4
5
6
7
8
9
10
# 亲和与反亲和调度
在守护进程集(DaemonSet)中讲到使用nodeSelector选择Pod要部署的节点,其实Kubernetes还支持更精细、更灵活的调度机制,那就是亲和(affinity)与反亲和(anti-affinity)调度。
Kubernetes支持节点和Pod两个层级的亲和与反亲和。通过配置亲和与反亲和规则,可以允许您指定硬性限制或者偏好,例如将前台Pod和后台Pod部署在一起、某类应用部署到某些特定的节点、不同应用部署到不同的节点等等。
# Node Affinity(节点亲和)
您肯定也猜到了亲和性规则的基础肯定也是标签,先来看一下CCE集群中节点上有些什么标签。
$ kubectl describe node 192.168.0.212
Name: 192.168.0.212
Roles: <none>
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
failure-domain.beta.kubernetes.io/is-baremetal=false
failure-domain.beta.kubernetes.io/region=cn-east-3
failure-domain.beta.kubernetes.io/zone=cn-east-3a
kubernetes.io/arch=amd64
kubernetes.io/availablezone=cn-east-3a
kubernetes.io/eniquota=12
kubernetes.io/hostname=192.168.0.212
kubernetes.io/os=linux
node.kubernetes.io/subnetid=fd43acad-33e7-48b2-a85a-24833f362e0e
os.architecture=amd64
os.name=EulerOS_2.0_SP5
os.version=3.10.0-862.14.1.5.h328.eulerosv2r7.x86_64
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这些标签都是在创建节点的时候CCE会自动添加上的,下面介绍几个在调度中会用到比较多的标签。
failure-domain.beta.kubernetes.io/region:表示节点所在的区域,如果上面这个节点标签值为cn-east-3,表示节点在上海一区域。
failure-domain.beta.kubernetes.io/zone:表示节点所在的可用区(availability zone)。
kubernetes.io/hostname:节点的hostname。
另外在Label:组织Pod的利器章节还介绍自定义标签,通常情况下,对于一个大型Kubernetes集群,肯定会根据业务需要定义很多标签。
在守护进程集(DaemonSet)中介绍了nodeSelector,通过nodeSelector可以让Pod只部署在具有特定标签的节点上。如下所示,Pod只会部署在拥有gpu=true这个标签的节点上。
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
nodeSelector: # 节点选择,当节点拥有gpu=true时才在节点上创建Pod
gpu: true
...
2
3
4
5
6
7
8
通过节点亲和性规则配置,也可以做到同样的事情,如下所示。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu
labels:
app: gpu
spec:
selector:
matchLabels:
app: gpu
replicas: 3
template:
metadata:
labels:
app: gpu
spec:
containers:
- image: nginx:alpine
name: gpu
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
看起来这要复杂很多,但这种方式可以得到更强的表达能力,后面会进一步介绍。
这里affinity表示亲和,nodeAffinity表示节点亲和,requiredDuringSchedulingIgnoredDuringExecution非常长,不过可以将这个分作两段来看:
前半段requiredDuringScheduling表示下面定义的规则必须强制满足(require)才会调度Pod到节点上。
后半段IgnoredDuringExecution表示已经在节点上运行的Pod不需要满足下面定义的规则,即去除节点上的某个标签,那些需要节点包含该标签的Pod不会被重新调度。
另外操作符operator的值为In,表示标签值需要在values的列表中,其他operator取值如下。
NotIn:标签的值不在某个列表中
Exists:某个标签存在
DoesNotExist:某个标签不存在
Gt:标签的值大于某个值(字符串比较)
Lt:标签的值小于某个值(字符串比较)
需要说明的是并没有nodeAntiAffinity(节点反亲和),因为NotIn和DoesNotExist可以提供相同的功能。
下面来验证这段规则是否生效,首先给192.168.0.212这个节点打上gpu=true的标签。
$ kubectl label node 192.168.0.212 gpu=true
node/192.168.0.212 labeled
$ kubectl get node -L gpu
NAME STATUS ROLES AGE VERSION GPU
192.168.0.212 Ready <none> 13m v1.15.6-r1-20.3.0.2.B001-15.30.2 true
192.168.0.94 Ready <none> 13m v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.97 Ready <none> 13m v1.15.6-r1-20.3.0.2.B001-15.30.2
2
3
4
5
6
7
8
创建这个Deployment,可以发现所有的Pod都部署在了192.168.0.212这个节点上。
$ kubectl create -f affinity.yaml
deployment.apps/gpu created
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
gpu-6df65c44cf-42xw4 1/1 Running 0 15s 172.16.0.37 192.168.0.212
gpu-6df65c44cf-jzjvs 1/1 Running 0 15s 172.16.0.36 192.168.0.212
gpu-6df65c44cf-zv5cl 1/1 Running 0 15s 172.16.0.38 192.168.0.212
2
3
4
5
6
7
8
# 节点优先选择规则
上面讲的requiredDuringSchedulingIgnoredDuringExecution是一种强制选择的规则,节点亲和还有一种优先选择规则,即preferredDuringSchedulingIgnoredDuringExecution,表示会根据规则优先选择哪些节点。
为演示这个效果,先为上面的集群添加一个节点,且这个节点跟另外三个节点不在同一个可用区,创建完之后查询节点的可用区标签,如下所示,新添加的节点在cn-east-3c这个可用区。
$ kubectl get node -L failure-domain.beta.kubernetes.io/zone,gpu
NAME STATUS ROLES AGE VERSION ZONE GPU
192.168.0.100 Ready <none> 7h23m v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3c
192.168.0.212 Ready <none> 8h v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3a true
192.168.0.94 Ready <none> 8h v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3a
192.168.0.97 Ready <none> 8h v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3a
2
3
4
5
6
下面定义一个Deployment,要求Pod优先部署在可用区cn-east-3a的节点上,可以像下面这样定义,使用preferredDuringSchedulingIgnoredDuringExecution规则,给cn-east-3a设置权重(weight)为80,而gpu=true权重为20,这样Pod就优先部署在cn-east-3a的节点上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu
labels:
app: gpu
spec:
selector:
matchLabels:
app: gpu
replicas: 10
template:
metadata:
labels:
app: gpu
spec:
containers:
- image: nginx:alpine
name: gpu
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: failure-domain.beta.kubernetes.io/zone
operator: In
values:
- cn-east-3a
- weight: 20
preference:
matchExpressions:
- key: gpu
operator: In
values:
- "true"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
来看实际部署后的情况,可以看到部署到192.168.0.212这个节点上的Pod有5个,而192.168.0.100上只有2个。
$ kubectl create -f affinity2.yaml
deployment.apps/gpu created
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
gpu-585455d466-5bmcz 1/1 Running 0 2m29s 172.16.0.44 192.168.0.212
gpu-585455d466-cg2l6 1/1 Running 0 2m29s 172.16.0.63 192.168.0.97
gpu-585455d466-f2bt2 1/1 Running 0 2m29s 172.16.0.79 192.168.0.100
gpu-585455d466-hdb5n 1/1 Running 0 2m29s 172.16.0.42 192.168.0.212
gpu-585455d466-hkgvz 1/1 Running 0 2m29s 172.16.0.43 192.168.0.212
gpu-585455d466-mngvn 1/1 Running 0 2m29s 172.16.0.48 192.168.0.97
gpu-585455d466-s26qs 1/1 Running 0 2m29s 172.16.0.62 192.168.0.97
gpu-585455d466-sxtzm 1/1 Running 0 2m29s 172.16.0.45 192.168.0.212
gpu-585455d466-t56cm 1/1 Running 0 2m29s 172.16.0.64 192.168.0.100
gpu-585455d466-t5w5x 1/1 Running 0 2m29s 172.16.0.41 192.168.0.212
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面这个例子中,对于节点排序优先级如下所示,有两个标签的节点排序最高,只有cn-east-3a标签的节点排序第二(权重为80),只有gpu=true的节点排序第三,没有的节点排序最低。
图1 优先级排序顺序

这里您看到Pod并没有调度到192.168.0.94这个节点上,这是因为这个节点上部署了很多其他Pod,资源使用较多,所以并没有往这个节点上调度,这也侧面说明preferredDuringSchedulingIgnoredDuringExecution是优先规则,而不是强制规则。
# 工作负载亲和(podAffinity)
节点亲和的规则只能影响Pod和节点之间的亲和,Kubernetes还支持Pod和Pod之间的亲和,例如将应用的前端和后端部署在一起,从而减少访问延迟。Pod亲和同样有requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution两种规则。
说明:
对于工作负载亲和来说,使用requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution规则时, topologyKey字段不允许为空。
来看下面这个例子,假设有个应用的后端已经创建,且带有app=backend的标签。
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-dlrz8 1/1 Running 0 2m36s 172.16.0.67 192.168.0.100
2
3
将前端frontend的pod部署在backend一起时,可以做如下Pod亲和规则配置。
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
selector:
matchLabels:
app: frontend
replicas: 3
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: nginx:alpine
name: frontend
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
创建frontend然后查看,可以看到frontend都创建到跟backend一样的节点上了。
$ kubectl create -f affinity3.yaml
deployment.apps/frontend created
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-dlrz8 1/1 Running 0 5m38s 172.16.0.67 192.168.0.100
frontend-67ff9b7b97-dsqzn 1/1 Running 0 6s 172.16.0.70 192.168.0.100
frontend-67ff9b7b97-hxm5t 1/1 Running 0 6s 172.16.0.71 192.168.0.100
frontend-67ff9b7b97-z8pdb 1/1 Running 0 6s 172.16.0.72 192.168.0.100
2
3
4
5
6
7
8
9
这里有个topologyKey字段(用于划分拓扑域),意思是先圈定topologyKey指定的范围,当节点上的标签键、值均相同时会被认为同一拓扑域,然后再选择下面规则定义的内容。这里每个节点上都有kubernetes.io/hostname,所以看不出topologyKey起到的作用。
如果backend有两个Pod,分别在不同的节点上。
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-5bpd6 1/1 Running 0 23m 172.16.0.40 192.168.0.97
backend-658f6cb858-dlrz8 1/1 Running 0 2m36s 172.16.0.67 192.168.0.100
2
3
4
给192.168.0.97和192.168.0.94打上prefer=true的标签。
$ kubectl label node 192.168.0.97 prefer=true
node/192.168.0.97 labeled
$ kubectl label node 192.168.0.94 prefer=true
node/192.168.0.94 labeled
$ kubectl get node -L prefer
NAME STATUS ROLES AGE VERSION PREFER
192.168.0.100 Ready <none> 44m v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.212 Ready <none> 91m v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.94 Ready <none> 91m v1.15.6-r1-20.3.0.2.B001-15.30.2 true
192.168.0.97 Ready <none> 91m v1.15.6-r1-20.3.0.2.B001-15.30.2 true
2
3
4
5
6
7
8
9
10
11
将podAffinity的topologyKey定义为prefer,则节点拓扑域的划分如图2所示。
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: prefer
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend
2
3
4
5
6
7
8
9
10
图2 拓扑域示意图

调度时,会根据prefer标签划分节点拓扑域,本示例中192.168.0.97和192.168.0.94被划作同一拓扑域。如果当拓扑域中运行着app=backend的Pod,即使该拓扑域中并非所有节点均运行了app=backend的Pod(本例该拓扑域中仅192.168.0.97节点上存在app=backend的Pod),frontend同样会部署在此拓扑域中(这里的192.168.0.97或192.168.0.94)。
$ kubectl create -f affinity3.yaml
deployment.apps/frontend created
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-5bpd6 1/1 Running 0 26m 172.16.0.40 192.168.0.97
backend-658f6cb858-dlrz8 1/1 Running 0 5m38s 172.16.0.67 192.168.0.100
frontend-67ff9b7b97-dsqzn 1/1 Running 0 6s 172.16.0.70 192.168.0.97
frontend-67ff9b7b97-hxm5t 1/1 Running 0 6s 172.16.0.71 192.168.0.97
frontend-67ff9b7b97-z8pdb 1/1 Running 0 6s 172.16.0.72 192.168.0.97
2
3
4
5
6
7
8
9
10
# 工作负载反亲和(podAntiAffinity)
前面讲了Pod的亲和,通过亲和将Pod部署在一起,有时候需求却恰恰相反,需要将Pod分开部署,例如Pod之间部署在一起会影响性能的情况。
说明:
对于工作负载反亲和来说,使用requiredDuringSchedulingIgnoredDuringExecution规则时, Kubernetes默认的准入控制器 LimitPodHardAntiAffinityTopology要求topologyKey字段只能是kubernetes.io/hostname。如果您希望使用其他定制拓扑逻辑,可以更改或者禁用该准入控制器。
下面例子中定义了反亲和规则,这个规则表示根据kubernetes.io/hostname标签划分节点拓扑域,且如果该拓扑域中的某个节点上已经存在带有app=frontend标签的Pod,那么拥有相同标签的Pod将不能被调度到该拓扑域内的其他节点上。
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
selector:
matchLabels:
app: frontend
replicas: 5
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: nginx:alpine
name: frontend
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname #节点拓扑域
labelSelector: #Pod标签匹配规则
matchExpressions:
- key: app
operator: In
values:
- frontend
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
创建并查看部署效果,示例中根据kubernetes.io/hostname标签划分节点拓扑域,在拥有kubernetes.io/hostname标签的节点中,每个节点的标签值均不同,因此一个拓扑域中只有一个节点。当一个拓扑域中(此处为一个节点)已经存在frontend标签的Pod时,该拓扑域不会被继续调度具有相同标签的Pod。本例中只有4个节点,因此还有一个Pod处于Pending状态无法调度。
$ kubectl create -f affinity4.yaml
deployment.apps/frontend created
$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
frontend-6f686d8d87-8dlsc 1/1 Running 0 18s 172.16.0.76 192.168.0.100
frontend-6f686d8d87-d6l8p 0/1 Pending 0 18s <none> <none>
frontend-6f686d8d87-hgcq2 1/1 Running 0 18s 172.16.0.54 192.168.0.97
frontend-6f686d8d87-q7cfq 1/1 Running 0 18s 172.16.0.47 192.168.0.212
frontend-6f686d8d87-xl8hx 1/1 Running 0 18s 172.16.0.23 192.168.0.94
2
3
4
5
6
7
8
9
10
打赏一下

「真诚赞赏,手留余香」
# 打赏记录
| 打赏者 | 打助金额 (元) | 支付方式 | 时间 | 备注 |
|---|---|---|---|---|
| John | 12 | 微信 | 2020-06-09 | |
| 艾斯 | 32 | 支付宝 | 2020-07-11 | nice |
| HickSalmon | 15 | 微信 | 2020-09-21 | 有赏交流 |