現(xiàn)在,你已經(jīng)非常清楚:Kubernetes 項(xiàng)目中的最小編排單位是 Pod,而不是容器。將這個(gè)設(shè)計(jì)落實(shí)到 API 對象上,容(Container)就成了 Pod 屬性里的一個(gè)普通的字段。那么,一個(gè)很自然的問題就是:到底哪些屬性屬于 Pod 對象,而又有哪些屬性屬于 Container 呢?
要徹底理解這個(gè)問題,你就一定要牢記我在上一篇文章中提到的一個(gè)結(jié)論:Pod 扮演的是傳統(tǒng)部署環(huán)境里“虛擬機(jī)”的角色。這樣的設(shè)計(jì),是為了使用戶從傳統(tǒng)環(huán)境(虛擬機(jī)環(huán)境)向 Kubernetes(容器環(huán)境)的遷移,更加平滑。
而如果你能把 Pod 看成傳統(tǒng)環(huán)境里的“機(jī)器”、把容器看作是運(yùn)行在這個(gè)“機(jī)器”里的“用戶程序”,那么很多關(guān)于 Pod 對象的設(shè)計(jì)就非常容易理解了。
比如,凡是調(diào)度、網(wǎng)絡(luò)、存儲(chǔ),以及安全相關(guān)的屬性,基本上是 Pod 級別的。
這些屬性的共同特征是,它們描述的是“機(jī)器”這個(gè)整體,而不是里面運(yùn)行的“程序”。比如,配置這個(gè)“機(jī)器”的網(wǎng)卡(即:Pod 的網(wǎng)絡(luò)定義),配置這個(gè)“機(jī)器”的磁盤(即:Pod 的存儲(chǔ)定義),配置這個(gè)“機(jī)器”的防火墻(即:Pod 的安全定義)。更不用說,這臺“機(jī)器”運(yùn)行在哪個(gè)服務(wù)器之上(即:Pod 的調(diào)度)。
接下來,我就先為你介紹 Pod 中幾個(gè)重要字段的含義和用法。
NodeSelector:是一個(gè)供用戶將 Pod 與 Node 進(jìn)行綁定的字段,用法如下所示:
apiVersion: v1
kind: Pod
...
spec:
nodeSelector:
disktype: ssd
這樣的一個(gè)配置,意味著這個(gè) Pod 永遠(yuǎn)只能運(yùn)行在攜帶了“disktype: ssd”標(biāo)簽(Label)的節(jié)點(diǎn)上;否則,它將調(diào)度失敗。
NodeName:一旦 Pod 的這個(gè)字段被賦值,Kubernetes 項(xiàng)目就會(huì)被認(rèn)為這個(gè) Pod 已經(jīng)經(jīng)過了調(diào)度,調(diào)度的結(jié)果就是賦值的節(jié)點(diǎn)名字。所以,這個(gè)字段一般由調(diào)度器負(fù)責(zé)設(shè)置,但用戶也可以設(shè)置它來“騙過”調(diào)度器,當(dāng)然這個(gè)做法一般是在測試或者調(diào)試的時(shí)候才會(huì)用到。
HostAliases:定義了 Pod 的 hosts 文件(比如 /etc/hosts)里的內(nèi)容,用法如下:
apiVersion: v1
kind: Pod
...
spec:
hostAliases:
- ip: "10.1.2.3"
hostnames:
- "foo.remote"
- "bar.remote"
...
在這個(gè) Pod 的 YAML 文件中,我設(shè)置了一組 IP 和 hostname 的數(shù)據(jù)。這樣,這個(gè) Pod 啟動(dòng)后,/etc/hosts 文件的內(nèi)容將如下所示:
cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
...
10.244.135.10 hostaliases-pod
10.1.2.3 foo.remote
10.1.2.3 bar.remote
其中,最下面兩行記錄,就是我通過 HostAliases 字段為 Pod 設(shè)置的。需要指出的是,在 Kubernetes 項(xiàng)目中,如果要設(shè)置 hosts 文件里的內(nèi)容,一定要通過這種方法。否則,如果直接修改了 hosts 文件的話,在 Pod 被刪除重建之后,kubelet 會(huì)自動(dòng)覆蓋掉被修改的內(nèi)容。
除了上述跟“機(jī)器”相關(guān)的配置外,你可能也會(huì)發(fā)現(xiàn):
凡是跟容器的 Linux Namespace 相關(guān)的屬性,也一定是 Pod 級別的。
這個(gè)原因也很容易理解:Pod 的設(shè)計(jì),就是要讓它里面的容器盡可能多地共享 Linux Namespace,僅保留必要的隔離和限制能力。這樣,Pod 模擬出的效果,就跟虛擬機(jī)里程序間的關(guān)系非常類似了。
舉個(gè)例子,在下面這個(gè) Pod 的 YAML 文件中,我定義了 shareProcessNamespace=true:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
shareProcessNamespace: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
這就意味著這個(gè) Pod 里的容器要共享 PID Namespace。
而在這個(gè) YAML 文件中,我還定義了兩個(gè)容器:一個(gè)是 nginx 容器,一個(gè)是開啟了 tty 和 stdin 的 shell 容器。
在 Pod 的 YAML 文件里聲明開啟它們倆,其實(shí)等同于設(shè)置了 docker run 里的 -it(-i 即 stdin,-t 即 tty)參數(shù)。
如果還是不太理解它們倆的作用的話,可以直接認(rèn)為 tty 就是 Linux 給用戶提供的一個(gè)常駐小程序,用于接收用戶的標(biāo)準(zhǔn)輸入,返回操作系統(tǒng)的標(biāo)準(zhǔn)輸出。當(dāng)然,為了能夠在 tty 中輸入信息,你還需要同時(shí)開啟 stdin(標(biāo)準(zhǔn)輸入流)。
于是,這個(gè) Pod 被創(chuàng)建后,你就可以使用 shell 容器的 tty 跟這個(gè)容器進(jìn)行交互了。我們一起實(shí)踐一下:
$ kubectl create -f nginx.yaml
接下來,我們使用 kubectl attach 命令,連接到 shell 容器的 tty 上:
kubectl attach -it nginx -c shell
這樣,我們就可以在 shell 容器里執(zhí)行 ps 指令,查看所有正在運(yùn)行的進(jìn)程:
$ kubectl attach -it nginx -c shell
/ # ps ax
PID USER TIME COMMAND
1 root 0:00 /pause
8 root 0:00 nginx: master process nginx -g daemon off;
14 101 0:00 nginx: worker process
15 root 0:00 sh
21 root 0:00 ps ax
可以看到,在這個(gè)容器里,我們不僅可以看到它本身的 ps ax 指令,還可以看到 nginx 容器的進(jìn)程,以及 Infra 容器的 /pause 進(jìn)程。這就意味著,整個(gè) Pod 里的每個(gè)容器的進(jìn)程,對于所有容器來說都是可見的:它們共享了同一個(gè) PID Namespace。
類似地,凡是 Pod 中的容器要共享宿主機(jī)的 Namespace,也一定是 Pod 級別的定義,比如:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
hostNetwork: true
hostIPC: true
hostPID: true
containers:
- name: nginx
image: nginx
- name: shell
image: busybox
stdin: true
tty: true
在這個(gè) Pod 中,我定義了共享宿主機(jī)的 Network、IPC 和 PID Namespace。這就意味著,這個(gè) Pod 里的所有容器,會(huì)直接使用宿主機(jī)的網(wǎng)絡(luò)、直接與宿主機(jī)進(jìn)行 IPC 通信、看到宿主機(jī)里正在運(yùn)行的所有進(jìn)程。
當(dāng)然,除了這些屬性,Pod 里最重要的字段當(dāng)屬“Containers”了。而在上一篇文章中,我還介紹過“Init Containers”。其實(shí),這兩個(gè)字段都屬于 Pod 對容器的定義,內(nèi)容也完全相同,只是 Init Containers 的生命周期,會(huì)先于所有的 Containers,并且嚴(yán)格按照定義的順序執(zhí)行。
Kubernetes 項(xiàng)目中對 Container 的定義,和 Docker 相比并沒有什么太大區(qū)別。我在前面的容器技術(shù)概念入門系列文章中,和你分享的 Image(鏡像)、Command(啟動(dòng)命令)、workingDir(容器的工作目錄)、Ports(容器要開發(fā)的端口),以及 volumeMounts(容器要掛載的 Volume)都是構(gòu)成 Kubernetes 項(xiàng)目中 Container 的主要字段。不過在這里,還有這么幾個(gè)屬性值得你額外關(guān)注。
首先,是 ImagePullPolicy 字段。它定義了鏡像拉取的策略。而它之所以是一個(gè) Container 級別的屬性,是因?yàn)槿萜麋R像本來就是 Container 定義中的一部分。
ImagePullPolicy 的值默認(rèn)是 Always,即每次創(chuàng)建 Pod 都重新拉取一次鏡像。另外,當(dāng)容器的鏡像是類似于 nginx 或者 nginx:latest 這樣的名字時(shí),ImagePullPolicy 也會(huì)被認(rèn)為 Always。
最新版本 v1.16 默認(rèn)值是 IfNotPresent
而如果它的值被定義為 Never 或者 IfNotPresent,則意味著 Pod 永遠(yuǎn)不會(huì)主動(dòng)拉取這個(gè)鏡像,或者只在宿主機(jī)上不存在這個(gè)鏡像時(shí)才拉取。
其次,是 Lifecycle 字段。它定義的是 Container Lifecycle Hooks。顧名思義,Container Lifecycle Hooks 的作用,是在容器狀態(tài)發(fā)生變化時(shí)觸發(fā)一系列“鉤子”。我們來看這樣一個(gè)例子:
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
containers:
- name: lifecycle-demo-container
image: nginx
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
這是一個(gè)來自 Kubernetes 官方文檔的 Pod 的 YAML 文件。它其實(shí)非常簡單,只是定義了一個(gè) nginx 鏡像的容器。不過,在這個(gè) YAML 文件的容器(Containers)部分,你會(huì)看到這個(gè)容器分別設(shè)置了一個(gè) postStart 和 preStop 參數(shù)。這是什么意思呢?
先說 postStart 吧。它指的是,在容器啟動(dòng)后,立刻執(zhí)行一個(gè)指定的操作。需要明確的是,postStart 定義的操作,雖然是在 Docker 容器 ENTRYPOINT 執(zhí)行之后,但它并不嚴(yán)格保證順序。也就是說,在 postStart 啟動(dòng)時(shí),ENTRYPOINT 有可能還沒有結(jié)束。
當(dāng)然,如果 postStart 執(zhí)行超時(shí)或者錯(cuò)誤,Kubernetes 會(huì)在該 Pod 的 Events 中報(bào)出該容器啟動(dòng)失敗的錯(cuò)誤信息,導(dǎo)致 Pod 也處于失敗的狀態(tài)。
而類似地,preStop 發(fā)生的時(shí)機(jī),則是容器被殺死之前(比如,收到了 SIGKILL 信號)。而需要明確的是,preStop 操作的執(zhí)行,是同步的。所以,它會(huì)阻塞當(dāng)前的容器殺死流程,直到這個(gè) Hook 定義操作完成之后,才允許容器被殺死,這跟 postStart 不一樣。
所以,在這個(gè)例子中,我們在容器成功啟動(dòng)之后,在 /usr/share/message 里寫入了一句“歡迎信息”(即 postStart 定義的操作)。而在這個(gè)容器被刪除之前,我們則先調(diào)用了 nginx 的退出指令(即 preStop 定義的操作),從而實(shí)現(xiàn)了容器的“優(yōu)雅退出”。
在熟悉了 Pod 以及它的 Container 部分的主要字段之后,我再和你分享一下這樣一個(gè)的 Pod 對象在 Kubernetes 中的生命周期。
Pod 生命周期的變化,主要體現(xiàn)在 Pod API 對象的 Status 部分,這是它除了 Metadata 和 Spec 之外的第三個(gè)重要字段。其中,pod.status.phase,就是 Pod 的當(dāng)前狀態(tài),它有如下幾種可能的情況:
1
Pending。這個(gè)狀態(tài)意味著,Pod 的 YAML 文件已經(jīng)提交給了 Kubernetes,API 對象已經(jīng)被創(chuàng)建并保存在 Etcd 當(dāng)中。但是,這個(gè) Pod 里有些容器因?yàn)槟撤N原因而不能被順利創(chuàng)建。比如,調(diào)度不成功。
2
Running。這個(gè)狀態(tài)下,Pod 已經(jīng)調(diào)度成功,跟一個(gè)具體的節(jié)點(diǎn)綁定。它包含的容器都已經(jīng)創(chuàng)建成功,并且至少有一個(gè)正在運(yùn)行中。
3
Succeeded。這個(gè)狀態(tài)意味著,Pod 里的所有容器都正常運(yùn)行完畢,并且已經(jīng)退出了。這種情況在運(yùn)行一次性任務(wù)時(shí)最為常見。
4
Failed。這個(gè)狀態(tài)下,Pod 里至少有一個(gè)容器以不正常的狀態(tài)(非 0 的返回碼)退出。這個(gè)狀態(tài)的出現(xiàn),意味著你得想辦法 Debug 這個(gè)容器的應(yīng)用,比如查看 Pod 的 Events 和日志。
5
Unknown。這是一個(gè)異常狀態(tài),意味著 Pod 的狀態(tài)不能持續(xù)地被 kubelet 匯報(bào)給 kube-apiserver,這很有可能是主從節(jié)點(diǎn)(Master 和 Kubelet)間的通信出現(xiàn)了問題。
更進(jìn)一步地,Pod 對象的 Status 字段,還可以再細(xì)分出一組 Conditions。這些細(xì)分狀態(tài)的值包括:PodScheduled、Ready、Initialized,以及 Unschedulable。它們主要用于描述造成當(dāng)前 Status 的具體原因是什么。
比如,Pod 當(dāng)前的 Status 是 Pending,對應(yīng)的 Condition 是 Unschedulable,這就意味著它的調(diào)度出現(xiàn)了問題。
而其中,Ready 這個(gè)細(xì)分狀態(tài)非常值得我們關(guān)注:它意味著 Pod 不僅已經(jīng)正常啟動(dòng)(Running 狀態(tài)),而且已經(jīng)可以對外提供服務(wù)了。這兩者之間(Running 和 Ready)是有區(qū)別的,你不妨仔細(xì)思考一下。
Pod 的這些狀態(tài)信息,是我們判斷應(yīng)用運(yùn)行情況的重要標(biāo)準(zhǔn),尤其是 Pod 進(jìn)入了非“Running”狀態(tài)后,你一定要能迅速做出反應(yīng),根據(jù)它所代表的異常情況開始跟蹤和定位,而不是去手忙腳亂地查閱文檔。
實(shí)際上,Pod API 對象是整個(gè) Kubernetes 體系中最核心的一個(gè)概念,也是后面講解各種控制器時(shí)都要用到的。