這篇文章我們來深入了解Pod的基本概念及相關使用
一. Pod的設計思路
首先Pod是 Kubernetes 項目中最小的 API 對象,而Pod也是由容器組組成的。Pod 里的所有容器,共享的是同一個 Network Namespace,并且可以聲明共享同一個 Volume。凡是調度、網絡、存儲,以及安全相關的屬性,基本上是 Pod 級別的,而容器就成為Pod屬性中一個普通的字段定義。
我們可以這樣理解:容器是相當于未來云計算的進程,鏡像是安裝包,Pod則是傳統環境的機器,k8s是操作系統。pod是一個小家庭,它把密不可分的家庭成員(container)聚在一起,Infra container則是家長,掌管家中共通資源,家庭成員通過sidecar方式互幫互助,其樂融融~。
二. Pod的網絡通信Infra容器
在 Kubernetes 項目里,Pod 的實現需要使用一個中間容器,這個容器叫作 Infra 容器。在這個 Pod 中,Infra 容器永遠都是第一個被創建的容器,而其他用戶定義的容器,則通過 Join Network Namespace 的方式,與 Infra 容器關聯在一起。這樣的組織關系,可以用下面這樣一個示意圖來表達:
如上圖所示,這個 Pod 里有兩個用戶容器 A 和 B,還有一個 Infra 容器。很容易理解,在 Kubernetes 項目里,Infra 容器一定要占用極少的資源,所以它使用的是一個非常特殊的鏡像,叫作:k8s.gcr.io/pause。這個鏡像是一個用匯編語言編寫的、永遠處于“暫停”狀態的容器,解壓后的大小也只有 100~200 KB 左右。而在 Infra 容器“Hold 住”Network Namespace 后,用戶容器就可以加入到 Infra 容器的 Network Namespace 當中了
- 這也就意味著,對于 Pod 里的容器 A 和容器 B 來說:
- 它們可以直接使用 localhost 進行通信;
- 它們看到的網絡設備跟 Infra 容器看到的完全一樣;
- 一個 Pod 只有一個 IP 地址,也就是這個 Pod 的 Network Namespace 對應的 IP 地址;
- 當然,其他的所有網絡資源,都是一個 Pod 一份,并且被該 Pod 中的所有容器共享;
- Pod 的生命周期只跟 Infra 容器一致,而與容器 A 和 B 無關。
而對于同一個 Pod 里面的所有用戶容器來說,它們的進出流量,也可以認為都是通過 Infra 容器完成的。這一點很重要,因為將來如果你要為 Kubernetes 開發一個網絡插件時,應該重點考慮的是如何配置這個 Pod 的 Network Namespace,而不是每一個用戶容器如何使用你的網絡配置,這是沒有意義的。
這就意味著,如果你的網絡插件需要在容器里安裝某些包或者配置才能完成的話,是不可取的:Infra 容器鏡像的 rootfs 里幾乎什么都沒有,沒有你隨意發揮的空間。當然,這同時也意味著你的網絡插件完全不必關心用戶容器的啟動與否,而只需要關注如何配置 Pod,也就是 Infra 容器的 Network Namespace 即可。
三. 伴生容器與容器初始化
- sidecar: 伴生容器,指的就是我們可以在一個 Pod 中,啟動一個輔助容器,來完成一些獨立于主進程(主容器)之外的工作。
- Init Container:初始化容器 就是用來做初始化工作的容器,可以是一個或者多個,如果有多個的話,這些容器會按定義的順序依次執行,只有所有的Init Container執行完后,主容器才會被啟動。我們知道一個Pod里面的所有容器是共享數據卷和網絡命名空間的,所以Init Container里面產生的數據可以被主容器使用到的。
例如:我們現在有一個 Java Web 應用的 WAR 包,它需要被放在 Tomcat 的 webapps 目錄下運行起來。假如,你現在只能用 Docker 來做這件事情,那該如何處理這個組合關系呢?
- 一種方法是,把 WAR 包直接放在 Tomcat 鏡像的 webapps 目錄下,做成一個新的鏡像運行起來??墒牵@時候,如果你要更新 WAR 包的內容,或者要升級 Tomcat 鏡像,就要重新制作一個新的發布鏡像,非常麻煩。
- 另一種方法是,你壓根兒不管 WAR 包,永遠只發布一個 Tomcat 容器。不過,這個容器的 webapps 目錄,就必須聲明一個 hostPath 類型的 Volume,從而把宿主機上的 WAR 包掛載進 Tomcat 容器當中運行起來。不過,這樣你就必須要解決一個問題,即:如何讓每一臺宿主機,都預先準備好這個存儲有 WAR 包的目錄呢?這樣來看,你只能獨立維護一套分布式存儲系統了。
實際上,有了 Pod 之后,這樣的問題就很容易解決了。我們可以把 WAR 包和 Tomcat 分別做成鏡像,然后把它們作為一個 Pod 里的兩個容器“組合”在一起。這個 Pod 的配置文件如下所示:
apiVersion: v1
kind: Pod
metadata:
name: javaweb-2
spec:
initContainers:
- image: geektime/sample:v2
name: war
command: ["cp", "/sample.war", "/app"]
volumeMounts:
- mountPath: /app
name: app-volume
containers:
- image: geektime/tomcat:7.0
name: tomcat
command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
volumeMounts:
- mountPath: /root/apache-tomcat-7.0.42-v2/webapps
name: app-volume
ports:
- containerPort: 8080
hostPort: 8001
volumes:
- name: app-volume
emptyDir: {}
在這個 Pod 中,我們定義了兩個容器,第一個容器使用的鏡像是geektime/sample:v2,這個鏡像里只有一個 WAR 包(sample.war)放在根目錄下。而第二個容器則使用的是一個標準的 Tomcat 鏡像。
不過,你可能已經注意到,WAR 包容器的類型不再是一個普通容器,而是一個 Init Container 類型的容器。在 Pod 中,所有 Init Container 定義的容器,都會比 spec.containers 定義的用戶容器先啟動。并且,Init Container 容器會按順序逐一啟動,而直到它們都啟動并且退出了,用戶容器才會啟動。
所以,這個 Init Container 類型的 WAR 包容器啟動后,我執行了一句"cp /sample.war /app",把應用的 WAR 包拷貝到 /app 目錄下,然后退出。而后這個 /app 目錄,就掛載了一個名叫 app-volume 的 Volume。接下來就很關鍵了。Tomcat 容器,同樣聲明了掛載 app-volume 到自己的 webapps 目錄下。所以,等 Tomcat 容器啟動時,它的 webapps 目錄下就一定會存在 sample.war 文件:這個文件正是 WAR 包容器啟動時拷貝到這個 Volume 里面的,而這個 Volume 是被這兩個容器共享的。
像這樣,我們就用一種“組合”方式,解決了 WAR 包與 Tomcat 容器之間耦合關系的問題。實際上,這個所謂的“組合”操作,正是容器設計模式里最常用的一種模式,它的名字叫:sidecar。
在我們的這個應用 Pod 中,Tomcat 容器是我們要使用的主容器,而 WAR 包容器的存在,只是為了給它提供一個 WAR 包而已。所以,我們用 Init Container初始化容器的方式優先運行 WAR 包容器,扮演了一個 sidecar 的角色。
四. Pod的生命周期狀態
- Pending。這個狀態意味著,Pod 的 YAML 文件已經提交給了 Kubernetes,API 對象已經被創建并保存在 Etcd 當中。但是,這個 Pod 里有些容器因為某種原因而不能被順利創建。比如,調度不成功。
- Running。這個狀態下,Pod已經調度成功,跟一個具體的節點綁定。它包含的容器都已經創建成功,并且至少有一個正在運行中。
- Succeeded。這個狀態意味著,Pod 里的所有容器都正常運行完畢,并且已經退出了。這種情況在運行一次性任務時最為常見。
- Failed。這個狀態下,Pod 里至少有一個容器以不正常的狀態(非 0 的返回碼)退出。這個狀態的出現,意味著你得想辦法 Debug 這個容器的應用,比如查看 Pod 的 Events 和日志。
- Unknown。這是一個異常狀態,意味著 Pod 的狀態不能持續地被 kubelet 匯報給 kube-apiserver,這很有可能是主從節點(Master 和 Kubelet)間的通信出現了問題。
五. Pod的鉤子Hook
Kubernetes 為我們的容器提供了生命周期鉤子的,就是我們說的Pod Hook,Pod Hook 是由 kubelet 發起的,當容器中的進程啟動前或者容器中的進程終止之前運行,這是包含在容器的生命周期之中。我們可以同時為 Pod 中的所有容器都配置 hook。
Kubernetes 為我們提供了兩種鉤子函數:
- PostStart:這個鉤子在容器創建后立即執行。但是,并不能保證鉤子將在容器ENTRYPOINT之前運行,因為沒有參數傳遞給處理程序。主要用于資源部署、環境準備等。不過需要注意的是如果鉤子花費太長時間以至于不能運行或者掛起, 容器將不能達到running狀態。
- PreStop:這個鉤子在容器終止之前立即被調用。它是阻塞的,意味著它是同步的, 所以它必須在刪除容器的調用發出之前完成。主要用于優雅關閉應用程序、通知其他系統等。如果鉤子在執行期間掛起, Pod階段將停留在running狀態并且永不會達到failed狀態。
如果PostStart或者PreStop鉤子失敗, 它會殺死容器。所以我們應該讓鉤子函數盡可能的輕量。當然有些情況下,長時間運行命令是合理的, 比如在停止容器之前預先保存狀態。
另外我們有兩種方式來實現上面的鉤子函數:
- Exec - 用于執行一段特定的命令,不過要注意的是該命令消耗的資源會被計入容器。
- HTTP - 對容器上的特定的端點執行HTTP請求
示例
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"]
在這個例子中,我們在容器成功啟動之后定義了Lifecycle字段,即在 /usr/share/message 里寫入了一句“歡迎信息”(即 postStart 定義的操作)。而在這個容器被刪除之前,我們則先調用了 nginx 的退出指令(即 preStop 定義的操作),從而實現了容器的“優雅退出”
六. Pod的健康檢查
在 Kubernetes 中,你可以為 Pod 里的容器定義一個健康檢查“探針”(Probe)。這樣,kubelet 就會根據這個 Probe 的返回值決定這個容器的狀態,而不是直接以容器進行是否運行(來自 Docker 返回的信息)作為依據。這種機制,是生產環境中保證應用健康存活的重要手段。
- liveness probe(存活探針):確定你的應用程序是否正在運行,通俗點將就是是否還活著。一般來說,如果你的程序一旦崩潰了, Kubernetes 就會立刻知道這個程序已經終止了,然后就會重啟這個程序。而我們的 liveness probe 的目的就是來捕獲到當前應用程序還沒有終止,還沒有崩潰,如果出現了這些情況,那么就重啟處于該狀態下的容器,使應用程序在存在 bug 的情況下依然能夠繼續運行下去。
- readiness probe(可讀性探針):確定容器是否已經就緒可以接收流量過來了。這個探針通俗點講就是說是否準備好了,現在可以開始工作了。只有當 Pod 中的容器都處于就緒狀態的時候 kubelet 才會認定該 Pod 處于就緒狀態,因為一個 Pod 下面可能會有多個容器。當然 Pod 如果處于非就緒狀態,那么我們就會將他從我們的工作隊列(實際上就是我們后面需要重點學習的 Service)中移除出來,這樣我們的流量就不會被路由到這個 Pod 里面來了,決定的這個 Pod 是不是能被通過 Service 的方式訪問到,而并不影響 Pod 的生命周期。。
探針支持的配置方式:
- exec:執行一段命令
- http:檢測某個 http 請求
- tcpSocket:檢查端口, kubelet 將嘗試在指定端口上打開容器的套接字。如果可以建立連接,容器被認為是健康的,如果不能就認為是失敗的。
示例
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 就會認為這個容器不僅已經啟動,而且是健康的,如果返回值為非0,那么kubelet將會重啟這個容器。initialDelaySeconds:5
,在容器啟動 5 s 后開始執行健康檢查,periodSeconds: 5
每 5 s 執行一次。
創建這個Pod查看過程
$ kubectl get pods
test-liveness-exec 1/1 Running 0 64s
30s后我們查看Pod的Event
$ kubectl describe pod test-liveness-exec
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m5s default-scheduler Successfully assigned default/test-liveness-exec to k8s-node01
Warning Unhealthy 66s (x3 over 76s) kubelet, k8s-node01 Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
通過Event發現健康檢查探查到 /tmp/healthy 已經不存在了,所以它報告容器是不健康的,我們在看一下Pod的當前狀態是否正常?
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
test-liveness-exec 1/1 Running 1 1m1s
我們可以看到Pod的狀態仍然是Running正常的,但是RESTARTS
字段已經由0已經變成1了,這是因為Pod已經被kubelet重啟了。
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 服務類的應用中非常常用。
七. Pod的故障恢復機制
上面例子中健康檢查如果沒有通過,kubelet則會重啟這個容器,這是因為默認Pod的恢復機制為Always,其實它是 Pod 的 Spec 部分的一個標準字段(pod.spec.restartPolicy)
,我們可以根據自己的需求來定通過設置 restartPolicy改變 Pod 的恢復策略:
- Always:在任何情況下,只要容器不在運行狀態,就自動重啟容器;
- OnFailure: 只在容器 異常時才自動重啟容器;
- Never: 從來不重啟容器。
基本的設計原理:
只要 Pod 的 restartPolicy 指定的策略允許重啟異常的容器(比如:Always),那么這個 Pod 就會保持 Running 狀態,并進行容器重啟。否則,Pod 就會進入 Failed 狀態 。
對于包含多個容器的 Pod,只有它里面所有的容器都進入異常狀態后,Pod 才會進入 Failed 狀態。在此之前,Pod 都是 Running 狀態。此時,Pod 的 READY 字段會顯示正常容器的個數
八. Pod的常用屬性定義
除了上文定義的一些屬性字段,我們常用的屬性還有以下字段定義:
- NodeSelector:是一個供用戶將 Pod 與 Node 進行綁定的字段
- ImagePullPolicy:[Always | Never | IfNotPresent] # 【String】 每次都嘗試重新拉取鏡像 | 僅使用本地鏡像 | 如果本地有鏡像則使用,沒有則拉取
- Volume:volume是Pod中多個容器訪問的共享目錄。volume被定義在pod上,被這個pod的多個容器掛載到相同或不同的路徑下。一般volume被用于持久化pod產生的數據。Kubernetes提供了眾多的volume類型,包括emptyDir、hostPath、nfs、glusterfs、cephfs、ceph rbd等。
- NodeName:一旦 Pod 的這個字段被賦值,Kubernetes 項目就會被認為這個 Pod 已經經過了調度,調度的結果就是賦值的節點名字。所以,這個字段一般由調度器負責設置,但用戶也可以設置它來“騙過”調度器,當然這個做法一般是在測試或者調試的時候才會用到。
- HostAliases:定義了 Pod 的 hosts 文件(比如 /etc/hosts)里的內容。
網上資料:
總結:在這篇文章中我們深度了解了Pod的設計模式和字段屬性定義,其實Pod 這個看似復雜的 API 對象,實際上就是對容器的進一步抽象和封裝而已,Pod 對象就是容器的升級版。它對容器進行了組合,添加了更多的屬性和字段。
上篇文章:k8s三 | 使用YAML文件定義資源對象
系列文章:深入理解Kuerneters
參考資料:深入剖析Kubernetes-張磊、從Docker到Kubernetes進階-陽明