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中各應用實例在創建和運行的過程中,都具有固定的身份標識和獨立的后端存儲;
還支持在運行時對集群規模進行擴容、保障集群的高可用等非常重要的功能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM