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