3-k8s笔记-深入掌握Pod

第3章 深入掌握Pod
3.1 Pod定义详解
3.2 Pod的基本用法
3.3 静态Pod
3.4 Pod容器共享Volume
3.5 Pod的配置管理
3.5.1 ConfigMap概述
3.5.2 创建ConfigMap资源对象
3.5.3 在Pod中使用ConfigMap
3.5.4 使用ConfigMap的限制条件
3.6 在容器内获取Pod信息(Downward API)
3.6.1 环境变量方式:将Pod信息注入为环境变量
3.6.2 环境变量方式:将容器资源信息注入为环境变量
3.6.3 Volume挂载方式
3.7 Pod生命周期和重启策略
3.8 Pod健康检查和服务可用性检查
3.9 玩转Pod调度
3.9.1 Deployment或RC:全自动调度
3.9.2 NodeSelector:定向调度
3.9.3 NodeAffinity:Node亲和性调度
3.9.4 PodAffinity:Pod亲和与互斥调度策略
3.9.5 Taints和Tolerations(污点和容忍)
3.9.6 Pod Priority Preemption:Pod优先级调度
3.9.7 DaemonSet:在每个Node上都调度一个Pod
3.9.8 Job:批处理调度
3.9.9 Cronjob:定时任务
3.9.10 自定义调度器
3.10 Init Container(初始化容器)
3.11 Pod的升级和回滚
3.11.1 Deployment的升级
3.11.2 Deployment的回滚
3.11.3 暂停和恢复Deployment的部署操作,以完成复杂的修改
3.11.4 使用kubectl rolling-update命令完成RC的滚动升级
3.11.5 其他管理对象的更新策略
3.12 Pod的扩缩容
3.12.1 手动扩缩容机制
3.12.2 自动扩缩容机制
3.13 使用StatefulSet搭建MongoDB集群
3.13.1 前提条件
3.13.2 创建StatefulSet
3.13.3 查看MongoDB集群的状态
3.13.4 StatefulSet的常见应用场景

接下来,让我们深入探索Pod的应用、配置、调度、升级及扩缩容,开始Kubernetes容器编排之旅。
本章将对Kubernetes如何发布与管理容器应用进行详细说明和示例,
主要包括Pod和容器的使用、应用配置管理、Pod的控制和调度管理、Pod的升级和回滚,以及Pod的扩缩容机制等内容。

3.1 Pod定义详解
YAML格式的Pod定义文件的完整内容如下:YAML 3.1 Pod定义完整内容
对各属性的详细说明如表3.1所示。
表3.1 对Pod定义文件模板中各属性的详细说明

3.2 Pod的基本用法
在对Pod的用法进行说明之前,有必要先对Docker容器中应用的运行要求进行说明。
在使用Docker时,可以使用docker run命令创建并启动一个容器。
而在Kubernetes系统中对长时间运行容器的要求是:其主程序需要一直在前台执行。
如果我们创建的Docker镜像的启动命令是后台执行程序,例如Linux脚本:nohup ./start.sh &
则在kubelet创建包含这个容器的Pod之后运行完该命令,即认为Pod执行结束,将立刻销毁该Pod。
如果为该Pod定义了ReplicationController,则系统会监控到该Pod已经终止,之后根据RC定义中Pod的replicas副本数量生成一个新的Pod。
一旦创建新的Pod,就在执行完启动命令后陷入无限循环的过程中。
这就是Kubernetes需要我们自己创建的Docker镜像并以一个前台命令作为启动命令的原因。
对于无法改造为前台执行的应用,也可以使用开源工具Supervisor辅助进行前台运行的功能。
Supervisor提供了一种可以同时启动多个后台应用,并保持Supervisor自身在前台执行的机制,可以满足Kubernetes对容器的启动要求。
关于Supervisor的安装和使用,请参考官网http://supervisord.org的文档说明。
接下来对Pod对容器的封装和应用进行说明。
Pod可以由1个或多个容器组合而成。
在上一节Guestbook的例子中,名为frontend的Pod只由一个容器组成,这个frontend Pod在成功启动之后,将启动1个Docker容器。
另一种场景是,当frontend和redis两个容器应用为紧耦合的关系,并组合成一个整体对外提供服务时,应将这两个容器打包为一个Pod,如图3.1所示。
配置文件frontend-localredis-pod.yaml的内容如下:图3.1 包含两个容器的Pod。
属于同一个Pod的多个容器应用之间相互访问时仅需要通过localhost就可以通信,使得这一组容器被“绑定”在了一个环境中。
在Docker容器kubeguide/guestbook-php-frontend:localredis的PHP网页中,直接通过URL地址“localhost:6379”对同属于一个Pod的redis-master进行访问。
运行kubectl create命令创建该Pod:
# kubectl create -f rontend-localredis-pod.yaml
查看已经创建的Pod:
# kubectl get pods
可以看到READY信息为2/2,表示Pod中的两个容器都成功运行了。
查看这个Pod的详细信息,可以看到两个容器的定义及创建的过程(Event事件信息):
# kubectl describe pod redis-php

3.3 静态Pod
静态Pod是由kubelet进行管理的仅存在于特定Node上的Pod。
它们不能通过API Server进行管理,无法与ReplicationController、Deployment或者DaemonSet进行关联,并且kubelet无法对它们进行健康检查。
静态Pod总是由kubelet创建的,并且总在kubelet所在的Node上运行。
创建静态Pod有两种方式:配置文件方式和HTTP方式。

1.配置文件方式
首先,需要设置kubelet的启动参数“--config”,指定kubelet需要监控的配置文件所在的目录,
kubelet会定期扫描该目录,并根据该目录下的.yaml或.json文件进行创建操作。
假设配置目录为/etc/kubelet.d/,配置启动参数为--config=/etc/kubelet.d/,然后重启kubelet服务。
在目录/etc/kubelet.d中放入static-web.yaml文件,内容如下:
apiVersion: v1
kind: Pod
metadata:
name: static-web
labels:
name: static-web
spec:
containers:
- name: static-web
image: nginx
ports:
- name: web
containerPort: 80
等待一会儿,查看本机中已经启动的容器:
# docker ps
可以看到一个Nginx容器已经被kubelet成功创建了出来。
到Master上查看Pod列表,可以看到这个static pod:
# kubectl get pods
由于静态Pod无法通过API Server直接管理,所以在Master上尝试删除这个Pod时,会使其变成Pending状态,且不会被删除。
# kubectl delete pod static-web-node1
# kubectl get pods
删除该Pod的操作只能是到其所在Node上,将其定义文件static-web.yaml从/etc/kubelet.d目录下删除。
# rm /etc/kubelet.d/static-web.yaml
# docker ps

2.HTTP方式
通过设置kubelet的启动参数“--manifest-url”,kubelet将会定期从该URL地址下载Pod的定义文件,并以.yaml或.json文件的格式进行解析,然后创建Pod。
其实现方式与配置文件方式是一致的。

3.4 Pod容器共享Volume
同一个Pod中的多个容器能够共享Pod级别的存储卷Volume。
Volume可以被定义为各种类型,多个容器各自进行挂载操作,将一个Volume挂载为容器内部需要的目录,如图3.2所示。
在下面的例子中,在Pod内包含两个容器:tomcat和busybox,在Pod级别设置Volume“app-logs”,用于tomcat向其中写日志文件,busybox读日志文件。
配置文件pod-volume-applogs.yaml的内容如下:
apiVersion: v1
kind: Pod
metadata:
name: volume-pod
spec:
containers:
- name: tomcat
image: tomcat
ports:
- containerPort: 8080
volumeMounts:
- name: app-logs
mountPath: /usr/local/tomcat/logs
- name: busybox
image: busybox
command: ["sh","-c","tail -f /logs/catalina*.log"]
volumeMounts:
- name: app-logs
mountPath: /logs
volumes:
- name: app-logs
emptyDir: {}
这里设置的Volume名为app-logs,类型为emptyDir(也可以设置为其他类型,详见第1章对Volume概念的说明),
挂载到tomcat容器内的/usr/local/tomcat/logs目录,同时挂载到logreader容器内的/logs目录。
tomcat容器在启动后会向/usr/local/tomcat/logs目录写文件,logreader容器就可以读取其中的文件了。
logreader容器的启动命令为tail -f /logs/catalina*.log,我们可以通过kubectl logs命令查看logreader容器的输出内容:
# kubectl logs volumn-pod -c busybox
这个文件为tomcat生成的日志文件/usr/local/tomcat/logs/catalina.<date>.log的内容。登录tomcat容器进行查看:
# kubectl exec -it volumn-pod -c tomcat -- ls /usr/local/tomcat/logs/catalina.yyyy-mm-dd.log
# kubectl exec -it volumn-pod -c tomcat -- tail /usr/local/tomcat/logs/catalina.yyyy-mm-dd.log

3.5 Pod的配置管理
应用部署的一个最佳实践是将应用所需的配置信息与程序进行分离,这样可以使应用程序被更好地复用,通过不同的配置也能实现更灵活的功能。
将应用打包为容器镜像后,可以通过环境变量或者外挂文件的方式在创建容器时进行配置注入,
但在大规模容器集群的环境中,对多个容器进行不同的配置将变得非常复杂。
从Kubernetes 1.2开始提供了一种统一的应用配置管理方案—ConfigMap。
本节对ConfigMap的概念和用法进行详细描述。

3.5.1 ConfigMap概述
ConfigMap供容器使用的典型用法如下:
(1)生成为容器内的环境变量。
(2)设置容器启动命令的启动参数(需设置为环境变量)。
(3)以Volume的形式挂载为容器内部的文件或目录。
ConfigMap以一个或多个key:value的形式保存在Kubernetes系统中供应用使用,
既可以用于表示一个变量的值(例如apploglevel=info),也可以用于表示一个完整配置文件的内容(例如server.xml=<?xml...>...)
可以通过YAML配置文件或者直接使用kubectl create configmap命令行的方式来创建ConfigMap。

3.5.2 创建ConfigMap资源对象
1.通过YAML配置文件方式创建
下面的例子cm-appvars.yaml描述了将几个应用所需的变量定义为ConfigMap的用法:
cm-appvars.yaml的内容如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-appvars
data:
apploglevel: info
appdatadir: /var/data
执行kubectl create命令创建该ConfigMap:
# kubectl create -f cm-appvars.yaml
查看创建好的ConfigMap:
# kubectl get configmap
# kubectl describe configmap cm-appvars
# kubectl get configmap cm-appvars -o yaml
下面的例子cm-appconfigfiles.yaml描述了将两个配置文件server.xml和logging.properties定义为ConfigMap的用法,
设置key为配置文件的别名,value则是配置文件的全部文本内容:
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-appconfigfiles
data:
key-serverxml: |
<?xml version='1.0' encoding='utf-8'?>
...
key-loggingproperties: "..."
执行kubectl create命令创建该ConfigMap:
# kubectl create -f cm-appconfigfiles.yaml
查看创建好的ConfigMap:
# kubectl get configmap cm-appconfigfiles
# kubectl describe configmap cm-appconfigfiles
查看已创建的ConfigMap的详细内容,可以看到两个配置文件的全文:
# kubectl get configmap cm-appconfigfiles -o yaml

2.通过kubectl命令行方式创建
不使用YAML文件,直接通过kubectl create configmap也可以创建ConfigMap,
可以使用参数--from-file或--from-literal指定内容,并且可以在一行命令中指定多个参数。
(1)通过--from-file参数从文件中进行创建,可以指定key的名称,也可以在一个命令行中创建包含多个key的ConfigMap,语法为:
# kubectl create configmap NAME --from-file=[key=]source --from-file=[key=]source
(2)通过--from-file参数从目录中进行创建,该目录下的每个配置文件名都被设置为key,文件的内容被设置为value,语法为:
# kubectl create configmap NAME --from-file=config-files-dir
(3)使用--from-literal时会从文本中进行创建,直接将指定的key#=value#创建为ConfigMap的内容,语法为:
# kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2
下面对这几种用法举例说明。
例如,在当前目录下含有配置文件server.xml,可以创建一个包含该文件内容的ConfigMap:
# kubectl create configmap cm-server.xml --from-file=server.xml
# kubectl describe configmap cm-server.xml
假设在configfiles目录下包含两个配置文件server.xml和logging.properties,创建一个包含这两个文件内容的ConfigMap:
# kubectl create configmap cm-appconf --from-file=configfiles
# kubectl describe configmap cm-appconf
使用--from-literal参数进行创建的示例如下:
# kubectl create configmap cm-appenv --from-literal=loglevel=info --from-literal=appdatadir=/var/data
# kubectl describe configmap cm-appenv
容器应用对ConfigMap的使用有以下两种方法。
(1)通过环境变量获取ConfigMap中的内容。
(2)通过Volume挂载的方式将ConfigMap中的内容挂载为容器内部的文件或目录。

3.5.3 在Pod中使用ConfigMap
1.通过环境变量方式使用ConfigMap
以前面创建的ConfigMap“cm-appvars”为例:
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-appvars
data:
apploglevel: info
appdatadir: /var/data
在Pod “cm-test-pod”的定义中,将ConfigMap “cm-appvars”中的内容以环境变量(APPLOGLEVEL和APPDATADIR)方式设置为容器内部的环境变量,
容器的启动命令将显示这两个环境变量的值("env | grep APP"):
apiVersion: v1
kind: Pod
metadata:
name: cm-test-pod
spec:
containers:
- name: cm-test
image: busybox
command: [ "/bin/sh","-c","env | grep APP" ]
env:
- name: APPLOGLEVEL # 定义环境变量名称
valueFrom: # key "apploglevel"对应的值
configMapKeyRef:
name: cm-appvars # 定义环境的值取自cm-appvars
key: apploglevel # key 为apploglevel
- name: APPDATADIR # 定义环境变量名称
valueFrom: # key "appdatadir"对应的值
configMapKeyRef:
name: cm-appvars # 定义环境的值取自cm-appvars
key: appdatadir # key 为appdatadir
restartPolocy: Never
使用kubectl create -f命令创建该Pod,由于是测试Pod,所以该Pod在执行完启动命令后将会退出,并且不会被系统自动重启(restartPolicy=Never):
# kubectl create -f cm-test-pod.yaml
使用kubectl get pods --show-all查看已经停止的Pod:
# kubectl get pods --show-all
查看该Pod的日志,可以看到启动命令“env | grep APP”的执行结果如下:
# kubectl logs cm-test-pod
说明容器内部的环境变量使用ConfigMap cm-appvars中的值进行了正确设置。
Kubernetes从1.6版本开始,引入了一个新的字段envFrom,
实现了在Pod环境中将ConfigMap(也可用于Secret资源对象)中所有定义的key=value自动生成为环境变量:
apiVersion: v1
kind: Pod
metadata:
name: cm-test-pod
spec:
containers:
- name: cm-test
image: busybox
command: [ "/bin/sh","-c","env" ]
envFrom:
- configMapRef:
name: cm-appvars # 定义cm的文件名
restartPolocy: Never
通过这个定义,在容器内部将会生成如下环境变量:
# kubectl logs cm-test-pod
需要说明的是,环境变量的名称受POSIX命名规范([a-zA-Z_][a-zA-Z0-9_]*)约束,不能以数字开头。
如果包含非法字符,则系统将跳过该条环境变量的创建,并记录一个Event来提示环境变量无法生成,但并不阻止Pod的启动。

2.通过volumeMount使用ConfigMap
在如下所示的cm-appconfigfiles.yaml例子中包含两个配置文件的定义:server.xml和logging.properties。
apiVersion: v1
kind: ConfigMap
metadata:
name: cm-appconfigfiles
data:
key-serverxml: |
<?xml version='1.0' encoding='utf-8'?>
...
key-loggingproperties: "..."
在Pod“cm-test-app”的定义中,将ConfigMap “cm-appconfigfiles”中的内容以文件的形式mount到容器内部的/configfiles目录下。
Pod配置文件cm-test-app.yaml的内容如下:
apiVersion: v1
kind: Pod
metadata:
name: cm-test-pod
spec:
containers:
- name: cm-test-app
image: kubeguide/tomcat-app:v1
ports:
- containerPort: 8080
volumeMounts:
- name: serverxml # 引用Volume的名称
mountPath: /configfiles # 挂载到容器内的目录
volumes:
- name: serverxml # 定义Volume的名称
configMap:
name: cm-appconfigfiles # 使用ConfigMap “cm-appconfigfiles”
items:
- key: key-serverxml # key=key-serverxml
path: server.xml # value将server.xml文件名进行挂载
- key: key-loggingproperties # key=key-loggingproperties
path: logging.properties # value将logging.properties文件名进行挂载
创建该Pod:
# kubectl create -f cm-test-app.yaml
登录容器,查看到在/configfiles目录下存在server.xml和logging.properties文件,它们的内容就是ConfigMap“cm-appconfigfiles”中两个key定义的内容:
# kubectl exec -it cm-test-app bash
# cat /configfiles/erver.xml
# cat /configfiles/logging.properties
如果在引用ConfigMap时不指定items,则使用volumeMount方式在容器内的目录下为每个item都生成一个文件名为key的文件。
Pod配置文件cm-test-app.yaml的内容如下:
apiVersion: v1
kind: Pod
metadata:
name: cm-test-pod
spec:
containers:
- name: cm-test-app
image: kubeguide/tomcat-app:v1
imagePullPolicy: Never
ports:
- containerPort: 8080
volumeMounts:
- name: serverxml # 引用Volume的名称
mountPath: /configfiles # 挂载到容器内的目录
volumes:
- name: serverxml # 定义Volume的名称
configMap:
name: cm-appconfigfiles # 使用ConfigMap “cm-appconfigfiles”
创建该Pod:
# kubectl create -f cm-test-app.yaml
登录容器,查看到在/configfiles目录下存在key-loggingproperties和key-serverxml文件,
文件的名称来自在ConfigMap cm-appconfigfiles中定义的两个key的名称,文件的内容则为value的内容:
# ls /configfiles

3.5.4 使用ConfigMap的限制条件
使用ConfigMap的限制条件如下。
ConfigMap必须在Pod之前创建。
ConfigMap受Namespace限制,只有处于相同Namespace中的Pod才可以引用它。
ConfigMap中的配额管理还未能实现。
kubelet只支持可以被API Server管理的Pod使用ConfigMap。
kubelet在本Node上通过 --manifest-url或--config自动创建的静态Pod将无法引用ConfigMap。
在Pod对ConfigMap进行挂载(volumeMount)操作时,在容器内部只能挂载为“目录”,无法挂载为“文件”。
在挂载到容器内部后,在目录下将包含ConfigMap定义的每个item,如果在该目录下原来还有其他文件,则容器内的该目录将被挂载的ConfigMap覆盖。
如果应用程序需要保留原来的其他文件,则需要进行额外的处理。
可以将ConfigMap挂载到容器内部的临时目录,再通过启动脚本将配置文件复制或者链接到(cp或link命令)应用所用的实际配置目录下。

3.6 在容器内获取Pod信息(Downward API)
我们知道,每个Pod在被成功创建出来之后,都会被系统分配唯一的名字、IP地址,并且处于某个Namespace中,
那么我们如何在Pod的容器内获取Pod的这些重要信息呢?答案就是使用Downward API。
Downward API可以通过以下两种方式将Pod信息注入容器内部。
(1)环境变量:用于单个变量,可以将Pod信息和Container信息注入容器内部。
(2)Volume挂载:将数组类信息生成为文件并挂载到容器内部。
下面通过几个例子对Downward API的用法进行说明。

3.6.1 环境变量方式:将Pod信息注入为环境变量
下面的例子通过Downward API将Pod的IP、名称和所在Namespace注入容器的环境变量中,容器应用使用env命令将全部环境变量打印到标准输出中:
dapi-test-pod.yaml内容:
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod
spec:
containers:
- name: test-container
image: busybox
command: [ "/bin/sh","-c","env" ]
env:
- name: MY_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: MY_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
restartPolicy: Never
注意到上面valueFrom这种特殊的语法是Downward API的写法。
目前Downward API提供了以下变量:
metadata.name:Pod的名称,当Pod通过RC生成时,其名称是RC随机产生的唯一名称。
status.podIP:Pod的IP地址,之所以叫作status.podIP而非metadata.IP,是因为Pod的IP属于状态数据,而非元数据。
metadata.namespace:Pod所在的Namespace。
运行kubectl create命令创建Pod:
# kubectl create -f dapi-test-pod.yaml
查看dapi-test-pod的日志:
# kubectl log dapi-test-pod
从日志中我们可以看到Pod的IP、Name及Namespace等信息都被正确保存到了Pod的环境变量中。

3.6.2 环境变量方式:将容器资源信息注入为环境变量
下面的例子通过Downward API将Container的资源请求和限制信息注入容器的环境变量中,
容器应用使用printenv命令将设置的资源请求和资源限制环境变量打印到标准输出中:
dapi-test-pod-containers-vars.yaml内容:
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod-containers-vars
spec:
containers:
- name: test-container
image: busybox
imagePullPolicy: Never
command: [ "/bin/sh","-c" ]
args:
- while ture; do
echo -en '\n';
printenv MY_CPU_REQUEST MY_CPU_LIMIT;
printenv MY_MEM_REQUEST MY_MEM_LIMIT;
sleep 3600;
done;
resources:
requests:
memory: "32Mi"
cpu: "125m"
limits:
memory: "64Mi"
cpu: "250m"
env:
- name: MY_CPU_REQUEST
valueFrom:
resourceFieldRef:
containerName: test-container
resource: requests.cpu
- name: MY_CPU_LIMIT
valueFrom:
resourceFieldRef:
containerName: test-container
resource: limits.cpu
- name: MY_MEM_REQUEST
valueFrom:
resourceFieldRef:
containerName: test-container
resource: requests.memory
- name: MY_MEM_LIMIT
valueFrom:
resourceFieldRef:
containerName: test-container
resource: limits.memory
restartPolicy: Never
注意valueFrom这种特殊的Downward API语法,目前resourceFieldRef可以将容器的资源请求和资源限制等配置设置为容器内部的环境变量。
requests.cpu:容器的CPU请求值。
limits.cpu:容器的CPU限制值。
requests.memory:容器的内存请求值。
limits.memory:容器的内存限制值。
运行kubectl create命令来创建Pod:
# kubectl create -f dapi-test-pod-container-vars.yaml
# kubectl get pods
查看dapi-test-pod-container-vars的日志:
# kubectl log -f dapi-test-pod-container-vars
从日志中我们可以看到Container的requests.cpu、limits.cpu、requests.memory、limits.memory等信息都被正确保存到了Pod的环境变量中。

3.6.3 Volume挂载方式
下面的例子通过Downward API将Pod的Label、Annotation列表通过Volume挂载为容器中的一个文件,
容器应用使用echo命令将文件的内容打印到标准输出中:
dapi-test-pod-volume.yaml内容:
apiVersion: v1
kind: Pod
metadata:
name: dapi-test-pod-volume
labels:
zone: us-est-coast
cluster: test-cluster1
rack: rack-22
annotations:
build: two
builder: john-doe
spec:
containers:
- name: test-container
image: busybox
imagePullPolicy: Never
command: [ "/bin/sh","-c" ]
args:
- while ture; do
if [[ -e /etc/labels ]]; then
echo -en '\n\n';
cat /etc/lables;
fi;
if [[ -e /etc/annotations ]]; then
echo -en '\n\n';
cat /etc/annotations;
fi;
sleep 3600;
done;
volumeMounts:
- name: podinfo
mountPath: /etc
readOnly: false
volumes:
- name: podinfo
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
这里要注意“volumes”字段中downwardAPI的特殊语法,通过items的设置,系统会根据path的名称生成文件。
根据上例的设置,系统将在容器内生成/etc/labels和/etc/annotations两个文件。
在/etc/labels文件中将包含metadata.labels的全部Label列表,在/etc/annotations文件中将包含metadata.annotations的全部Label列表。
运行kubectl create命令创建Pod:
# kubectl create -f dapi-test-pod-volume.yaml
# kubectl get pod
查看dapi-test-pod-volume的日志:
# kubectl log dapi-test-pod-volume
从日志中我们看到Pod的Label和Annotation信息都被保存到了容器内的/etc/labels和/etc/annotations文件中。
那么,Downward API有什么价值呢?
在某些集群中,集群中的每个节点都需要将自身的标识(ID)及进程绑定的IP地址等信息事先写入配置文件中,
进程在启动时会读取这些信息,然后将这些信息发布到某个类似服务注册中心的地方,以实现集群节点的自动发现功能。
此时Downward API就可以派上用场了,具体做法是先编写一个预启动脚本或Init Container,
通过环境变量或文件方式获取Pod自身的名称、IP地址等信息,然后将这些信息写入主程序的配置文件中,最后启动主程序。

3.7 Pod生命周期和重启策略
Pod在整个生命周期中被系统定义为各种状态,熟悉Pod的各种状态对于理解如何设置Pod的调度策略、重启策略是很有必要的。
Pod的状态如表3.2所示。
Pod的状态如下:
Pending:API Server已经创建该Pod,但在Pod内还有一个或多个容器的镜像没有创建,包括正在下载镜像的过程。
Running:Pod内所有容器均已创建,且至少有一个容器处于运行状态、正在启动状态或正在重启状态。
Succeeded:Pod内所有容器均成功执行后退出,且不会再重启。
Failed:Pod内所有容器均已退出,但至少有一个容器退出为失败状态。
Unknown:由于某种原因无法获取该Pod的状态,可能由于网络通信不畅导致。
Pod的重启策略(RestartPolicy)应用于Pod内的所有容器,并且仅在Pod所处的Node上由kubelet进行判断和重启操作。
当某个容器异常退出或者健康检查(详见下节)失败时,kubelet将根据RestartPolicy的设置来进行相应的操作。
Pod的重启策略包括Always、OnFailure和Never,默认值为Always。
Always:当容器失效时,由kubelet自动重启该容器。
OnFailure:当容器终止运行且退出码不为0时,由kubelet自动重启该容器。
Never:不论容器运行状态如何,kubelet都不会重启该容器。
kubelet重启失效容器的时间间隔以sync-frequency乘以2n来计算,例如1、2、4、8倍等,最长延时5min,并且在成功重启后的10min后重置该时间。
Pod的重启策略与控制方式息息相关,当前可用于管理Pod的控制器包括ReplicationController、Job、DaemonSet及直接通过kubelet管理(静态Pod)。
每种控制器对Pod的重启策略要求如下:
RC和DaemonSet:必须设置为Always,需要保证该容器持续运行。
Job:OnFailure或Never,确保容器执行完成后不再重启。
kubelet:在Pod失效时自动重启它,不论将RestartPolicy设置为什么值,也不会对Pod进行健康检查。
结合Pod的状态和重启策略,表3.3列出一些常见的状态转换场景。

3.8 Pod健康检查和服务可用性检查
Kubernetes对Pod的健康状态可以通过两类探针来检查:LivenessProbe 和ReadinessProbe,kubelet定期执行这两类探针来诊断容器的健康状况。
(1)LivenessProbe探针:用于判断容器是否存活(Running状态),
如果LivenessProbe探针探测到容器不健康,则kubelet将杀掉该容器,并根据容器的重启策略做相应的处理。
如果一个容器不包含LivenessProbe探针,那么kubelet认为该容器的LivenessProbe探针返回的值永远是Success。
(2)ReadinessProbe探针:用于判断容器服务是否可用(Ready状态),达到Ready状态的Pod才可以接收请求。
对于被Service管理的Pod,Service与Pod Endpoint的关联关系也将基于Pod是否Ready进行设置。
如果在运行过程中Ready状态变为False,则系统自动将其从Service的后端Endpoint列表中隔离出去,
后续再把恢复到Ready状态的Pod加回后端Endpoint列表。
这样就能保证客户端在访问Service时不会被转发到服务不可用的Pod实例上。
LivenessProbe和ReadinessProbe均可配置以下三种实现方式。
(1)ExecAction:在容器内部执行一个命令,如果该命令的返回码为0,则表明容器健康。
在下面的例子中,通过执行“cat /tmp/health”命令来判断一个容器运行是否正常。
在该Pod运行后,将在创建/tmp/health文件10s后删除该文件,
而LivenessProbe健康检查的初始探测时间(initialDelaySeconds)为15s,探测结果是Fail,将导致kubelet杀掉该容器并重启它:
apiVersion: v1
kind: Pod
metadata:
name: liveness-exec
labels:
test: liveness
spec:
containers:
- name: liveness
image: gcr.io/google_containers/busybox
args:
- /bin/sh
- -c
- echo ok > /tmp/health;sleep 10; rm -rf /tmp/health; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/health
initialDelaySeconds: 15
timeoutSeconds: 1
(2)TCPSocketAction:通过容器的IP地址和端口号执行TCP检查,如果能够建立TCP连接,则表明容器健康。
在下面的例子中,通过与容器内的localhost:80建立TCP连接进行健康检查:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-healthcheck
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 30
timeoutSeconds: 1
(3)HTTPGetAction:通过容器的IP地址、端口号及路径调用HTTP Get方法,如果响应的状态码大于等于200且小于400,则认为容器健康。
在下面的例子中,kubelet定时发送HTTP请求到localhost:80/_status/healthz来进行容器应用的健康检查:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-healthcheck
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /_status/healthz
port: 80
initialDelaySeconds: 30
timeoutSeconds: 1
对于每种探测方式,都需要设置initialDelaySeconds和timeoutSeconds两个参数,它们的含义分别如下。
initialDelaySeconds:启动容器后进行首次健康检查的等待时间,单位为s。
timeoutSeconds:健康检查发送请求后等待响应的超时时间,单位为s。当超时发生时,kubelet会认为容器已经无法提供服务,将会重启该容器。
Kubernetes的ReadinessProbe机制可能无法满足某些复杂应用对容器内服务可用状态的判断,
所以Kubernetes从1.11版本开始,引入Pod Ready++特性对Readiness探测机制进行扩展,
在1.14版本时达到GA稳定版,称其为Pod Readiness Gates。
通过Pod Readiness Gates机制,用户可以将自定义的ReadinessProbe探测方式设置在Pod上,辅助Kubernetes设置Pod何时达到服务可用状态(Ready)。
为了使自定义的ReadinessProbe生效,用户需要提供一个外部的控制器(Controller)来设置相应的Condition状态。
Pod的Readiness Gates在Pod定义中的ReadinessGate字段进行设置。
下面的例子设置了一个类型为www.example.com/feature-1的新Readiness Gate:
apiVersion: v1
kind: Pod
metadata:
name: pod-with-healthcheck
spec:
readinessDates:
- conditionType: "www.example.com/feature-1"
status:
conditions:
- type: Ready # kubernets系统内置的名为Ready的Condition
status: "True"
lastProbeTime: null
lastTransitionTime: 2018-01-01T00:00:00Z
- type: "www.example.com/feature-1" # 用户自定义的Condition
status: "False"
lastProbeTime: null
lastTransitionTime: 2019-03-01T00:00:00Z
containerStatuses:
- containerID: docker://abcd...
ready:true
新增的自定义Condition的状态(status)将由用户自定义的外部控制器设置,默认值为False。
Kubernetes将在判断全部readinessGates条件都为True时,才设置Pod为服务可用状态(Ready为True)。

3.9 玩转Pod调度
在Kubernetes平台上,我们很少会直接创建一个Pod,
在大多数情况下会通过RC、Deployment、DaemonSet、Job等控制器完成对一组Pod副本的创建、调度及全生命周期的自动控制任务。
在最早的Kubernetes版本里是没有这么多Pod副本控制器的,只有一个Pod副本控制器RC(Replication Controller),
这个控制器是这样设计实现的:RC独立于所控制的Pod,并通过Label标签这个松耦合关联关系控制目标Pod实例的创建和销毁,
随着Kubernetes的发展,RC也出现了新的继任者——Deployment,用于更加自动地完成Pod副本的部署、版本更新、回滚等功能。
严谨地说,RC的继任者其实并不是Deployment,而是ReplicaSet,因为ReplicaSet进一步增强了RC标签选择器的灵活性。
之前RC的标签选择器只能选择一个标签,而ReplicaSet拥有集合式的标签选择器,可以选择多个Pod标签,如下所示:
selector:
matchLabels:
tier: frontend
matchExpressions:
- {key: tier,operator: In,values: [frontend]}
与RC不同,ReplicaSet被设计成能控制多个不同标签的Pod副本。
一种常见的应用场景是,应用MyApp目前发布了v1与v2两个版本,用户希望MyApp的Pod副本数保持为3个,可以同时包含v1和v2版本的Pod,
就可以用ReplicaSet来实现这种控制,写法如下:
selector:
matchLabels:
version: v2
matchExpressions:
- {key: version,operator: In,values: [v1,v2]}
其实,Kubernetes的滚动升级就是巧妙运用ReplicaSet的这个特性来实现的,同时,Deployment也是通过ReplicaSet来实现Pod副本自动控制功能的。
我们不应该直接使用底层的ReplicaSet来控制Pod副本,而应该使用管理ReplicaSet的Deployment对象来控制副本,这是来自官方的建议。
在大多数情况下,我们希望Deployment创建的Pod副本被成功调度到集群中的任何一个可用节点,而不关心具体会调度到哪个节点。
但是,在真实的生产环境中的确也存在一种需求:希望某种Pod的副本全部在指定的一个或者一些节点上运行,
比如希望将MySQL数据库调度到一个具有SSD磁盘的目标节点上,此时Pod模板中的NodeSelector属性就开始发挥作用了,
上述MySQL定向调度案例的实现方式可分为以下两步。
(1)把具有SSD磁盘的Node都打上自定义标签“disk=ssd”。
(2)在Pod模板中设定NodeSelector的值为“disk: ssd”。
如此一来,Kubernetes在调度Pod副本的时候,就会先按照Node的标签过滤出合适的目标节点,然后选择一个最佳节点进行调度。
上述逻辑看起来既简单又完美,但在真实的生产环境中可能面临以下令人尴尬的问题:
(1)如果NodeSelector选择的Label不存在或者不符合条件,比如这些目标节点此时宕机或者资源不足,该怎么办?
(2)如果要选择多种合适的目标节点,比如SSD磁盘的节点或者超高速硬盘的节点,该怎么办?
Kubernates引入了NodeAffinity(节点亲和性设置)来解决该需求。
在真实的生产环境中还存在如下所述的特殊需求。
(1)不同Pod之间的亲和性(Affinity)。
比如MySQL数据库与Redis中间件不能被调度到同一个目标节点上,或者两种不同的Pod必须被调度到同一个Node上,
以实现本地文件共享或本地网络通信等特殊需求,这就是PodAffinity要解决的问题。
(2)有状态集群的调度。
对于ZooKeeper、Elasticsearch、MongoDB、Kafka等有状态集群,虽然集群中的每个Worker节点看起来都是相同的,
但每个Worker节点都必须有明确的、不变的唯一ID(主机名或IP地址),这些节点的启动和停止次序通常有严格的顺序。
此外,由于集群需要持久化保存状态数据,所以集群中的Worker节点对应的Pod不管在哪个Node上恢复,都需要挂载原来的Volume,
因此这些Pod还需要捆绑具体的PV。
针对这种复杂的需求,Kubernetes提供了StatefulSet这种特殊的副本控制器来解决问题,
在Kubernetes 1.9版本发布后,StatefulSet才可用于正式生产环境中。
(3)在每个Node上调度并且仅仅创建一个Pod副本。
这种调度通常用于系统监控相关的Pod,比如主机上的日志采集、主机性能采集等进程需要被部署到集群中的每个节点,
并且只能部署一个副本,这就是DaemonSet这种特殊Pod副本控制器所解决的问题。
(4)对于批处理作业,需要创建多个Pod副本来协同工作,当这些Pod副本都完成自己的任务时,整个批处理作业就结束了。
这种Pod运行且仅运行一次的特殊调度,用常规的RC或者Deployment都无法解决,
所以Kubernates引入了新的Pod调度控制器Job来解决问题,并继续延伸了定时作业的调度控制器CronJob。
与单独的Pod实例不同,由RC、ReplicaSet、Deployment、DaemonSet等控制器创建的Pod副本实例都是归属于这些控制器的,
这就产生了一个问题:控制器被删除后,归属于控制器的Pod副本该何去何从?
在Kubernates 1.9之前,在RC等对象被删除后,它们所创建的Pod副本都不会被删除;
在Kubernates 1.9以后,这些Pod副本会被一并删除。
如果不希望这样做,则可以通过kubectl命令的--cascade=false参数来取消这一默认特性:
# kubectl delete replicaset my-repset --cascade=false
接下来深入理解和实践这些Pod调度控制器的各种功能和特性。

3.9.1 Deployment或RC:全自动调度
Deployment或RC的主要功能之一就是自动部署一个容器应用的多份副本,以及持续监控副本的数量,在集群内始终维持用户指定的副本数量。
下面是一个Deployment配置的例子,使用这个配置文件可以创建一个ReplicaSet,这个ReplicaSet会创建3个Nginx应用的Pod:
nginx-deployment.yaml文件内容:
apiVersion: apps/v1
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
运行kubectl create命令创建这个Deployment:
# kubectl create -f nginx-deployment.yaml
查看Deployment的状态:
# kubectl get deployments
该状态说明Deployment已创建好所有3个副本,并且所有副本都是最新的可用的。
通过运行kubectl get rs和kubectl get pods可以查看已创建的ReplicaSet(RS)和Pod的信息。
# kubectl get rs
# kubectl get pods
从调度策略上来说,这3个Nginx Pod由系统全自动完成调度。
它们各自最终运行在哪个节点上,完全由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。
除了使用系统自动调度算法完成一组Pod的部署,Kubernetes也提供了多种丰富的调度策略,
用户只需在Pod的定义中使用NodeSelector、NodeAffinity、PodAffinity、Pod驱逐等更加细粒度的调度策略设置,就能完成对Pod的精准调度。
下面对这些策略进行说明。

3.9.2 NodeSelector:定向调度
Kubernetes Master上的Scheduler服务(kube-scheduler进程)负责实现Pod的调度,
整个调度过程通过执行一系列复杂的算法,最终为每个Pod都计算出一个最佳的目标节点,
这一过程是自动完成的,通常我们无法知道Pod最终会被调度到哪个节点上。
在实际情况下,也可能需要将Pod调度到指定的一些Node上,可以通过Node的标签(Label)和Pod的nodeSelector属性相匹配,来达到上述目的。
(1)首先通过kubectl label命令给目标Node打上一些标签:
# kubectl label nodes <node-name> <label-key>=<label-value>
这里,我们为k8s-node-1节点打上一个zone=north标签,表明它是“北方”的一个节点:
# kubectl label nodes k8s-node-1 zone=north
上述命令行操作也可以通过修改资源定义文件的方式,并执行kubectl replace -f xxx.yaml命令来完成。
(2)然后,在Pod的定义中加上nodeSelector的设置,以redis-master-controller.yaml为例:
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master
labels:
name: redis-master
spec:
replicas: 1
selector:
name: redis-master
template:
metadata:
labels:
name: redis-master
spec:
containers:
- name: master
image: kubeguide/redis-master
ports:
- containerPort: 6379
nodeSelector:
zone: north
运行kubectl create -f命令创建Pod,scheduler就会将该Pod调度到拥有zone=north标签的Node上。
使用kubectl get pods -o wide命令可以验证Pod所在的Node:
# kubectl get pods -o wide
如果我们给多个Node都定义了相同的标签(例如zone=north),则scheduler会根据调度算法从这组Node中挑选一个可用的Node进行Pod调度。
通过基于Node标签的调度方式,我们可以把集群中具有不同特点的Node都贴上不同的标签,
例如“role=frontend”“role=backend”“role=database”等标签,在部署应用时就可以根据应用的需求设置NodeSelector来进行指定Node范围的调度。
需要注意的是,如果我们指定了Pod的nodeSelector条件,且在集群中不存在包含相应标签的Node,
则即使在集群中还有其他可供使用的Node,这个Pod也无法被成功调度。
除了用户可以自行给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版本开始启用)
用户也可以使用这些系统标签进行Pod的定向调度。
NodeSelector通过标签的方式,简单实现了限制Pod所在节点的方法。
亲和性调度机制则极大扩展了Pod的调度能力,主要的增强功能如下:
更具表达力(不仅仅是“符合全部”的简单情况)。
可以使用软限制、优先采用等限制方式,代替之前的硬限制,这样调度器在无法满足优先需求的情况下,会退而求其次,继续运行该Pod。
可以依据节点上正在运行的其他Pod的标签来进行限制,而非节点本身的标签。这样就可以定义一种规则来描述Pod之间的亲和或互斥关系。
亲和性调度功能包括节点亲和性(NodeAffinity)和Pod亲和性(PodAffinity)两个维度的设置。
节点亲和性与NodeSelector类似,增强了上述前两点优势;
Pod的亲和与互斥限制则通过Pod标签而不是节点标签来实现,也就是上面第4点内容所陈述的方式,同时具有前两点提到的优点。
NodeSelector将会继续使用,随着节点亲和性越来越能够表达nodeSelector的功能,最终NodeSelector会被废弃。

3.9.3 NodeAffinity:Node亲和性调度
NodeAffinity意为Node亲和性的调度策略,是用于替换NodeSelector的全新调度策略。
目前有两种节点亲和性表达:
RequiredDuringSchedulingIgnoredDuringExecution:必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于硬限制。
PreferredDuringSchedulingIgnoredDuringExecution:强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制。
多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序。
IgnoredDuringExecution的意思是:如果一个Pod所在的节点在Pod运行期间标签发生了变更,不再符合该Pod的节点亲和性需求,
则系统将忽略Node上Label的变化,该Pod能继续在该节点运行。
下面的例子设置了NodeAffinity调度的如下规则。
requiredDuringSchedulingIgnoredDuringExecution要求只运行在amd64的节点上(beta.kubernetes.io/arch In amd64)。
preferredDuringSchedulingIgnoredDuringExecution的要求是尽量运行在磁盘类型为ssd(disk-type In ssd)的节点上。
代码如下:
apiVersion: v1
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就可以实现排斥的功能了。
NodeAffinity规则设置的注意事项如下:
如果同时定义了nodeSelector和nodeAffinity,那么必须两个条件都得到满足,Pod才能最终运行在指定的Node上。
如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能够匹配成功即可。
如果在nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。

3.9.4 PodAffinity:Pod亲和与互斥调度策略
Pod间的亲和与互斥从Kubernetes 1.4版本开始引入。
这一功能让用户从另一个角度来限制Pod所能运行的节点:根据在节点上正在运行的Pod的标签而不是节点的标签进行判断和调度,要求对节点和Pod两个条件进行匹配。
这种规则可以描述为:如果在具有标签X的Node上运行了一个或者多个符合条件Y的Pod,那么Pod应该(如果是互斥的情况,那么就变成拒绝)运行在这个Node上。
这里X指的是一个集群中的节点、机架、区域等概念,通过Kubernetes内置节点标签中的key来进行声明。
这个key的名字为topologyKey,意为表达节点所属的topology范围。
kubernetes.io/hostname
failure-domain.beta.kubernetes.io/zone
failure-domain.beta.kubernetes.io/region
与节点不同的是,Pod是属于某个命名空间的,所以条件Y表达的是一个或者全部命名空间中的一个Label Selector。
和节点亲和相同,Pod亲和与互斥的条件设置也是requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution。
Pod的亲和性被定义于PodSpec的affinity字段下的podAffinity子字段中。Pod间的互斥性则被定义于同一层次的podAntiAffinity子字段中。
下面通过实例来说明Pod间的亲和性和互斥性策略设置。
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
2.Pod的亲和性调度
下面创建第2个Pod来说明Pod的亲和性调度,这里定义的亲和标签是security=S1,对应上面的Pod“pod-flag”,topologyKey的值被设置为“kubernetes.io/hostname”:
apiVersion: v1
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了。

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:failure-domain.beta. kubernetes.io/zone
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- nginx
topologyKey:kubernetes.io/zone
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上。
与节点亲和性类似,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及failuredomain.beta.kubernetes.io/region的组合。
如果不是上述情况,就可以采用任意合法的topologyKey了。
PodAffinity规则设置的注意事项如下:
除了设置Label Selector和topologyKey,用户还可以指定Namespace列表来进行限制,同样,使用Label Selector对Namespace进行选择。
Namespace的定义和Label Selector及topologyKey同级。省略Namespace的设置,表示使用定义了affinity/anti-affinity的Pod所在的Namespace。
如果Namespace被设置为空值(""),则表示所有Namespace。
在所有关联requiredDuringSchedulingIgnoredDuringExecution的matchExpressions全都满足之后,系统才能将Pod调度到某个Node上。
关于Pod亲和性和互斥性调度的更多信息可以参考其设计文档,网址为https://github.com/ kubernetes/kubernetes/blob/master/docs/design/podaffinity.md。

3.9.5 Taints和Tolerations(污点和容忍)
前面介绍的NodeAffinity节点亲和性,是在Pod上定义的一种属性,使得Pod能够被调度到某些Node上运行(优先选择或强制要求)。
Taint则正好相反,它让Node拒绝Pod的运行。
Taint需要和Toleration配合使用,让Pod避开那些不合适的Node。在Node上设置一个或多个Taint之后,除非Pod明确声明能够容忍这些污点,否则无法在这些Node上运行。
Toleration是Pod的属性,让Pod能够(注意,只是能够,而非必须)运行在标注了Taint的Node上。
可以用kubectl taint命令为Node设置Taint信息:
# kubectl taint nodes node1 key=value:NoSchedule
这个设置为node1加上了一个Taint,该Taint的键为key,值为value,Taint的效果是NoSchedule。
这意味着除非Pod明确声明可以容忍这个Taint,否则就不会被调度到node1上。
然后,需要在Pod上声明Toleration。下面的两个Toleration都被设置为可以容忍(Tolerate)具有该Taint的Node,使得Pod能够被调度到node1上:
tolerations:
- key: "key"
operator: "Equal"
value: "value"
effect: "NoSchedule"

tolerations:
- key: "key"
operator: "Exists"
effect: "NoSchedule"
Pod的Toleration声明中的key和effect需要与Taint的设置保持一致,并且满足以下条件之一。
operator的值是Exists(无须指定value)。
operator的值是Equal并且value相等。
如果不指定operator,则默认值为Equal。
另外,有如下两个特例。
空的key配合Exists操作符能够匹配所有的键和值。
空的effect匹配所有的effect。
在上面的例子中,effect的取值为NoSchedule,还可以取值为PreferNoSchedule,这个值的意思是优先,也可以算作NoSchedule的软限制版本—
一个Pod如果没有声明容忍这个Taint,则系统会尽量避免把这个Pod调度到这一节点上,但不是强制的。
后面还会介绍另一个effect “NoExecute”。
系统允许在同一个Node上设置多个Taint,也可以在Pod上设置多个Toleration。
Kubernetes调度器处理多个Taint和Toleration的逻辑顺序为:
首先列出节点中所有的Taint,然后忽略Pod的Toleration能够匹配的部分,剩下的没有忽略的Taint就是对Pod的效果了。
下面是几种特殊情况:
如果在剩余的Taint中存在effect=NoSchedule,则调度器不会把该Pod调度到这一节点上。
如果在剩余的Taint中没有NoSchedule效果,但是有PreferNoSchedule效果,则调度器会尝试不把这个Pod指派给这个节点。
如果在剩余的Taint中有NoExecute效果,并且这个Pod已经在该节点上运行,则会被驱逐;如果没有在该节点上运行,则也不会再被调度到该节点上。
例如,我们这样对一个节点进行Taint设置:
# kubectl taint nodes node1 key1=value1:NoSchedule
# kubectl taint nodes node1 key1=value1:NoExecute
# kubectl taint nodes node1 key2=value2:NoSchedule
然后在Pod上设置两个Toleration:
tolerations:
- key: "key1"
operator: "Equal"
value: "value1"
effect: "NoSchedule"
- key: "key1"
operator: "Equal"
value: "value1"
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: "value1"
effect: "NoExecute"
olerationSeconds: 3600
上述定义的意思是,如果Pod正在运行,所在节点都被加入一个匹配的Taint,则这个Pod会持续在这个节点上存活3600s后被逐出。
如果在这个宽限期内Taint被移除,则不会触发驱逐事件。
Taint和Toleration是一种处理节点并且让Pod进行规避或者驱逐Pod的弹性处理方式,下面列举一些常见的用例。

1.独占节点
如果想要拿出一部分节点专门给一些特定应用使用,则可以为节点添加这样的Taint:
# kubectl taint nodes nodename dedicated=groupName:NoSchedule
然后给这些应用的Pod加入对应的Toleration。这样,带有合适Toleration的Pod就会被允许同使用其他节点一样使用有Taint的节点。
通过自定义Admission Controller也可以实现这一目标。
如果希望让这些应用独占一批节点,并且确保它们只能使用这些节点,则还可以给这些Taint节点加入类似的标签dedicated=groupName,
然后Admission Controller需要加入节点亲和性设置,要求Pod只会被调度到具有这一标签的节点上。

2.具有特殊硬件设备的节点
在集群里可能有一小部分节点安装了特殊的硬件设备(如GPU芯片),
用户自然会希望把不需要占用这类硬件的Pod排除在外,以确保对这类硬件有需求的Pod能够被顺利调度到这些节点。
可以用下面的命令为节点设置Taint:
# kubectl taint nodes nodename special=true:NoSchedule
# kubectl taint nodes nodename special=true:PerferNoSchedule
然后在Pod中利用对应的Toleration来保障特定的Pod能够使用特定的硬件。
和上面的独占节点的示例类似,使用Admission Controller来完成这一任务会更方便。
例如,Admission Controller使用Pod的一些特征来判断这些Pod,如果可以使用这些硬件,就添加Toleration来完成这一工作。
要保障需要使用特殊硬件的Pod只被调度到安装这些硬件的节点上,则还需要一些额外的工作,
比如将这些特殊资源使用opaque-int-resource的方式对自定义资源进行量化,然后在PodSpec中进行请求;
也可以使用标签的方式来标注这些安装有特别硬件的节点,然后在Pod中定义节点亲和性来实现这个目标。

3.定义Pod驱逐行为,以应对节点故障(为Alpha版本的功能)
前面提到的NoExecute这个Taint效果对节点上正在运行的Pod有以下影响。
没有设置Toleration的Pod会被立刻驱逐。
配置了对应Toleration的Pod,如果没有为tolerationSeconds赋值,则会一直留在这一节点中。
配置了对应Toleration的Pod且指定了tolerationSeconds值,则会在指定时间后驱逐。
Kubernetes从1.6版本开始引入一个Alpha版本的功能,即把节点故障标记为Taint
(目前只针对node unreachable及node not ready,相应的NodeCondition "Ready"的值分别为Unknown和False)。
激活TaintBasedEvictions功能后(在--feature-gates参数中加入TaintBasedEvictions=true),NodeController会自动为Node设置Taint,
而在状态为Ready的Node上,之前设置过的普通驱逐逻辑将会被禁用。
注意,在节点故障的情况下,为了保持现存的Pod驱逐的限速(rate-limiting)设置,
系统将会以限速的模式逐步给Node设置Taint,这就能避免在一些特定情况下(比如Master暂时失联)大量的Pod被驱逐。
这一功能兼容于tolerationSeconds,允许Pod定义节点故障时持续多久才被逐出。
例如,一个包含很多本地状态的应用可能需要在网络发生故障时,还能持续在节点上运行,期望网络能够快速恢复,从而避免被从这个节点上驱逐。
Pod的Toleration可以这样定义:
tolerations:
- key: "node.alpha.kubernetses.io/unreachable"
operator: "Exists"
effect: "NoExecute"
olerationSeconds: 6000
对于Node未就绪状态,可以把Key设置为node.alpha.kubernetes.io/notReady。
如果没有为Pod指定node.alpha.kubernetes.io/notReady的Toleration,
那么Kubernetes会自动为Pod加入tolerationSeconds=300的node.alpha.kubernetes.io/notReady类型的Toleration。
同样,如果Pod没有定义node.alpha.kubernetes.io/unreachable的Toleration,
那么系统会自动为其加入tolerationSeconds=300的node.alpha.kubernetes.io/unreachable类型的Toleration。
这些系统自动设置的toleration在Node发现问题时,能够为Pod确保驱逐前再运行5min。
这两个默认的Toleration由Admission Controller“DefaultTolerationSeconds”自动加入。

3.9.6 Pod Priority Preemption:Pod优先级调度
对于运行各种负载(如Service、Job)的中等规模或者大规模的集群来说,出于各种原因,我们需要尽可能提高集群的资源利用率。
而提高资源利用率的常规做法是采用优先级方案,即不同类型的负载对应不同的优先级,
同时允许集群中的所有负载所需的资源总量超过集群可提供的资源,
在这种情况下,当发生资源不足的情况时,系统可以选择释放一些不重要的负载(优先级最低的),保障最重要的负载能够获取足够的资源稳定运行。
在Kubernetes 1.8版本之前,当集群的可用资源不足时,在用户提交新的Pod创建请求后,该Pod会一直处于Pending状态,
即使这个Pod是一个很重要(很有身份)的Pod,也只能被动等待其他Pod被删除并释放资源,才能有机会被调度成功。
Kubernetes 1.8版本引入了基于Pod优先级抢占(Pod Priority Preemption)的调度策略,
此时Kubernetes会尝试释放目标节点上低优先级的Pod,以腾出空间(资源)安置高优先级的Pod,这种调度方式被称为“抢占式调度”。
在Kubernetes 1.11版本中,该特性升级为Beta版本,默认开启,在后继的Kubernetes 1.14版本中正式Release。
如何声明一个负载相对其他负载“更重要”?
我们可以通过以下几个维度来定义:
Priority,优先级;
QoS,服务质量等级;
系统定义的其他度量指标。
优先级抢占调度策略的核心行为分别是驱逐(Eviction)与抢占(Preemption),这两种行为的使用场景不同,效果相同。
Eviction是kubelet进程的行为,即当一个Node发生资源不足(under resource pressure)的情况时,该节点上的kubelet进程会执行驱逐动作,
此时Kubelet会综合考虑Pod的优先级、资源申请量与实际使用量等信息来计算哪些Pod需要被驱逐;
当同样优先级的Pod需要被驱逐时,实际使用的资源量超过申请量最大倍数的高耗能Pod会被首先驱逐。
对于QoS等级为“Best Effort”的Pod来说,由于没有定义资源申请(CPU/Memory Request),所以它们实际使用的资源可能非常大。
Preemption则是Scheduler执行的行为,当一个新的Pod因为资源无法满足而不能被调度时,
Scheduler可能(有权决定)选择驱逐部分低优先级的Pod实例来满足此Pod的调度目标,这就是Preemption机制。
需要注意的是,Scheduler可能会驱逐Node A上的一个Pod以满足Node B上的一个新Pod的调度任务。
比如下面的这个例子:
一个低优先级的Pod A在Node A(属于机架R)上运行,此时有一个高优先级的Pod B等待调度,目标节点是同属机架R的Node B,
他们中的一个或全部都定义了anti-affinity规则,不允许在同一个机架上运行,此时Scheduler只好“丢车保帅”,驱逐低优先级的Pod A以满足高优先级的Pod B的调度。
Pod优先级调度示例如下:
首先,由集群管理员创建PriorityClasses,PriorityClass不属于任何命名空间:
apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClasses
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority calss should be used for XYZ service pods only."
上述YAML文件定义了一个名为high-priority的优先级类别,优先级为100000,数字越大,优先级越高,
超过一亿的数字被系统保留,用于指派给系统组件。
我们可以在任意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字段会记录目标节点N的名称。
需要注意,高优先级Pod仍然无法保证最终被调度到节点N上,在节点N上低优先级Pod被驱逐的过程中,如果有新的节点满足高优先级Pod的需求,就会把它调度到新的Node上。
而如果在等待低优先级的Pod退出的过程中,又出现了优先级更高的Pod,调度器将会调度这个更高优先级的Pod到节点N上,并重新调度之前等待的高优先级Pod。
优先级抢占的调度方式可能会导致调度陷入“死循环”状态。
当Kubernetes集群配置了多个调度器(Scheduler)时,这一行为可能就会发生,比如下面这个例子:
Scheduler A为了调度一个(批)Pod,特地驱逐了一些Pod,因此在集群中有了空余的空间可以用来调度,
此时Scheduler B恰好抢在Scheduler A之前调度了一个新的Pod,消耗了相应的资源,
因此,当Scheduler A清理完资源后正式发起Pod的调度时,却发现资源不足,被目标节点的kubelet进程拒绝了调度请求!
这种情况的确无解,因此最好的做法是让多个Scheduler相互协作来共同实现一个目标。
最后要指出一点:使用优先级抢占的调度策略可能会导致某些Pod永远无法被成功调度。
因此优先级调度不但增加了系统的复杂性,还可能带来额外不稳定的因素。
因此,一旦发生资源紧张的局面,首先要考虑的是集群扩容,如果无法扩容,则再考虑有监管的优先级调度特性,
比如结合基于Namespace的资源配额限制来约束任意优先级抢占行为。

3.9.7 DaemonSet:在每个Node上都调度一个Pod
DaemonSet是Kubernetes 1.2版本新增的一种资源对象,用于管理在集群中每个Node上仅运行一份Pod的副本实例,如图3.3所示。
这种用法适合有这种需求的应用。
在每个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范围进行调度。
下面的例子定义为在每个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和Pod,可以看到在每个Node上都创建了一个Pod:
# kubectl get daemonset --namespace=kube-system
# kubectl get pods --namespace=kube-system
在Kubernetes 1.6以后的版本中,DaemonSet也能执行滚动升级了,
即在更新一个DaemonSet模板的时候,旧的Pod副本会被自动删除,同时新的Pod副本会被自动创建,
此时DaemonSet的更新策略(updateStrategy)为RollingUpdate,如下所示:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: goldpinger
spec:
updateStrategy:
type: RollingUpdate
updateStrategy的另外一个值是OnDelete,即只有手工删除了DaemonSet创建的Pod副本,新的Pod副本才会被创建出来。
如果不设置updateStrategy的值,则在Kubernetes 1.6之后的版本中会被默认设置为RollingUpdate。

3.9.8 Job:批处理调度
Kubernetes从1.2版本开始支持批处理类型的应用,我们可以通过Kubernetes Job资源对象来定义并启动一个批处理任务。
批处理任务通常并行(或者串行)启动多个计算进程去处理一批工作项(work item),处理完成后,整个批处理任务结束。
按照批处理任务实现方式的不同,批处理任务可以分为如图3.4所示的几种模式。
Job Template Expansion模式:一个Job对象对应一个待处理的Work item,有几个Work item就产生几个独立的Job,
通常适合Work item数量少、每个Work item要处理的数据量比较大的场景,
比如有一个100GB的文件作为一个Work item,总共有10个文件需要处理。
Queue with Pod Per Work Item模式:采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,
在这种模式下,Job会启动N个Pod,每个Pod都对应一个Work item。
Queue with Variable Pod Count模式:也是采用一个任务队列存放Work item,一个Job对象作为消费者去完成这些Work item,
但与上面的模式不同,Job启动的Pod数量是可变的。
还有一种被称为Single Job with Static Work Assignment的模式,也是一个Job产生多个Pod,但它采用程序静态方式分配任务项,而不是采用队列模式进行动态分配。
如表3.4所示是这几种模式的一个对比。
考虑到批处理的并行问题,Kubernetes将Job分以下三种类型。
1.Non-parallel Jobs
通常一个Job只启动一个Pod,除非Pod异常,才会重启该Pod,一旦此Pod正常结束,Job将结束。
2.Parallel Jobs with a fixed completion count
并行Job会启动多个Pod,此时需要设定Job的.spec.completions参数为一个正数,当正常结束的Pod数量达至此参数设定的值后,Job结束。
此外,Job的.spec.parallelism参数用来控制并行度,即同时启动几个Job来处理Work Item。
3.Parallel Jobs with a work queue
任务队列方式的并行Job需要一个独立的Queue,Work item都在一个Queue中存放,不能设置Job的.spec.completions参数,此时Job有以下特性。
每个Pod都能独立判断和决定是否还有任务项需要处理。
如果某个Pod正常结束,则Job不会再启动新的Pod。
如果一个Pod成功结束,则此时应该不存在其他Pod还在工作的情况,它们应该都处于即将结束、退出的状态。
如果所有Pod都结束了,且至少有一个Pod成功结束,则整个Job成功结束。
下面分别讲解常见的三种批处理模型在Kubernetes中的应用例子。
例子1:
首先是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:
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 -l
# kubectl create -f jobs
首先,观察Job的运行情况:
# kubectl get jobs -l jobgroup=jobexample

例子2:
其次,我们看看Queue with Pod Per Work Item模式,在这种模式下需要一个任务队列存放Work item,比如RabbitMQ,
客户端程序先把要处理的任务变成Work item放入任务队列,然后编写Worker程序、打包镜像并定义成为Job中的Work Pod。
Worker程序的实现逻辑是从任务队列中拉取一个Work item并处理,在处理完成后即结束进程。
并行度为2的Demo示意图如图3.5所示。

例子3:
最后,我们看看Queue with Variable Pod Count模式,如图3.6所示。
由于这种模式下,Worker程序需要知道队列中是否还有等待处理的Work item,如果有就取出来处理,
否则就认为所有工作完成并结束进程,所以任务队列通常要采用Redis或者数据库来实现。

3.9.9 Cronjob:定时任务
Kubernetes从1.5版本开始增加了一种新类型的Job,即类似Linux Cron的定时任务Cron Job,下面看看如何定义和使用这种类型的Job。
首先,确保Kubernetes的版本为1.8及以上。
其次,需要掌握Cron Job的定时表达式,它基本上照搬了Linux Cron的表达式,区别是第1位是分钟而不是秒,格式如下:
Minutes Hours DayofMonth Month DayofWeek Year
其中每个域都可出现的字符如下。
Minutes:可出现“,” “-” “*” “/”这4个字符,有效范围为0~59的整数。
Hours:可出现“,” “-” “*” “/”这4个字符,有效范围为0~23的整数。
DayofMonth:可出现“,” “-” “*” “/” “?” “L” “W” “C”这8个字符,有效范围为0~31的整数。
Month:可出现“,” “-” “*” “/”这4个字符,有效范围为1~12的整数或JAN~DEC。
DayofWeek:可出现““,” “-” “*” “/” “?” “L” “C” “#”这8个字符,有效范围为1~7的整数或SUN~SAT。1表示星期天,2表示星期一,以此类推。
表达式中的特殊字符“*”与“/”的含义如下。
*:表示匹配该域的任意值,假如在Minutes域使用“*”,则表示每分钟都会触发事件。
/:表示从起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域设置为5/20,
则意味着第1次触发在第5min时,接下来每20min触发一次,将在第25min、第45min等时刻分别触发。
比如,我们要每隔1min执行一次任务,则Cron表达式如下:
*/1 * * * *
掌握这些基本知识后,就可以编写一个Cron Job的配置文件(cron.yaml)了:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
spec:
dchedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- bin/sh
- -c
- date;echo Hello from the Kuberbets cluster
restartPolicy: OnFailure
该例子定义了一个名为hello的Cron Job,任务每隔1min执行一次,
运行的镜像是busybox,执行的命令是Shell脚本,脚本执行时会在控制台输出当前时间和字符串“Hello from the Kubernetes cluster”。
接下来执行kubectl create命令完成创建:
# kubectl create -f cron.yaml
然后每隔1min执行kubectl get cronjob hello查看任务状态,发现的确每分钟调度了一次:
# kubectl get cronjob hello
还可以通过查找Cron Job对应的容器,验证每隔1min产生一个容器的事实,如下所示:
# docker ps -a | grep busybox
查看任意一个容器的日志,结果如下:
# docker log pod-name
运行下面的命令,可以更直观地了解Cron Job定期触发任务执行的历史和现状:
# kubectl get jobs --watch
其中SUCCESSFUL列为1的每一行都是一个调度成功的Job,以第1行的“hello-1498761060”的Job为例,它对应的Pod可以通过下面的方式得到:
# kubectl get pods --show-all | grep hello-1498761060
查看该Pod的日志:
# kubectl logs hello-1498761060-shpwx
最后,当不需要某个Cron Job时,可以通过下面的命令删除它:
# kubectl delete cronjob hello
在Kubernetes 1.9版本后,kubectrl命令增加了别名cj来表示cronjob,同时kubectl set image/env命令也可以作用在CronJob对象上了。

3.9.10 自定义调度器
如果Kubernetes调度器的众多特性还无法满足我们的独特调度需求,则还可以用自己开发的调度器进行调度。
从1.6版本开始,Kubernetes的多调度器特性也进入了快速发展阶段。
一般情况下,每个新Pod都会由默认的调度器进行调度。
但是如果在Pod中提供了自定义的调度器名称,那么默认的调度器会忽略该Pod,转由指定的调度器完成Pod的调度。
在下面的例子中为Pod指定了一个名为my-scheduler的自定义调度器:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
schedulerName: my-scheduler
containers:
- name: nginx
image: nginx
如果自定义的调度器还未在系统中部署,则默认的调度器会忽略这个Pod,这个Pod将会永远处于Pending状态。
下面看看如何创建一个自定义的调度器。
可以用任何语言来实现简单或复杂的自定义调度器。
下面的简单例子使用Bash脚本进行实现,调度策略为随机选择一个Node(注意,这个调度器需要通过kubectl proxy来运行):
#! /bin.bash
SERVER='localhost:localhost: 8081'
while true;
...
一旦这个自定义调度器成功启动,前面的Pod就会被正确调度到某个Node上。

3.10 Init Container(初始化容器)
在很多应用场景中,应用在启动之前都需要进行如下初始化操作。
等待其他关联组件正确运行(例如数据库或某个后台服务)。
基于环境变量或配置模板生成配置文件。
从远程数据库获取本地所需配置,或者将自身注册到某个中央数据库中。
下载相关依赖包,或者对系统进行一些预配置操作。
Kubernetes 1.3引入了一个Alpha版本的新特性init container(初始化容器,在Kubernetes 1.5时被更新为Beta版本),
用于在启动应用容器(app container)之前启动一个或多个初始化容器,完成应用容器所需的预置条件,如图3.7所示。
init container与应用容器在本质上是一样的,但它们是仅运行一次就结束的任务,并且必须在成功执行完成后,系统才能继续执行下一个容器。
根据Pod的重启策略(RestartPolicy),当init container执行失败,而且设置了RestartPolicy=Never时,Pod将会启动失败;
而设置RestartPolicy=Always时,Pod将会被系统自动重启。
下面以Nginx应用为例,在启动Nginx之前,通过初始化容器busybox为Nginx创建一个index.html主页文件。
这里为init container和Nginx设置了一个共享的Volume,以供Nginx访问init container设置的index.html文件(nginx-init-containers.yaml):
apiVersion: v1
kind: Pod
metadata:
name: nginx
annotations:
spec:
initContainers:
- name: install
image: busybox
command:
- wget
- "-0"
- "/work-dir/index.html"
- http://kubernetes.io
volumeMounts:
- name: workdir
mountPath: "/work-dir"
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: workdir
mountPath: /usr/share/nginx/html
dnsPolicy: Default
volumes:
- name: workdir
emptyDir: {}
创建这个Pod:
# kubectl create -f nginx-init-containers.yaml
在运行init container的过程中查看Pod的状态,可见init过程还未完成:
# kubectl get pods
在init container成功执行完成后,系统继续启动Nginx容器,再次查看Pod的状态:
# kubectl get pods
查看Pod的事件,可以看到系统首先创建并运行init container容器(名为install),成功后继续创建和运行Nginx容器:
# kubectl describe pod nginx
启动成功后,登录进Nginx容器,可以看到/usr/share/nginx/html目录下的index.html文件为init container所生成。
init container与应用容器的区别如下:
(1)init container的运行方式与应用容器不同,它们必须先于应用容器执行完成,
当设置了多个init container时,将按顺序逐个运行,并且只有前一个init container运行成功后才能运行后一个init container。
当所有init container都成功运行后,Kubernetes才会初始化Pod的各种信息,并开始创建和运行应用容器。
(2)在init container的定义中也可以设置资源限制、Volume的使用和安全策略,等等。但资源限制的设置与应用容器略有不同。
如果多个init container都定义了资源请求/资源限制,则取最大的值作为所有init container的资源请求值/资源限制值。
Pod的有效(effective)资源请求值/资源限制值取以下二者中的较大值。
a)所有应用容器的资源请求值/资源限制值之和。
b)init container的有效资源请求值/资源限制值。
调度算法将基于Pod的有效资源请求值/资源限制值进行计算,也就是说init container可以为初始化操作预留系统资源,即使后续应用容器无须使用这些资源。
Pod的有效QoS等级适用于init container和应用容器。
资源配额和限制将根据Pod的有效资源请求值/资源限制值计算生效。
Pod级别的cgroup将基于Pod的有效资源请求/限制,与调度机制一致。
(3)init container不能设置readinessProbe探针,因为必须在它们成功运行后才能继续运行在Pod中定义的普通容器。
在Pod重新启动时,init container将会重新运行,常见的Pod重启场景如下:
init container的镜像被更新时,init container将会重新运行,导致Pod重启。仅更新应用容器的镜像只会使得应用容器被重启。
Pod的infrastructure容器更新时,Pod将会重启。
若Pod中的所有应用容器都终止了,并且RestartPolicy=Always,则Pod会重启。

3.11 Pod的升级和回滚
下面说说Pod的升级和回滚问题。
当集群中的某个服务需要升级时,我们需要停止目前与该服务相关的所有Pod,然后下载新版本镜像并创建新的Pod。
如果集群规模比较大,则这个工作变成了一个挑战,而且先全部停止然后逐步升级的方式会导致较长时间的服务不可用。
Kubernetes提供了滚动升级功能来解决上述问题。
如果Pod是通过Deployment创建的,则用户可以在运行时修改Deployment的Pod定义(spec.template)或镜像名称,
并应用到Deployment对象上,系统即可完成Deployment的自动更新操作。
如果在更新过程中发生了错误,则还可以通过回滚操作恢复Pod的版本。

3.11.1 Deployment的升级
以Deployment nginx为例nginx-deployment.yaml:
apiVersion: apps/v1
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
已运行的Pod副本数量有3个:
# kubectl get pods
现在Pod镜像需要被更新为Nginx:1.9.1,我们可以通过kubectl set image命令为Deployment设置新的镜像名称:
# kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
另一种更新的方法是使用kubectl edit命令修改Deployment的配置,
将spec.template.spec.containers[0].image从Nginx:1.7.9更改为Nginx:1.9.1:
# kubectl edit deployment/nginx-deployment
一旦镜像名(或Pod定义)发生了修改,则将触发系统完成Deployment所有运行Pod的滚动升级操作。
可以使用kubectl rollout status命令查看Deployment的更新过程:
# kubectl rollout status deployment/nginx-deployment
查看当前运行的Pod,名称已经更新了:
# kubectl get pods
查看Pod使用的镜像,已经更新为Nginx:1.9.1了:
# kubectl describe pod/nginx-deployment-xxxx
那么,Deployment是如何完成Pod更新的呢?
我们可以使用kubectl describe deployments/nginx-deployment命令仔细观察Deployment的更新过程。
初始创建Deployment时,系统创建了一个ReplicaSet(nginx-deployment-4087004473),并按用户的需求创建了3个Pod副本。
当更新Deployment时,系统创建了一个新的ReplicaSet(nginx-deployment-3599678771),并将其副本数量扩展到1,然后将旧的ReplicaSet缩减为2。
之后,系统继续按照相同的更新策略对新旧两个ReplicaSet进行逐个调整。
最后,新的ReplicaSet运行了3个新版本Pod副本,旧的ReplicaSet副本数量则缩减为0。如图3.8所示。
下面列出Deployment nginx-deployment的详细事件信息:
# kubectl describe deployments/nginx-deployment
运行kubectl get rs命令,查看两个ReplicaSet的最终状态:
# kubectl get rs
在整个升级的过程中,系统会保证至少有两个Pod可用,并且最多同时运行4个Pod,这是Deployment通过复杂的算法完成的。
Deployment需要确保在整个更新过程中只有一定数量的Pod可能处于不可用状态。
在默认情况下,Deployment确保可用的Pod总数至少为所需的副本数量(DESIRED)减1,也就是最多1个不可用(maxUnavailable=1)。
Deployment还需要确保在整个更新过程中Pod的总数量不会超过所需的副本数量太多。
在默认情况下,Deployment确保Pod的总数最多比所需的Pod数多1个,也就是最多1个浪涌值(maxSurge=1)。
Kubernetes从1.6版本开始,maxUnavailable和maxSurge的默认值将从1、1更新为所需副本数量的25%、25%。
这样,在升级过程中,Deployment就能够保证服务不中断,并且副本数量始终维持为用户指定的数量(DESIRED)。
对更新策略的说明如下:
在Deployment的定义中,可以通过spec.strategy指定Pod更新的策略,目前支持两种策略:Recreate(重建)和RollingUpdate(滚动更新),
默认值为RollingUpdate。在前面的例子中使用的就是RollingUpdate策略。
Recreate:设置spec.strategy.type=Recreate,表示Deployment在更新Pod时,会先杀掉所有正在运行的Pod,然后创建新的Pod。
RollingUpdate:设置spec.strategy.type=RollingUpdate,表示Deployment会以滚动更新的方式来逐个更新Pod。
同时,可以通过设置spec.strategy.rollingUpdate下的两个参数(maxUnavailable和maxSurge)来控制滚动更新的过程。
下面对滚动更新时两个主要参数的说明如下:
spec.strategy.rollingUpdate.maxUnavailable:用于指定Deployment在更新过程中不可用状态的Pod数量的上限。
该maxUnavailable的数值可以是绝对值(例如5)或Pod期望的副本数的百分比(例如10%),
如果被设置为百分比,那么系统会先以向下取整的方式计算出绝对值(整数)。
而当另一个参数maxSurge被设置为0时,maxUnavailable则必须被设置为绝对数值大于0(从Kubernetes 1.6开始,maxUnavailable的默认值从1改为25%)。
举例来说,当maxUnavailable被设置为30%时,旧的ReplicaSet可以在滚动更新开始时立即将副本数缩小到所需副本总数的70%。
一旦新的Pod创建并准备好,旧的ReplicaSet会进一步缩容,新的ReplicaSet又继续扩容,
整个过程中系统在任意时刻都可以确保可用状态的Pod总数至少占Pod期望副本总数的70%。
spec.strategy.rollingUpdate.maxSurge:用于指定在Deployment更新Pod的过程中Pod总数超过Pod期望副本数部分的最大值。
该maxSurge的数值可以是绝对值(例如5)或Pod期望副本数的百分比(例如10%)。
如果设置为百分比,那么系统会先按照向上取整的方式计算出绝对数值(整数)。从Kubernetes 1.6开始,maxSurge的默认值从1改为25%。
举例来说,当maxSurge的值被设置为30%时,新的ReplicaSet可以在滚动更新开始时立即进行副本数扩容,
只需要保证新旧ReplicaSet的Pod副本数之和不超过期望副本数的130%即可。
一旦旧的Pod被杀掉,新的ReplicaSet就会进一步扩容。
在整个过程中系统在任意时刻都能确保新旧ReplicaSet的Pod副本总数之和不超过所需副本数的130%。
这里需要注意多重更新(Rollover)的情况。
如果Deployment的上一次更新正在进行,此时用户再次发起Deployment的更新操作,
那么Deployment会为每一次更新都创建一个ReplicaSet,而每次在新的ReplicaSet创建成功后,会逐个增加Pod副本数,
同时将之前正在扩容的ReplicaSet停止扩容(更新),并将其加入旧版本ReplicaSet列表中,然后开始缩容至0的操作。
例如,假设我们创建一个Deployment,这个Deployment开始创建5个Nginx:1.7.9的Pod副本,在这个创建Pod动作尚未完成时,
我们又将Deployment进行更新,在副本数不变的情况下将Pod模板中的镜像修改为Nginx:1.9.1,
又假设此时Deployment已经创建了3个Nginx:1.7.9的Pod副本,则Deployment会立即杀掉已创建的3个Nginx:1.7.9 Pod,并开始创建Nginx:1.9.1 Pod。
Deployment不会在等待Nginx:1.7.9的Pod创建到5个之后再进行更新操作。
还需要注意更新Deployment的标签选择器(Label Selector)的情况。
通常来说,不鼓励更新Deployment的标签选择器,因为这样会导致Deployment选择的Pod列表发生变化,也可能与其他控制器产生冲突。
如果一定要更新标签选择器,那么请务必谨慎,确保不会出现其他问题。
关于Deployment标签选择器的更新的注意事项如下:
(1)添加选择器标签时,必须同步修改Deployment配置的Pod的标签,为Pod添加新的标签,否则Deployment的更新会报验证错误而失败。
添加标签选择器是无法向后兼容的,这意味着新的标签选择器不会匹配和使用旧选择器创建的ReplicaSets和Pod,
因此添加选择器将会导致所有旧版本的ReplicaSets和由旧ReplicaSets创建的Pod处于孤立状态(不会被系统自动删除,也不受新的ReplicaSet控制)。
为标签选择器和Pod模板添加新的标签(使用kubectl edit deployment命令)后,效果如下:
# kubectl get rs
可以看到新ReplicaSet(nginx-deployment-3661742516)创建的3个新Pod:
# kubectl get pods
(2)更新标签选择器,即更改选择器中标签的键或者值,也会产生与添加选择器标签类似的效果。
(3)删除标签选择器,即从Deployment的标签选择器中删除一个或者多个标签,该Deployment的ReplicaSet和Pod不会受到任何影响。
但需要注意的是,被删除的标签仍会存在于现有的Pod和ReplicaSets上。

3.11.2 Deployment的回滚
有时(例如新的Deployment不稳定时)我们可能需要将Deployment回滚到旧版本。
在默认情况下,所有Deployment的发布历史记录都被保留在系统中,以便于我们随时进行回滚(可以配置历史记录数量)。
假设在更新Deployment镜像时,将容器镜像名误设置成Nginx:1.91(一个不存在的镜像):
# kubectl set image deployment/nginx-deployment nginx=nginx:1.91
则这时Deployment的部署过程会卡住:
# kubectl rollout status deployments nginx-deployment
由于执行过程卡住,所以需要执行Ctrl-C命令来终止这个查看命令。
查看ReplicaSet,可以看到新建的ReplicaSet(nginx-deployment-3660254150):
# kubectl get rs
再查看创建的Pod,会发现新的ReplicaSet创建的1个Pod被卡在镜像拉取过程中。
# kubectl get pods
为了解决上面这个问题,我们需要回滚到之前稳定版本的Deployment。
首先,用kubectl rollout history命令检查这个Deployment部署的历史记录:
# kubectl rollout history deployment/nginx-deployment
注意,在创建Deployment时使用--record参数,就可以在CHANGE-CAUSE列看到每个版本使用的命令了。
另外,Deployment的更新操作是在Deployment进行部署(Rollout)时被触发的,
这意味着当且仅当Deployment的Pod模板(即spec.template)被更改时才会创建新的修订版本,例如更新模板标签或容器镜像。
其他更新操作(如扩展副本数)将不会触发Deployment的更新操作,
这也意味着我们将Deployment回滚到之前的版本时,只有Deployment的Pod模板部分会被修改。
如果需要查看特定版本的详细信息,则可以加上--revision=<N>参数:
# kubectl rollout history deployment/nginx-deployment --revision=3
现在我们决定撤销本次发布并回滚到上一个部署版本:
# kubectl rollout unod deployment/nginx-deployment
当然,也可以使用--to-revision参数指定回滚到的部署版本号:
# kubectl rollout unod deployment/nginx-deployment --to-revision=2
这样,该Deployment就回滚到之前的稳定版本了,可以从Deployment的事件信息中查看到回滚到版本2的操作过程:
# kubectl describe deployment/nginx-deployment

3.11.3 暂停和恢复Deployment的部署操作,以完成复杂的修改
对于一次复杂的Deployment配置修改,为了避免频繁触发Deployment的更新操作,
可以先暂停Deployment的更新操作,然后进行配置修改,再恢复Deployment,一次性触发完整的更新操作,就可以避免不必要的Deployment更新操作了。
以之前创建的Nginx为例:
# kubectl get deployments
# kubectl get rs
通过kubectl rollout pause命令暂停Deployment的更新操作:
# kubectl rollout pause deployment/nginx-deployment
然后修改Deployment的镜像信息:
# kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
查看Deployment的历史记录,发现并没有触发新的Deployment部署操作:
# kubectl rollout history deployment/nginx-deployment
在暂停Deployment部署之后,可以根据需要进行任意次数的配置更新。例如,再次更新容器的资源限制:
# kubectl set resources deployment nginx-deployment -c=nginx --limits=cpu=200m,memory=512Mi
最后,恢复这个Deployment的部署操作:
# kubectl rollout resume deploy nginx-deployment
可以看到一个新的ReplicaSet被创建出来了:
# kubectl get rs
查看Deployment的事件信息,可以看到Deployment完成了更新:
# kubectl describe deployment/nginx-deployment
注意,在恢复暂停的Deployment之前,无法回滚该Deployment。

3.11.4 使用kubectl rolling-update命令完成RC的滚动升级
对于RC的滚动升级,Kubernetes还提供了一个kubectl rolling-update命令进行实现。
该命令创建了一个新的RC,然后自动控制旧的RC中的Pod副本数量逐渐减少到0,同时新的RC中的Pod副本数量从0逐步增加到目标值,来完成Pod的升级。
需要注意的是,系统要求新的RC与旧的RC都在相同的命名空间内。
以redis-master为例,假设当前运行的redis-master Pod是1.0版本,现在需要升级到2.0版本。
创建redis-master-controller-v2.yaml的配置文件如下:
apiVersion: v1
kind: ReplicationController
metadata:
name: redis-master-v2
version: v2
spec:
replicas: 1
selector:
name: redis-master
version: v2
template:
metadata:
labels:
name: redis-master
version: v2
spec:
containers:
- name: master
image: kubeguide/redis-master:2.0
ports:
- containerPort: 6379
在配置文件中需要注意以下两点。
RC的名字(name)不能与旧RC的名字相同。
在selector中应至少有一个Label与旧RC的Label不同,以标识其为新RC。在本例中新增了一个名为version的Label,以与旧RC进行区分。
运行kubectl rolling-update命令完成Pod的滚动升级:
# kubectl rolling-update redis-master -f redis-master-controller-v2.yaml
等所有新的Pod都启动完成后,旧的Pod也被全部销毁,这样就完成了容器集群的更新工作。
另一种方法是不使用配置文件,直接用kubectl rolling-update命令,加上--image参数指定新版镜像名称来完成Pod的滚动升级:
# kubectl rolling-update redis-master --image=redis-master:2.0
与使用配置文件的方式不同,执行的结果是旧RC被删除,新RC仍将使用旧RC的名称。
可以看到,kubectl通过新建一个新版本Pod,停掉一个旧版本Pod,如此逐步迭代来完成整个RC的更新。
更新完成后,查看RC:
# kubectl get rc
可以看到,kubectl给RC增加了一个key为“deployment”的Label(这个key的名字可通过--deployment-label-key参数进行修改),
Label的值是RC的内容进行Hash计算后的值,相当于签名,这样就能很方便地比较RC里的Image名字及其他信息是否发生了变化。
如果在更新过程中发现配置有误,则用户可以中断更新操作,并通过执行kubectl rolling-update --rollback完成Pod版本的回滚:
# kubectl rolling-update redis-master --image=redis-master:2.0 --rollback
至此,可以看到Pod恢复到更新前的版本了。
可以看出,RC的滚动升级不具有Deployment在应用版本升级过程中的历史记录、新旧版本数量的精细控制等功能,
在Kubernetes的演进过程中,RC将逐渐被RS和Deployment所取代,建议用户优先考虑使用Deployment完成Pod的部署和升级操作。

3.11.5 其他管理对象的更新策略
Kubernetes从1.6版本开始,对DaemonSet和StatefulSet的更新策略也引入类似于Deployment的滚动升级,通过不同的策略自动完成应用的版本升级。
1.DaemonSet的更新策略
目前DaemonSet的升级策略包括两种:OnDelete和RollingUpdate。
(1)OnDelete:DaemonSet的默认升级策略,与1.5及以前版本的Kubernetes保持一致。
当使用OnDelete作为升级策略时,在创建好新的DaemonSet配置之后,新的Pod并不会被自动创建,直到用户手动删除旧版本的Pod,才触发新建操作。
(2)RollingUpdate:从Kubernetes 1.6版本开始引入。
当使用RollingUpdate作为升级策略对DaemonSet进行更新时,旧版本的Pod将被自动杀掉,然后自动创建新版本的DaemonSet Pod。
整个过程与普通Deployment的滚动升级一样是可控的。
不过有两点不同于普通Pod的滚动升级:
一是目前Kubernetes还不支持查看和管理DaemonSet的更新历史记录;
二是DaemonSet的回滚(Rollback)并不能如同Deployment一样直接通过kubectl rollback命令来实现,必须通过再次提交旧版本配置的方式实现。
2.StatefulSet的更新策略
Kubernetes从1.6版本开始,针对StatefulSet的更新策略逐渐向Deployment和DaemonSet的更新策略看齐,
也将实现RollingUpdate、Paritioned和OnDelete这几种策略,
以保证StatefulSet中各Pod有序地、逐个地更新,并且能够保留更新历史,也能回滚到某个历史版本。

3.12 Pod的扩缩容
在实际生产系统中,我们经常会遇到某个服务需要扩容的场景,也可能会遇到由于资源紧张或者工作负载降低而需要减少服务实例数量的场景。
此时可以利用Deployment/RC的Scale机制来完成这些工作。
Kubernetes对Pod的扩缩容操作提供了手动和自动两种模式,
手动模式通过执行kubectl scale命令或通过RESTful API对一个Deployment/RC进行Pod副本数量的设置,即可一键完成。
自动模式则需要用户根据某个性能指标或者自定义业务指标,并指定Pod副本数量的范围,系统将自动在这个范围内根据性能指标的变化进行调整。

3.12.1 手动扩缩容机制
以Deployment nginx为例(nginx-deployment.yaml):
apiVersion: apps/v1
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
已运行的Pod副本数量为3个:
# kubectl get pods
通过kubectl scale命令可以将Pod副本数量从初始的3个更新为5个:
# kubectl scale deployment nginx-deployment --replicas 5
# kubectl get pods
将--replicas设置为比当前Pod副本数量更小的数字,系统将会“杀掉”一些运行中的Pod,以实现应用集群缩容:
# kubectl scale deployment nginx-deployment --replicas 1
# kubectl get pods

3.12.2 自动扩缩容机制
Kubernetes从1.1版本开始,新增了名为Horizontal Pod Autoscaler(HPA)的控制器,用于实现基于CPU使用率进行自动Pod扩缩容的功能。
HPA控制器基于Master的kube-controller-manager服务启动参数--horizontal-pod-autoscaler-sync-period定义的探测周期(默认值为15s),
周期性地监测目标Pod的资源性能指标,并与HPA资源对象中的扩缩容条件进行对比,在满足条件时对Pod副本数量进行调整。
Kubernetes在早期版本中,只能基于Pod的CPU使用率进行自动扩缩容操作,关于CPU使用率的数据来源于Heapster组件。
Kubernetes从1.6版本开始,引入了基于应用自定义性能指标的HPA机制,并在1.9版本之后逐步成熟。
本节对Kubernetes的HPA的原理和实践进行详细说明。

1.HPA的工作原理
Kubernetes中的某个Metrics Server(Heapster或自定义Metrics Server)持续采集所有Pod副本的指标数据。
HPA控制器通过Metrics Server的API(Heapster的API或聚合API)获取这些数据,基于用户定义的扩缩容规则进行计算,得到目标Pod副本数量。
当目标Pod副本数量与当前副本数量不同时,HPA控制器就向Pod的副本控制器(Deployment、RC或ReplicaSet)发起scale操作,调整Pod的副本数量,完成扩缩容操作。
图3.9描述了HPA体系中的关键组件和工作流程。
接下来首先对HPA能够管理的指标类型、扩缩容算法、HPA对象的配置进行详细说明,然后通过一个完整的示例对如何搭建和使用基于自定义指标的HPA体系进行说明。

2.指标的类型
Master的kube-controller-manager服务持续监测目标Pod的某种性能指标,以计算是否需要调整副本数量。
目前Kubernetes支持的指标类型如下:
Pod资源使用率:Pod级别的性能指标,通常是一个比率值,例如CPU使用率。
Pod自定义指标:Pod级别的性能指标,通常是一个数值,例如接收的请求数量。
Object自定义指标或外部自定义指标:通常是一个数值,需要容器应用以某种方式提供,例如通过HTTP URL“/metrics”提供,或者使用外部服务提供的指标采集URL。
Kubernetes从1.11版本开始,弃用基于Heapster组件完成Pod的CPU使用率采集的机制,全面转向基于Metrics Server完成数据采集。
Metrics Server将采集到的Pod性能指标数据通过聚合API(Aggregated API)
如metrics.k8s.io、custom.metrics.k8s.io和external.metrics.k8s.io提供给HPA控制器进行查询。
关于聚合API和API聚合器(API Aggregator)的概念详见9.4节的说明。

3.扩缩容算法详解
Autoscaler控制器从聚合API获取到Pod性能指标数据之后,基于下面的算法计算出目标Pod副本数量,
与当前运行的Pod副本数量进行对比,决定是否需要进行扩缩容操作:
desiredReplicas = ceil [ currentReplicas * ( currentMetricValue / desiredMetricValue ) ]
即当前副本数×(当前指标值/期望的指标值),将结果向上取整。
以CPU请求数量为例,如果用户设置的期望指标值为100m,当前实际使用的指标值为200m,则计算得到期望的Pod副本数量应为两个(200/100=2)。
如果设置的期望指标值为50m,计算结果为0.5,则向上取整值为1,得到目标Pod副本数量应为1个。
当计算结果与1非常接近时,可以设置一个容忍度让系统不做扩缩容操作。
容忍度通过kube-controller-manager服务的启动参数--horizontal-pod-autoscaler-tolerance进行设置,默认值为0.1(即10%),
表示基于上述算法得到的结果在[-10%-+10%]区间内,即[0.9-1.1],控制器都不会进行扩缩容操作。
也可以将期望指标值(desiredMetricValue)设置为指标的平均值类型,例如targetAverageValue或targetAverageUtilization,
此时当前指标值(currentMetricValue)的算法为所有Pod副本当前指标值的总和除以Pod副本数量得到的平均值。
此外,存在几种Pod异常的情况,如下所述:
Pod正在被删除(设置了删除时间戳):将不会计入目标Pod副本数量。
Pod的当前指标值无法获得:本次探测不会将这个Pod纳入目标Pod副本数量,后续的探测会被重新纳入计算范围。
如果指标类型是CPU使用率,则对于正在启动但是还未达到Ready状态的Pod,也暂时不会纳入目标副本数量范围。
可以通过kube-controller-manager服务的启动参数--horizontal-pod-autoscaler-initial-readiness-delay设置首次探测Pod是否Ready的延时时间,默认值为30s。
另一个启动参数--horizontal-pod-autoscaler-cpuinitialization-period设置首次采集Pod的CPU使用率的延时时间。
在计算“当前指标值/期望的指标值”(currentMetricValue / desiredMetricValue)时将不会包括上述这些异常Pod。
当存在缺失指标的Pod时,系统将更保守地重新计算平均值。
系统会假设这些Pod在需要缩容(Scale Down)时消耗了期望指标值的100%,在需要扩容(Scale Up)时消耗了期望指标值的0%,这样可以抑制潜在的扩缩容操作。
此外,如果存在未达到Ready状态的Pod,并且系统原本会在不考虑缺失指标或NotReady的Pod情况下进行扩展,
则系统仍然会保守地假设这些Pod消耗期望指标值的0%,从而进一步抑制扩容操作。
如果在HorizontalPodAutoscaler中设置了多个指标,系统就会对每个指标都执行上面的算法,在全部结果中以期望副本数的最大值为最终结果。
如果这些指标中的任意一个都无法转换为期望的副本数(例如无法获取指标的值),系统就会跳过扩缩容操作。
最后,在HPA控制器执行扩缩容操作之前,系统会记录扩缩容建议信息(Scale Recommendation)。
控制器会在操作时间窗口(时间范围可以配置)中考虑所有的建议信息,并从中选择得分最高的建议。
这个值可通过kube-controller-manager服务的启动参数--horizontal-pod-autoscaler-downscale-stabilization-window进行配置,默认值为5min。
这个配置可以让系统更为平滑地进行缩容操作,从而消除短时间内指标值快速波动产生的影响。

4.HorizontalPodAutoscaler配置详解
Kubernetes将HorizontalPodAutoscaler资源对象提供给用户来定义扩缩容的规则。
HorizontalPodAutoscaler资源对象处于Kubernetes的API组“autoscaling”中,目前包括v1和v2两个版本。
其中autoscaling/v1仅支持基于CPU使用率的自动扩缩容,autoscaling/v2则用于支持基于任意指标的自动扩缩容配置,
包括基于资源使用率、Pod指标、其他指标等类型的指标数据,当前版本为autoscaling/v2beta2。
下面对HorizontalPodAutoscaler的配置和用法进行说明。

(1)基于autoscaling/v1版本的HorizontalPodAutoscaler配置,仅可以设置CPU使用率:
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 50
参数说明:
scaleTargetRef:目标作用对象,可以是Deployment、ReplicationController或ReplicaSet。
targetCPUUtilizationPercentage:期望每个Pod的CPU使用率都为50%,该使用率基于Pod设置的CPU Request值进行计算,
例如该值为200m,那么系统将维持Pod的实际CPU使用值为100m。
minReplicas和maxReplicas:Pod副本数量的最小值和最大值,系统将在这个范围内进行自动扩缩容操作,并维持每个Pod的CPU使用率为50%。
为了使用autoscaling/v1版本的HorizontalPodAutoscaler,需要预先安装Heapster组件或Metrics Server,用于采集Pod的CPU使用率。
Heapster从Kubernetes 1.11版本开始进入弃用阶段,本节不再对Heapster进行详细说明。
关于Metrics Server的说明请参考9.4节的介绍,本节主要对基于自定义指标进行自动扩缩容的设置进行说明。

(2)基于autoscaling/v2beta2的HorizontalPodAutoscaler配置:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
参数说明:
scaleTargetRef:目标作用对象,可以是Deployment、ReplicationController或ReplicaSet。
minReplicas和maxReplicas:Pod副本数量的最小值和最大值,系统将在这个范围内进行自动扩缩容操作,并维持每个Pod的CPU使用率为50%。
metrics:目标指标值。
在metrics中通过参数type定义指标的类型;通过参数target定义相应的指标目标值,
系统将在指标数据达到目标值时(考虑容忍度的区间,见前面算法部分的说明)触发扩缩容操作。
可以将metrics中的type(指标类型)设置为以下三种,可以设置一个或多个组合,如下所述。
(1)Resource:基于资源的指标值,可以设置的资源为CPU和内存。
(2)Pods:基于Pod的指标,系统将对全部Pod副本的指标值进行平均值计算。
(3)Object:基于某种资源对象(如Ingress)的指标或应用系统的任意自定义指标。
Resource类型的指标可以设置CPU和内存。
对于CPU使用率,在target参数中设置averageUtilization定义目标平均CPU使用率。
对于内存资源,在target参数中设置AverageValue定义目标平均内存使用值。
指标数据可以通过API“metrics.k8s.io”进行查询,要求预先启动Metrics Server服务。
Pods类型和Object类型都属于自定义指标类型,指标的数据通常需要搭建自定义Metrics Server和监控工具进行采集和处理。
指标数据可以通过API“custom.metrics.k8s.io”进行查询,要求预先启动自定义Metrics Server服务。

类型为Pods的指标数据来源于Pod对象本身,其target指标类型只能使用AverageValue,示例如下:
metrics:
- type: Pods
pods:
metric:
name: packets-per-second
target:
type: AverageValue
averageValue: 1k
其中,设置Pod的指标名为packets-per-second,在目标指标平均值为1000时触发扩缩容操作。

类型为Object的指标数据来源于其他资源对象或任意自定义指标,其target指标类型可以使用Value或AverageValue(根据Pod副本数计算平均值)进行设置。
下面对几种常见的自定义指标给出示例和说明。
例1,设置指标的名称为requests-per-second,其值来源于Ingress“main-route”,将目标值(value)设置为2000,
即在Ingress的每秒请求数量达到2000个时触发扩缩容操作:
metrics:
- type: Object
object:
metric:
name: requests-per-second
describedObject:
apiVersion: extensions/v1beta1
kind: Ingress
name: main-route
target:
type: Value
value: 2k
例2,设置指标的名称为http_requests,并且该资源对象具有标签“verb=GET”,在指标平均值达到500时触发扩缩容操作:
metrics:
- type: Object
object:
metric:
name: 'http_requests'
selector: 'verb=GET'
target:
type: AverageValue
averageValue: 500
还可以在同一个HorizontalPodAutoscaler资源对象中定义多个类型的指标,
系统将针对每种类型的指标都计算Pod副本的目标数量,以最大值为准进行扩缩容操作。
例如:
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: php-apache
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: php-apache
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
- type: Pods
pods:
metric:
name: packets-per-second
targetAverageValue: 1k
- type: Object
object:
metric:
name: requests-per-second
describedObject:
apiVersion: extensions/v1beta1
kind: Ingress
name: main-route
target:
type: Value
value: 2k

从1.10版本开始,Kubernetes引入了对外部系统指标的支持。
例如,用户使用了公有云服务商提供的消息服务或外部负载均衡器,
希望基于这些外部服务的性能指标(如消息服务的队列长度、负载均衡器的QPS)对自己部署在Kubernetes中的服务进行自动扩缩容操作。
这时,就可以在metrics参数部分设置type为External来设置自定义指标,然后就可以通过API“external.metrics.k8s.io”查询指标数据了。
当然,这同样要求自定义Metrics Server服务已正常工作。
例3,设置指标的名称为queue_messages_ready,具有queue=worker_tasks标签在目标指标平均值为30时触发自动扩缩容操作:
- type: External
object:
metric:
name: queue_messages_ready
selector: "queue=worker_tasks"
target:
type: AverageValue
averageValue: 30
在使用外部服务的指标时,要安装、部署能够对接到Kubernetes HPA模型的监控系统,并且完全了解监控系统采集这些指标的机制,后续的自动扩缩容操作才能完成。
Kubernetes推荐尽量使用type为Object的HPA配置方式,这可以通过使用Operator模式,将外部指标通过CRD(自定义资源)定义为API资源对象来实现。

5.基于自定义指标的HPA实践
下面通过一个完整的示例,对如何搭建和使用基于自定义指标的HPA体系进行说明。
基于自定义指标进行自动扩缩容时,需要预先部署自定义Metrics Server,
目前可以使用基于Prometheus、Microsoft Azure、Datadog Cluster等系统的Adapter实现自定义Metrics Server,
未来还将提供基于Google Stackdriver的实现自定义Metrics Server。
读者可以参考官网https://github.com/kubernetes/metrics/blob/master/IMPLEMENTATIONS.md#custommetrics-api的说明。
本节基于Prometheus监控系统对HPA的基础组件部署和HPA配置进行详细说明。
基于Prometheus的HPA架构如图3.10所示。
关键组件包括如下:
Prometheus:定期采集各Pod的性能指标数据。
Custom Metrics Server:自定义Metrics Server,用Prometheus Adapter进行具体实现。
它从Prometheus服务采集性能指标数据,通过Kubernetes的Metrics Aggregation层将自定义指标API注册到Master的API Server中,
以/apis/custom.metrics.k8s.io路径提供指标数据。
HPA Controller:Kubernetes的HPA控制器,基于用户定义的HorizontalPodAutoscaler进行自动扩缩容操作。
接下来对整个系统的部署过程进行说明。
(1)在Master的API Server启动Aggregation层,通过设置kube-apiserver服务的下列启动参数进行开启。
--requestheader-client-ca-file=/etc/kubernetes/ssl_keys/ca.crt:客户端CA证书。
--requestheader-allowed-names=:允许访问的客户端common names列表,通过header中由--requestheader-username-headers参数指定的字段获取。
客户端common names的名称需要在client-ca-file中进行配置,将其设置为空值时,表示任意客户端都可以访问。
--requestheader-extra-headers-prefix=X-Remote-Extra-:请求头中需要检查的前缀名。
--requestheader-group-headers=X-Remote-Group:请求头中需要检查的组名。
--requestheader-username-headers=X-Remote-User:请求头中需要检查的用户名。
--proxy-client-cert-file=/etc/kubernetes/ssl_keys/kubelet_client.crt:在请求期间验证Aggregator的客户端CA证书。
--proxy-client-key-file=/etc/kubernetes/ssl_keys/kubelet_client.key:在请求期间验证Aggregator的客户端私钥。
配置kube-controller-manager服务中HPA的相关启动参数(可选配置)如下。
--horizontal-pod-autoscaler-sync-period=10s:HPA控制器同步Pod副本数量的时间间隔,默认值为15s。
--horizontal-pod-autoscaler-downscale-stabilization=1m0s:执行缩容操作的等待时长,默认值为5min。
--horizontal-pod-autoscaler-initial-readiness-delay=30s:等待Pod达到Ready状态的时延,默认值为30min。
--horizontal-pod-autoscaler-tolerance=0.1:扩缩容计算结果的容忍度,默认值为0.1,表示[-10%-+10%]。

(2)部署Prometheus,这里使用Operator模式进行部署。
首先,使用下面的YAML配置文件(prometheus-operator.yaml)部署prometheus-operator:
apiVersion: apps/v1
kind: Deployment
metadata:
name: prometheus-operator
labels:
k8s-app: prometheus-operator
spec:
replicas: 1
selector:
matchLabels:
k8s-app: prometheus-operator
template:
metadata:
labels:
k8s-app: prometheus-operator
spec:
containers:
- image: quay.io/coreos/prometheus-operator:v0.17.0
imagePullPolicy: IfNotPresent
name: prometheus-operator
ports:
- containerPort: 8080
name: http
resources:
limits:
cpu: 200m
memory: 100Mi
requests:
cpu: 100m
memory: 50Mi
这个prometheus-operator会自动创建名为monitoring.coreos.com的CRD资源。
然后,通过Operator的配置部署Prometheus服务:
apiVersion: monotoring.coreos.com/v1
kind: Prometheus
metadata:
name: prometheus
labels:
app: prometheus
prometheus: prometheus
spec:
replicas: 1
baseImage: prom/prometheus
version: v2.8.0
serviceMonitorSelector:
matchLabels:
service-monitor: function
resources:
requests:
memory: 300Mi
---------------------
apiVersion: v1
kind: Service
metadata:
name: prometheus
labels:
app: prometheus
prometheus: prometheus
spec:
selector:
prometheus: prometheus
ports:
- name: http
port: 9090
确认Prometheus Operator和Prometheus服务正常运行:
# kubectl get pods

(3)部署自定义Metrics Server,这里以Prometheus Adapter的实现进行部署。
下面的YAML配置文件主要包含Namespace、ConfigMap、Deployment、Service和自定义API资源custom.metrics.k8s.io/v1beta1,
这里将它们部署在一个新的Namespace“custom-metrics”中。
-----
kind: Namespace
apiVersion: v1
metadata:
name: custom-metrics
-----
apiVersion: v1
kind: ConfigMap
metadata:
name: adapter-config
namespace: custom-metrics
data:
config.yaml: |
rules:
-----
apiVersion: apps/v1
kind: Deployment
metadata:
name: custom-metrics-server
namespace: custom-metrics
labels:
app: custom-metrics-server
spec:
replicas: 1
selector:
matchLabels:
app: custom-metrics-server
template:
metadata:
name: custom-metrics-server
labels:
app: custom-metrics-server
spec:
containers:
- name: custom-metrics-server
image: directxman12/k8s-prometheus-adapter-amd64
imagePullPolicy: IfNotPresent
args:
- --prometheus-url=http://prometheus.default.svc:9090/ # 设置prometheus服务在kubernetes中的DNA域名格式地址
- --metrics-relist-interval=30s # 设置更新指标缓存的频率,应将其设置为大于或等于prometheus的指标采集频率
- --v=10
- --config=/etc/adapter/config.yaml
- --logtostderr=true
ports:
- containerPort: 443
securityContext:
runAsUser: 0
volumeMounts:
- mountPath: /etc/adapter/
name: config
readOnly: true
volumes:
- name: config
configMap:
name: adapter-config
-----
apiVersion: apps/v1
kind: Service
metadata:
name: custom-metrics-server
namespace: custom-metrics
spec:
ports:
- port: 443
targetPort: 443
selector:
app: custom-metrics-server
-----
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1beta1.custom.metrics.k8s.io
spec:
service:
name: custom-metrics-server
namespace: custom-metrics
group: custom.metrics.k8s.io
version: v1beta1
insecureSkipTLSVerify: true
groupPriorityMinimum: 100
versionPriority: 100
确认custom-metrics-server正常运行:
# kubectl -n custom-metrics get pods

(4)部署应用程序,它会在HTTP URL“/metrics”路径提供名为http_requests_total的指标值:
-----
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-app
labels:
app: sample-app
spec:
replicas: 1
selector:
matchLabels:
app: sample-app
template:
metadata:
name: sample-app
labels:
app: sample-app
spec:
containers:
- name: metrics-provider
image: luxas/autoscale-demo:v0.1.2
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
-----
apiVersion: apps/v1
kind: Service
metadata:
name: sample-app
labels:
app: sample-app
spec:
ports:
- name: http
port: 80
targetPort: 8080
selector:
app: sample-app
部署成功之后,可以在应用的URL“/metrics”中查看指标http_requests_total的值:
# kubectl get service sample-app
# curl IP/metrics

(5)创建一个Prometheus的ServiceMonitor对象,用于监控应用程序提供的指标:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: sample-app
labels:
service-monitor: function
spec:
selector:
matchLables:
app: sample-app
endpoints:
- port: http
关键配置参数如下:
Selector:设置为Pod的Label“app: sample-app”。
Endpoints:设置为在Service中定义的端口名称“http”。

(6)创建一个HorizontalPodAutoscaler对象,用于为HPA控制器提供用户期望的自动扩缩容配置。
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: sample-app
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sample-app
minReplicas: 1
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: http_requests
target:
type: AverageValue
averageValue: 500m
参数说明:
scaleTargetRef:设置HPA的作用对象为之前部署的Deployment“sample-app”。
type=Pods:设置指标类型为Pods,表示从Pod获取指标数据。
metric.name=http_requests:将指标的名称设置为“http_requests”,
是自定义Metrics Server将应用程序提供的指标“http_requests_total”经过计算转换成的一个新比率值,
即sum(rate(http_requests_total{namespace="xx",pod="xx"}[1m])) by pod,
指过去1min内全部Pod指标http_requests_total总和的每秒平均值。
target:将指标http_requests的目标值设置为500m,类型为AverageValue,表示基于全部Pod副本数据计算平均值。
目标Pod副本数量将使用公式“http_requests当前值/500m”进行计算。
minReplicas和maxReplicas:将扩缩容区间设置为1~10(单位是Pod副本)。
此时可以通过查看自定义Metrics Server提供的URL“custom.metrics.k8s.io/v1beta1”查看Pod的指标是否已经被成功采集,并能够通过聚合API进行查询:
# kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/default/pods/*/http_requests?selector=app%3Dsample-app"
从结果中看到正确的value值,说明自定义Metrics Server工作正常。
查看HorizontalPodAutoscaler的详细信息,可以看到其成功从自定义Metrics Server处获取了应用的指标数据,可以进行扩缩容操作:
# kubectl describe hpa.v2beta2.autoscaling sample-app

(7)对应用的服务地址发起HTTP访问请求,验证HPA自动扩容机制。
例如,可以使用如下脚本对应用进行压力测试:
# for i in {1..100000}; do wget -q -o- 169.169.43.254 > /dev/null; done
一段时间之后,观察HorizontalPodAutoscaler和Pod数量的变化,可以看到自动扩容的过程:
# kubectl describe hpa.v2beta2.autoscaling sample-app
发现Pod数量扩容到了10个(被maxReplicas参数限制的最大值):
# kubectl get pods -l app=sample-app
停止访问应用服务,等待一段时间后,观察HorizontalPodAutoscaler和Pod数量的变化,可以看到缩容操作:
# kubectl describe hpa.v2beta2.autoscaling sample-app
发现Pod的数量已经缩容到最小值1个:
# kubectl get pods -l app=sample-app

3.13 使用StatefulSet搭建MongoDB集群
本节以MongoDB为例,使用StatefulSet完成MongoDB集群的创建,
为每个MongoDB实例在共享存储中(这里采用GlusterFS)都申请一片存储空间,以实现一个无单点故障、高可用、可动态扩展的MongoDB集群。
部署架构如图3.11所示。

3.13.1 前提条件
在创建StatefulSet之前,需要确保在Kubernetes集群中管理员已经创建好共享存储,并能够与StorageClass对接,以实现动态存储供应的模式。
本节的示例将使用GlusterFS作为共享存储(GlusterFS的部署方法参见8.6节的说明)。

3.13.2 创建StatefulSet
为了完成MongoDB集群的搭建,需要创建如下三个资源对象。
一个StorageClass,用于StatefulSet自动为各个应用Pod申请PVC。
一个Headless Service,用于维护MongoDB集群的状态。
一个StatefulSet。

首先,创建一个StorageClass对象。
storageclass-fast.yaml文件的内容如下:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/glusterfs
parameters:
resturl: "http://<heketi-rest-url>"
执行kubectl create命令创建该StorageClass:
# kubectl create -f storageclass-fast.yaml

接下来,创建对应的Headless Service。
mongo-sidecar作为MongoDB集群的管理者,将使用此Headless Service来维护各个MongoDB实例之间的集群关系,以及集群规模变化时的自动更新。
mongo-headless-service.yaml文件的内容如下:
apiVersion: v1
kind: Service
metadata:
name: mongo
labels:
name: mongo
spec:
ports:
- port: 27017
targetPort: 27017
clusterIP: None
selector:
role: mongo
使用kubectl create命令创建该Headless Service:
# kubectl create -f mongo-headless-service.yaml

最后,创建MongoDB StatefulSet。
statefulset-mongo.yaml文件的内容如下:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
spec:
serviceName: "mongo"
replicas: 3
template:
metadata:
labels:
role: mongo
environment: test
spec:
terminationGracePeriodSeconds: 10
containers:
- name: mongo
image: mongo
command:
- mongod
- "--replSet"
- rs0
- "--smallfiles"
- "--noprealloc"
ports:
- containerPort: 27017
volumeMounts:
- name: mongo-persistent-storage
mountPath: /data/db
- name: mongo-sidecar
image: cvallance/mongo-k8s-sidecar
env:
- name: MONGO_SIDECAR_POD_LABELS
value: "role=mongo,environment=test"
- name: KUBERNETES_MONGO_SERVICE_NAME
value: "mongo"
volumeClaimTemplates:
- metadata:
name: mongo-persistent-storage
annotations:
volume.beta.kubernetes.io/storage-class: "fast"
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 100Gi
其中的主要配置说明如下:
(1)在该StatefulSet的定义中包括两个容器:mongo和mongo-sidecar。
mongo是主服务程序,mongo-sidecar是将多个mongo实例进行集群设置的工具。
mongo-sidecar中的环境变量如下:
MONGO_SIDECAR_POD_LABELS:设置为mongo容器的标签,用于sidecar查询它所要管理的MongoDB集群实例。
KUBERNETES_MONGO_SERVICE_NAME:它的值为mongo,表示sidecar将使用mongo这个服务名来完成MongoDB集群的设置。
(2)replicas=3表示这个MongoDB集群由3个mongo实例组成。
(3)volumeClaimTemplates是StatefulSet最重要的存储设置。
在annotations段设置volume.beta.kubernetes.io/storage-class="fast"表示使用名为fast的StorageClass自动为每个mongo Pod实例分配后端存储。
resources.requests.storage=100Gi表示为每个mongo实例都分配100GiB的磁盘空间。
使用kubectl create命令创建这个StatefulSet:
# kubectl create -f statefulset-mongo.yaml
最终可以看到StatefulSet依次创建并启动了3个mongo Pod实例,它们的名字依次为mongo-0、mongo-1、mongo-2:
# kubectl get pods -l role=mongo
StatefulSet会用volumeClaimTemplates中的定义为每个Pod副本都创建一个PVC实例,
每个PVC的名称由StatefulSet定义中volumeClaimTemplates的名称和Pod副本的名称组合而成。
查看系统中的PVC,可以验证这一点:
# kubectl get pvc
下面是mongo-0这个Pod中的Volume设置,可以看到系统自动为其挂载了对应的PVC:
# kubectl get pod mongo-0 -o yaml
至此,一个由3个实例组成的MongoDB集群就创建完成了,其中每个实例都拥有稳定的名称和独立的存储空间。

3.13.3 查看MongoDB集群的状态
登录任意一个mongo Pod,在mongo命令行界面用rs.status()命令查看MongoDB集群的状态,可以看到mongo集群已通过sidecar完成了创建。
在集群中包含3个节点,每个节点的名称都是StatefulSet设置的DNS域名格式的网络标识名称:
mongo-0.mongo.default.svc.cluster.local
mongo-1.mongo.default.svc.cluster.local
mongo-2.mongo.default.svc.cluster.local
同时,可以看到3个mongo实例各自的角色(PRIMARY或SECONDARY)也都进行了正确的设置:
# kubectl exec -it mongo-0 -- mongo
对于需要访问这个mongo集群的Kubernetes集群内部客户端来说,
可以通过Headless Service“mongo”获取后端的所有Endpoints列表,并组合为数据库链接串,
例如“mongodb://mongo-0.mongo, mongo-1.mongo, mongo-2.mongo:27017/dbname_?”。

3.13.4 StatefulSet的常见应用场景
下面对MongoDB集群常见的两种场景进行操作,说明StatefulSet对有状态应用的自动化管理功能。

1.MongoDB集群的扩容
假设在系统运行过程中,3个mongo实例不足以满足业务的要求,这时就需要对mongo集群进行扩容。
仅需要通过对StatefulSet进行scale操作,就能实现在mongo集群中自动添加新的mongo节点。
使用kubectl scale命令将StatefulSet设置为4个实例:
# kubectl scale --replicas=4 statefulset mongo
等待一会儿,看到第4个实例“mongo-3”创建成功:
# kubectl get pod -l role=mongo
进入某个实例查看mongo集群的状态,可以看到第4个节点已经加入:
# kubectl exec -it mongo-0 -- mongo
同时,系统也为mongo-3分配了一个新的PVC用于保存数据,
此处不再赘述,有兴趣的读者可自行查看系统为mongo-3绑定的Volume设置和后端GlusterFS共享存储的资源分配情况。

2.自动故障恢复(MongoDB集群的高可用)
假设在系统运行过程中,某个mongo实例或其所在主机发生故障,
则StatefulSet将会自动重建该mongo实例,并保证其身份(ID)和使用的数据(PVC)不变。
以mongo-0实例发生故障为例,StatefulSet将会自动重建mongo-0实例,并为其挂载之前分配的PVC“mongo-persistent-storage-mongo-0”。
服务“mongo-0”在重新启动后,原数据库中的数据不会丢失,可继续使用。
# kubectl get pod -l role=mongo
# kubectl get pod mongo-0 -o yaml
进入某个实例查看mongo集群的状态,mongo-0发生故障前在集群中的角色为PRIMARY,
在其脱离集群后,mongo集群会自动选出一个SECONDARY节点提升为PRIMARY节点(本例中为mongo-2)。
重启后的mongo-0则会成为一个新的SECONDARY节点:
# kubectl exec -it mongo-0 -- mongo
从上面的例子中可以看出,Kubernetes使用StatefulSet来搭建有状态的应用集群(MongoDB、MySQL等),同部署无状态的应用一样简便。
Kubernetes能够保证StatefulSet中各应用实例在创建和运行的过程中,都具有固定的身份标识和独立的后端存储;
还支持在运行时对集群规模进行扩容、保障集群的高可用等非常重要的功能。

猜你喜欢

转载自www.cnblogs.com/BradMiller/p/12228243.html