一、Volume
Pod作為 Kubernetes 項目裡最核心的編排對象,Pod 攜帶的資訊非常豐富。其中,資源定義(比如 CPU、記憶體等),以及排程相關的字段,本文從volume入手,深入了解Pod對象各個重要字段的含義。
Volume,叫作Projected Volume,可以把它翻譯為“投射資料卷”。(kubernetes v1.11之後的新特性)
在 Kubernetes 中,有幾種特殊的 Volume,它們存在的意義不是為了存放容器裡的資料,也不是用來進行容器和主控端之間的資料交換。這些特殊 Volume 的作用,是為容器提供預先定義好的資料。是以,從容器的角度來看,這些 Volume 裡的資訊就是仿佛是被 Kubernetes“投射”(Project)進入容器當中的。這正是 Projected Volume 的含義。
到目前為止,Kubernetes 支援的 Projected Volume 一共有四種:
- Secret;
- ConfigMap;
- Downward API;
- ServiceAccountToken。
1.1、Secret
Secret它的作用,是把 Pod 想要通路的加密資料,存放到 Etcd 中。然後,你就可以通過在 Pod 的容器裡挂載 Volume 的方式,通路到這些 Secret 裡儲存的資訊了。
Secret 最典型的使用場景:存放資料庫的 Credential 資訊,如下
apiVersion: v1
kind: Pod
metadata:
name: test-projected-volume
spec:
containers:
- name: test-secret-volume
image: busybox
args:
- sleep
- "86400"
volumeMounts:
- name: mysql-cred
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: mysql-cred
projected:
sources:
- secret:
name: user
- secret:
name: pass
在這個 Pod 中,定義了一個簡單的容器。它聲明挂載的 Volume,并不是常見的 emptyDir 或者 hostPath 類型,而是 projected 類型。而這個 Volume 的資料來源(sources),則是名為 user 和 pass 的 Secret 對象,分别對應的是資料庫的使用者名和密碼。
這裡用到的資料庫的使用者名、密碼,正是以 Secret 對象的方式交給 Kubernetes 儲存的。
完成這個操作的操作指令如下:
$ cat ./username.txt
admin
$ cat ./password.txt
c1oudc0w!
$ kubectl create secret generic user --from-file=./username.txt
$ kubectl create secret generic pass --from-file=./password.txt
其中,username.txt 和 password.txt 檔案裡,存放的就是使用者名和密碼;而 user 和 pass,則是我為 Secret 對象指定的名字。而我想要檢視這些 Secret 對象的話,隻要執行一條
kubectl get
指令就可以了:
$ kubectl get secrets
NAME TYPE DATA AGE
user Opaque 1 51s
pass Opaque 1 51s
除了使用
kubectl create secret
指令外,也可以直接通過編寫 YAML 檔案的方式來建立這個 Secret 對象,比如:
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
user: YWRtaW4=
pass: MWYyZDFlMmU2N2Rm
可以看到,通過編寫 YAML 檔案建立出來的 Secret 對象隻有一個。但它的 data 字段,卻以 Key-Value 的格式儲存了兩份 Secret 資料。其中,“user”就是第一份資料的 Key,“pass”是第二份資料的 Key。
注意:Secret 對象要求這些資料必須是經過 Base64 轉碼的,以免出現明文密碼的安全隐患。
$ echo -n 'admin' | base64
YWRtaW4=
$ echo -n '1f2d1e2e67df' | base64
MWYyZDFlMmU2N2Rm
注意:像這樣建立的 Secret 對象,它裡面的内容僅僅是經過了轉碼,而并沒有被加密。在真正的生産環境中,你需要在 Kubernetes 中開啟 Secret 的加密插件,增強資料的安全性。
接下來,嘗試建立這個Pod:
$ kubectl create -f test-projected-volume.yaml
當 Pod 變成 Running 狀态之後,再驗證一下這些 Secret 對象是不是已經在容器裡了:
$ kubectl exec -it test-projected-volume -- /bin/sh
$ ls /projected-volume/
user
pass
$ cat /projected-volume/user
root
$ cat /projected-volume/pass
1f2d1e2e67df
從傳回結果中,可以看到,儲存在 Etcd 裡的使用者名和密碼資訊,已經以檔案的形式出現在了容器的 Volume 目錄裡。而這個檔案的名字,就是 kubectl create secret 指定的 Key,或者說是 Secret 對象的 data 字段指定的 Key。
更重要的是,像這樣通過挂載方式進入到容器裡的 Secret,一旦其對應的 Etcd 裡的資料被更新,這些 Volume 裡的檔案内容,同樣也會被更新。其實,這是 kubelet 元件在定時維護這些 Volume。
注意:這個更新可能會有一定的延時。是以在編寫應用程式時,在發起資料庫連接配接的代碼處寫好重試和逾時的邏輯,絕對是個好習慣。
1.2、ConfigMap
與 Secret 類似的是 ConfigMap,它與 Secret 的差別在于,ConfigMap 儲存的是不需要加密的、應用所需的配置資訊。而 ConfigMap 的用法幾乎與 Secret 完全相同:可以使用 kubectl create configmap 從檔案或者目錄建立 ConfigMap,也可以直接編寫 ConfigMap 對象的 YAML 檔案。
如:一個 Java 應用所需的配置檔案(.properties 檔案),就可以通過下面這樣的方式儲存在 ConfigMap 裡:
# .properties檔案的内容
$ cat example/ui.properties
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
# 從.properties檔案建立ConfigMap
$ kubectl create configmap ui-config --from-file=example/ui.properties
# 檢視這個ConfigMap裡儲存的資訊(data)
$ kubectl get configmaps ui-config -o yaml
apiVersion: v1
data:
ui.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
how.nice.to.look=fairlyNice
kind: ConfigMap
metadata:
name: ui-config
...
備注:
kubectl get -o yaml
這樣的參數,會将指定的 Pod API 對象以 YAML 的方式展示出來。
1.3、Downward API
它的作用是:讓 Pod 裡的容器能夠直接擷取到這個 Pod API 對象本身的資訊。
例子:
apiVersion: v1
kind: Pod
metadata:
name: test-downwardapi-volume
labels:
zone: us-est-coast
cluster: test-cluster1
rack: rack-22
spec:
containers:
- name: client-container
image: k8s.gcr.io/busybox
command: ["sh", "-c"]
args:
- while true; do
if [[ -e /etc/podinfo/labels ]]; then
echo -en '\n\n'; cat /etc/podinfo/labels; fi;
sleep 5;
done;
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
readOnly: false
volumes:
- name: podinfo
projected:
sources:
- downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
在這個 Pod 的 YAML 檔案中,定義了一個簡單的容器,聲明了一個 projected 類型的 Volume。隻不過這次 Volume 的資料來源,變成了 Downward API。而這個 Downward API Volume,則聲明了要暴露 Pod 的 metadata.labels 資訊給容器。
通過這樣的聲明方式,目前 Pod 的 Labels 字段的值,就會被 Kubernetes 自動挂載成為容器裡的 /etc/podinfo/labels 檔案。
而這個容器的啟動指令,則是不斷列印出 /etc/podinfo/labels 裡的内容。是以,當我建立了這個 Pod 之後,就可以通過 kubectl logs 指令,檢視到這些 Labels 字段被列印出來,如下所示:
$ kubectl create -f dapi-volume.yaml
$ kubectl logs test-downwardapi-volume
cluster="test-cluster1"
rack="rack-22"
zone="us-est-coast"
目前,Downward API 支援的字段已經非常豐富了,比如:
1. 使用fieldRef可以聲明使用:
spec.nodeName - 主控端名字
status.hostIP - 主控端IP
metadata.name - Pod的名字
metadata.namespace - Pod的Namespace
status.podIP - Pod的IP
spec.serviceAccountName - Pod的Service Account的名字
metadata.uid - Pod的UID
metadata.labels['<KEY>'] - 指定<KEY>的Label值
metadata.annotations['<KEY>'] - 指定<KEY>的Annotation值
metadata.labels - Pod的所有Label
metadata.annotations - Pod的所有Annotation
2. 使用resourceFieldRef可以聲明使用:
容器的CPU limit
容器的CPU request
容器的memory limit
容器的memory request
上面這個清單的内容,随着 Kubernetes 項目的發展肯定還會不斷增加。是以這裡列出來的資訊僅供參考,在使用 Downward API 時,還是要記得去查閱一下官方文檔。
注意:Downward API 能夠擷取到的資訊,一定是 Pod 裡的容器程序啟動之前就能夠确定下來的資訊。而如果你想要擷取 Pod 容器運作後才會出現的資訊,比如,容器程序的 PID,那就肯定不能使用 Downward API 了,而應該考慮在 Pod 裡定義一個 sidecar 容器。
其實,Secret、ConfigMap,以及 Downward API 這三種 Projected Volume 定義的資訊,大多還可以通過環境變量的方式出現在容器裡。但是,通過環境變量擷取這些資訊的方式,不具備自動更新的能力。是以,一般情況下,建議你使用 Volume 檔案的方式擷取這些資訊。
二、Service Account
疑問:現在有了一個Pod,能不能在這個Pod裡安裝一個kubernetes的Client,這樣就可以從容器裡直接通路并且操作這個kubernetes的API了呢?
答案是肯定的,不過首先要解決API Server的授權問題。
Service Account 對象的作用,就是 Kubernetes 系統内置的一種“服務賬戶”,它是 Kubernetes 進行權限配置設定的對象。比如,Service Account A,可以隻被允許對 Kubernetes API 進行 GET 操作,而 Service Account B,則可以有 Kubernetes API 的所有操作權限。
像這樣的 Service Account 的授權資訊和檔案,實際上儲存在它所綁定的一個特殊的 Secret 對象裡的。這個特殊的 Secret 對象,就叫作 ServiceAccountToken。任何運作在 Kubernetes 叢集上的應用,都必須使用這個 ServiceAccountToken 裡儲存的授權資訊,也就是 Token,才可以合法地通路 API Server。
是以說,Kubernetes 項目的 Projected Volume 其實隻有三種,因為第四種 ServiceAccountToken,隻是一種特殊的 Secret 而已。
另外,為了友善使用,Kubernetes 已經為你提供了一個預設“服務賬戶”(default Service Account)。并且,任何一個運作在 Kubernetes 裡的 Pod,都可以直接使用這個預設的 Service Account,而無需顯示地聲明挂載它。
這是靠Projected Volume 機制做到的。
如果你檢視一下任意一個運作在 Kubernetes 叢集裡的 Pod,就會發現,每一個 Pod,都已經自動聲明一個類型是 Secret、名為 default-token-xxxx 的 Volume,然後 自動挂載在每個容器的一個固定目錄上。比如:
$ kubectl describe pod nginx-deployment-5c678cfb6d-lg9lw
Containers:
...
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from default-token-s8rbq (ro)
Volumes:
default-token-s8rbq:
Type: Secret (a volume populated by a Secret)
SecretName: default-token-s8rbq
Optional: false
這個 Secret 類型的 Volume,正是預設 Service Account 對應的 ServiceAccountToken。是以說,Kubernetes 其實在每個 Pod 建立的時候,自動在它的 spec.volumes 部分添加上了預設 ServiceAccountToken 的定義,然後自動給每個容器加上了對應的 volumeMounts 字段。這個過程對于使用者來說是完全透明的。
這樣,一旦 Pod 建立完成,容器裡的應用就可以直接從這個預設 ServiceAccountToken 的挂載目錄裡通路到授權資訊和檔案。這個容器内的路徑在 Kubernetes 裡是固定的,即:/var/run/secrets/kubernetes.io/serviceaccount ,而這個 Secret 類型的 Volume 裡面的内容如下所示:
$ ls /var/run/secrets/kubernetes.io/serviceaccount
ca.crt namespace token
是以,你的應用程式隻要直接加載這些授權檔案,就可以通路并操作 Kubernetes API 了。而且,如果你使用的是 Kubernetes 官方的 Client 包(k8s.io/client-go)的話,它還可以自動加載這個目錄下的檔案,你不需要做任何配置或者編碼操作。
這種把 Kubernetes 用戶端以容器的方式運作在叢集裡,然後使用 default Service Account 自動授權的方式,被稱作“InClusterConfig”,也是最推薦的進行 Kubernetes API 程式設計的授權方式。
當然,考慮到自動挂載預設 ServiceAccountToken 的潛在風險,Kubernetes 允許你設定預設不為 Pod 裡的容器自動挂載這個 Volume。
除了這個預設的 Service Account 外,我們很多時候還需要建立一些我們自己定義的 Service Account,來對應不同的權限設定。這樣,我們的 Pod 裡的容器就可以通過挂載這些 Service Account 對應的 ServiceAccountToken,來使用這些自定義的授權資訊。
三、容器健康檢查和恢複機制
3.1、livenessProbe字段
在 Kubernetes 中,可以為 Pod 裡的容器定義一個健康檢查“探針”(Probe)。這樣,kubelet 就會根據這個 Probe 的傳回值決定這個容器的狀态,而不是直接以容器鏡像是否運作(來自 Docker 傳回的資訊)作為依據。這種機制,是生産環境中保證應用健康存活的重要手段。
例子:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: test-liveness-exec
spec:
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
在這個 Pod 中,它在啟動之後做的第一件事,就是在 /tmp 目錄下建立了一個 healthy 檔案,以此作為自己已經正常運作的标志。而 30 s 過後,它會把這個檔案删除掉。
與此同時,我們定義了一個這樣的 livenessProbe(健康檢查)。它的類型是 exec,這意味着,它會在容器啟動後,在容器裡面執行一條我們指定的指令,比如:“cat /tmp/healthy”。這時,如果這個檔案存在,這條指令的傳回值就是 0,Pod 就會認為這個容器不僅已經啟動,而且是健康的。這個健康檢查,在容器啟動 5 s 後開始執行(initialDelaySeconds: 5),每 5 s 執行一次(periodSeconds: 5)。
下面來具體實踐一下這個過程:
首先,建立這個Pod:
$ kubectl create -f test-liveness-exec.yaml
然後,檢視這個Pod的狀态:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
test-liveness-exec 1/1 Running 0 10s
可以看到,由于已經通過了健康檢查,這個 Pod 就進入了 Running 狀态。
而 30 s 之後,我們再檢視一下 Pod 的 Events:
$ kubectl describe pod test-liveness-exec
你會發現,這個 Pod 在 Events 報告了一個異常:
FirstSeen LastSeen Count From SubobjectPath Type Reason Message
--------- -------- ----- ---- ------------- -------- ------ -------
2s 2s 1 {kubelet worker0} spec.containers{liveness} Warning Unhealthy Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
顯然,這個健康檢查探查到 /tmp/healthy 已經不存在了,是以它報告容器是不健康的。那麼接下來會發生什麼呢?
再次檢視這個Pod狀态:
$ kubectl get pod test-liveness-exec
NAME READY STATUS RESTARTS AGE
liveness-exec 1/1 Running 1 1m
此時,Pod并沒有進入Failed狀态,而是保持了Running狀态。這是為什麼呢?
其實,如果你注意到 RESTARTS 字段從 0 到 1 的變化,就明白原因了:這個異常的容器已經被 Kubernetes 重新開機了。在這個過程中,Pod 保持 Running 狀态不變。
需要注意的是:Kubernetes 中并沒有 Docker 的 Stop 語義。是以雖然是 Restart(重新開機),但實際卻是重新建立了容器。
這個功能就是 Kubernetes 裡的 Pod 恢複機制,也叫 restartPolicy。它是 Pod 的 Spec 部分的一個标準字段(pod.spec.restartPolicy),預設值是 Always,即:任何時候這個容器發生了異常,它一定會被重新建立。
但一定要強調的是,Pod 的恢複過程,永遠都是發生在目前節點上,而不會跑到别的節點上去。事實上,一旦一個 Pod 與一個節點(Node)綁定,除非這個綁定發生了變化(pod.spec.node 字段被修改),否則它永遠都不會離開這個節點。這也就意味着,如果這個主控端當機了,這個 Pod 也不會主動遷移到其他節點上去。
而如果你想讓 Pod 出現在其他的可用節點上,就必須使用 Deployment 這樣的“控制器”來管理 Pod,哪怕你隻需要一個 Pod 副本。這也是一個單Pod的Deployment與一個Pod最主要的差別。
而作為使用者,你還可以通過設定 restartPolicy,改變 Pod 的恢複政策。除了 Always,它還有 OnFailure 和 Never 兩種情況:
-
:在任何情況下,隻要容器不在運作狀态,就自動重新開機容器;Always
-
: 隻在容器 異常時才自動重新開機容器;OnFailure
-
: 從來不重新開機容器。Never
在實際使用時,我們需要根據應用運作的特性,合理設定這三種恢複政策。
比如,一個 Pod,它隻計算 1+1=2,計算完成輸出結果後退出,變成 Succeeded 狀态。這時,你如果再用 restartPolicy=Always 強制重新開機這個 Pod 的容器,就沒有任何意義了。
而如果你要關心這個容器退出後的上下文環境,比如容器退出後的日志、檔案和目錄,就需要将 restartPolicy 設定為 Never。因為一旦容器被自動重新建立,這些内容就有可能丢失掉了(被垃圾回收了)。
Kubernetes 的官方文檔,把 restartPolicy 和 Pod 裡容器的狀态,以及 Pod 狀态的對應關系,總結了非常複雜的一大堆情況。無需死記硬背,隻需記住下面兩個基本的設計原理即可:
- 隻要 Pod 的 restartPolicy 指定的政策允許重新開機異常的容器(比如:Always),那麼這個 Pod 就會保持 Running 狀态,并進行容器重新開機。否則,Pod 就會進入 Failed 狀态 。
- 對于包含多個容器的 Pod,隻有它裡面所有的容器都進入異常狀态後,Pod 才會進入 Failed 狀态。在此之前,Pod都是Running狀态。此時,Pod 的 READY 字段會顯示正常容器的個數。
如:
$ kubectl get pod test-liveness-exec
NAME READY STATUS RESTARTS AGE
liveness-exec 0/1 Running 1 1m
是以,假如一個 Pod 裡隻有一個容器,然後這個容器異常退出了。那麼,隻有當 restartPolicy=Never 時,這個 Pod 才會進入 Failed 狀态。而其他情況下,由于 Kubernetes 都可以重新開機這個容器,是以 Pod 的狀态保持 Running 不變。
而如果這個 Pod 有多個容器,僅有一個容器異常退出,它就始終保持 Running 狀态,哪怕即使 restartPolicy=Never。隻有當所有容器也異常退出之後,這個 Pod 才會進入 Failed 狀态。
其他情況,都可以以此類推出來。
回看livenessProbe,除了在容器中執行指令外,livenessProbe 也可以定義為發起 HTTP 或者 TCP 請求的方式,定義格式如下:
...
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: X-Custom-Header
value: Awesome
initialDelaySeconds: 3
periodSeconds: 3
...
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
是以,你的 Pod 其實可以暴露一個健康檢查 URL(比如 /healthz),或者直接讓健康檢查去檢測應用的監聽端口。這兩種配置方法,在 Web 服務類的應用中非常常用。
3.2、readinessProbe字段
在 Kubernetes 的 Pod 中,還有一個叫 readinessProbe 的字段。雖然它的用法與 livenessProbe 類似,但作用卻大不一樣。readinessProbe 檢查結果的成功與否,決定的這個 Pod 是不是能被通過 Service 的方式通路到,而并不影響 Pod 的生命周期。
疑問:Pod 的字段這麼多,又不可能全記住,Kubernetes 能不能自動給 Pod 填充某些字段呢?
這個需求實際上非常實用。比如,開發人員隻需要送出一個基本的、非常簡單的 Pod YAML,Kubernetes 就可以自動給對應的 Pod 對象加上其他必要的資訊,比如 labels,annotations,volumes 等等。而這些資訊,可以是運維人員事先定義好的。
這麼一來,開發人員編寫 Pod YAML 的門檻,就被大大降低了。
是以,這個叫作 PodPreset(Pod 預設定)的功能 已經出現在了 v1.11 版本的 Kubernetes 中。
例子:開發人員編寫了如下一個pod.yaml檔案
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
spec:
containers:
- name: website
image: nginx
ports:
- containerPort: 80
作為 Kubernetes 的初學者,你肯定眼前一亮:這不就是我最擅長編寫的、最簡單的 Pod 嘛。沒錯,這個 YAML 檔案裡的字段,想必你現在閉着眼睛也能寫出來。
可是,如果運維人員看到了這個 Pod,說這種 Pod 在生産環境裡根本不能用啊!
是以,這個時候,運維人員就可以定義一個 PodPreset 對象。在這個對象中,凡是他想在開發人員編寫的 Pod 裡追加的字段,都可以預先定義好。比如這個 preset.yaml:
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
name: allow-database
spec:
selector:
matchLabels:
role: frontend
env:
- name: DB_PORT
value: "6379"
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
在這個 PodPreset 的定義中,首先是一個 selector。這就意味着後面這些追加的定義,隻會作用于 selector 所定義的、帶有“role: frontend”标簽的 Pod 對象,這就可以防止“誤傷”。
然後,定義了一組 Pod 的 Spec 裡的标準字段,以及對應的值。比如,env 裡定義了 DB_PORT 這個環境變量,volumeMounts 定義了容器 Volume 的挂載目錄,volumes 定義了一個 emptyDir 的 Volume。
接下來,我們假定運維人員先建立了這個 PodPreset,然後開發人員才建立 Pod:
$ kubectl create -f preset.yaml
$ kubectl create -f pod.yaml
這時,Pod 運作起來之後,我們檢視一下這個 Pod 的 API 對象:
$ kubectl get pod website -o yaml
apiVersion: v1
kind: Pod
metadata:
name: website
labels:
app: website
role: frontend
annotations:
podpreset.admission.kubernetes.io/podpreset-allow-database: "resource version"
spec:
containers:
- name: website
image: nginx
volumeMounts:
- mountPath: /cache
name: cache-volume
ports:
- containerPort: 80
env:
- name: DB_PORT
value: "6379"
volumes:
- name: cache-volume
emptyDir: {}
這個時候,我們就可以清楚地看到,這個 Pod 裡多了新添加的 labels、env、volumes 和 volumeMount 的定義,它們的配置跟 PodPreset 的内容一樣。此外,這個 Pod 還被自動加上了一個 annotation 表示這個 Pod 對象被 PodPreset 改動過。
需要說明的是,PodPreset 裡定義的内容,隻會在 Pod API 對象被建立之前追加在這個對象本身上,而不會影響任何 Pod 的控制器的定義。
比如,我們現在送出的是一個 nginx-deployment,那麼這個 Deployment 對象本身是永遠不會被 PodPreset 改變的,被修改的隻是這個 Deployment 建立出來的所有 Pod。這一點請務必區厘清楚。
如果你定義了同時作用于一個 Pod 對象的多個 PodPreset,會發生什麼呢?
實際上,Kubernetes 項目會幫你合并(Merge)這兩個 PodPreset 要做的修改。而如果它們要做的修改有沖突的話,這些沖突字段就不會被修改。