Kubernetes Pod詳解

語言: CN / TW / HK

由於本文篇幅過大,下圖是本文涵蓋的一些內容,方便大家從總到分的進行理解和學習:

image.png

為什麼要有Pod

Pod是什麼?

Pod是Kubernetes叢集中最小的排程單位,具有以下特點:

  1. Kuberenetes叢集中最小的部署單位

  2. 一個Pod中可以擁有多個容器

  3. 同一個Pod共享網路和儲存

  4. 每一個Pod都會有一個Pause容器

  5. Pod的生命週期只跟Pause容器一致,與其他應用容器無關

為什麼要有Pod的存在?

  1. 容器不具備處理多程序的能力

  2. 很多應用程式相互之間並不是獨立執行的,有著密切的協作關係,必須部署在一個節點上

Pod共享機制

Pod實現機制?

Pod中可以共享網路,並且可以宣告共享儲存。

  • 共享儲存是通過資料卷Volume的方式進行共享,該Volume可以定義在Pod級別,在容器中進行掛載即可實現共享

  • 共享網路是通過共享Network Namespace的方式進行的,具體的方式是通過一個稱為Pause容器來實現的

Pause容器的作用?

Pod中通過共享Network Namespace的方式進行網路的共享,但是如果是以下方式進行Network Namespace共享會有問題:

```shell

A容器共享B容器的網路

$ docker run --net=B --volumes-from=B --name=A image-A ...

```

上述的方式我們是通過A容器共享了B容器的網路,本質上是A加入到了B的Network Namespace中,但是這種方式需要容器B必須先於A啟動。

為了解決應用容器的上述啟動順序問題,Kubernetes引入了一箇中間容器,叫Pause容器(或稱Infra容器),Pause容器時Pod中第一個被建立的容器,其他使用者容器都會以Join Network Namespace的方式與Pause容器關聯起來,實現同一個Pod中所有容器共享網路。

共享儲存示例

```yaml

apiVersion: v1

kind: Pod

metadata:

labels:

run: busybox

name: busybox

spec:

containers:

  • image: busybox

name: busybox-write

command: ["/bin/sh"]

args: ["-c", "while true; do date >> /data/result.txt; sleep 5; done;"]

容器內掛載資料卷

volumeMounts:

  • name: data

容器內目錄

mountPath: /data

  • image: busybox

name: busybox-read

command: ["/bin/sh"]

args: ["-c", "while true; do date; sleep 10; done;"]

容器內掛載資料卷

volumeMounts:

  • name: data

容器內目錄

mountPath: /data

定義共享資料卷

volumes:

  • name: data

emptyDir: {}

```

  • spec.volumes欄位我們定義了整個Pod共享的資料卷

  • spec.containers.volumeMounts欄位用來掛載資料卷

```shell

建立POD

$ kubect apply -f 001.yaml

進入只讀容器busybox-read檢視檔案,如果共享儲存是對的,我們可以在只讀容器/data找到result.txt檔案

$ kubectl exec busybox -c busybox-read -i -t -- sh -il

$ tail -f -n 10 /data/result.txt

```

image.png

可以看到/data/result.txt每5s中會多一行,這是因為我們的busybox-write每5s中往該容器寫入時間,通過資料卷volume我們實現了同一個Pod內容器的儲存共享。

Pod引數

Pod映象拉取策略

Pod映象拉取策略是定義在容器級別的,由spec.containers.imagePullPolicy定義,如下:

```yaml

apiVersion: v1

kind: Pod

metadata:

name: busybox

spec:

containers:

  • image: busybox

定義映象拉取策略

imagePullPolicy: Always

name: busybox-write

```

imagePullPolicy預設值為Always,目前主要有三種欄位:

  • Always:每次建立Pod都需要重新拉取映象

  • Never:永遠不會主動拉取映象

  • IfNotPresent:Pod所在的宿主機上不存在這個映象時拉取

當容器的映象是類似busybox或者busybox:latest這樣的名字時,imagePullPolicy會被認為Always。

Pod容器重啟策略

```shell

apiVersion: v1

kind: Pod

metadata:

name: busybox

spec:

重啟策略

restartPolicy: Always

containers:

  • image: busybox

name: busybox-write

```

容器的重啟策略定義在Pod級別,欄位為spec.restartPolicy,該欄位有以下值:

  • Always: 當容器失效時,由Kubelet自動重啟容器

  • OnFailure:當容器終止執行且退出碼不為0時,由Kubelet自動重啟該容器

  • Never:不論容器執行狀態如何,都不會重啟容器

Pod資源限制

Kubernetes對Pod進行排程的時候,我們可以對Pod進行一些定義,來干涉排程器Scheduler的分配邏輯。

資源型別

在Kubernetes中,資源型別有以下兩種:

  • 可壓縮資源:此類資源不足時,Pod只會飢餓,不會退出,比如CPU

  • 不可壓縮資源:此類資源不足時,Pod會被核心殺掉,比如記憶體

資源配置

CPU和記憶體資源的限額定義都在container級別,Pod整體資源的配置是由Container的配置值累加得到。

定義資源示例

```yaml

apiVersion: v1

kind: Pod

metadata:

name: busybox

spec:

containers:

  • image: busybox

name: busybox-write

command: ["/bin/sh"]

args: ["-c", "while true; do date >> /data/result.txt; sleep 5; done;"]

resources:

limits:

cpu: "500m"

memory: "50Mi"

requests:

cpu: "500m"

memory: "10Mi"

```

Pod中的資源限制如上述檔案所示,還要分為兩類:

  • requests:kube-scheduler在進行排程的時候會按照該值去檢查Kubernetes的node是否符合要求

  • limits:Pod在實際執行時能夠使用到的資源上限(真正設定Cgroups限制的時候)

```shell

$ free -h

```

image.png

通過上圖我們可以看到我們的node-1節點記憶體大小為1.9Gi,我們把resources.limits.memory和resources.requests.memory設定成2.0Gi。

```yaml

apiVersion: v1

kind: Pod

metadata:

name: busybox

spec:

containers:

  • image: busybox

name: busybox-write

command: ["/bin/sh"]

args: ["-c", "while true; do date >> /data/result.txt; sleep 5; done;"]

resources:

limits:

cpu: "500m"

memory: "2.0Gi"

requests:

cpu: "500m"

memory: "2.0Gi"

```

```shell

$ kubectl apply -f 001.yaml

$ kubectl get pods -o wide

$ kubectl describe pod busybox

```

image.png

image.png

通過上圖可以看出,buxbox的Pod沒有被排程到任何節點,一直處於Pending狀態,然後通過檢視pod的Event可以看出原因:一共有兩個節點,其中1個節點(master)被打上了不允許排程的汙點,另一個節點1個(k8s-node-01)記憶體資源不足,因此Pod不能被成功排程。

Pod QoS類別的劃分?

  • Guaranteed類別:Pod中的每一個container中都設定了相同的requests和limit

  • Burstable類別:不滿足Guaranteed類別,但Pod中至少一個containers設定了requests

  • BestEffort類別:Pod中沒有設定requests和limits

為什麼要進行Pod QoS劃分?

QoS主要用來,當宿主機資源發生緊張時,Kubelet對Pod進行Eviction(資源回收)時需要使用。

什麼情況會觸發Eviction?

Kubernetes管理的宿主機上的不可壓縮資源短缺時,將有可能觸發Eviction,常見有以下幾種:

  • 可用記憶體(memory.avaliable):可用記憶體低於閥值,預設閥值100Mi

  • 可用磁碟空間(nodefs.avaliable):可用空間低於閥值,預設閥值10%

  • 可用磁碟空間(nodefs.inodesFree):linux節點,可用空間低於閥值,預設閥值5%

  • 容器執行時映象儲存空間(imagefs.available):可用空間低於閥值,預設閥值15%

上述閥值也可以通過以下命令進行修改

```shell

--eviction-hard 指定Hard Eviction

--eviction-soft、--eviction-soft-grace-period和--eviction-max-pod-grace-period共同指定了Soft Eviction

$ kubelet --eviction-hard=imagefs.available<10%,memory.available<500Mi,nodefs.available<5%,nodefs.inodesFree<5% --eviction-soft=imagefs.available<30%,nodefs.available<10% --eviction-soft-grace-period=imagefs.available=2m,nodefs.available=2m --eviction-max-pod-grace-period=600

```

Hard Eviction和Soft Eviction的區別?

  • Hard Eviction:Eviction在達到閥值時會立即進行資源回收

  • Soft Eviction:Eviction在達到閥值時會等待一段時間才開始進行Pod驅逐,該時間由eviction-soft-grace-period和eviction-max-pod-grace-period中的最小值決定

Eviction對Pod驅逐的策略是什麼?

當Eviction被觸發以後,Kubelet將會挑選Pod進行刪除,如何挑選就需要參考QoS類別:

  • 首先被刪除的是BestEffort類別的Pod

  • 其次是屬於Burstable類別,並且發生飢餓的資源使用量超過了requests的Pod

  • 最後是Guaranteed類別,Guaranteed類別的Pod只有資源使用量超過了limits的限制或者宿主機處在記憶體緊張的時候才會被Eviction。

對於每一種QoS類別的Pod,Kubernetes還會按照Pod優先順序進行Pod的選擇

CPU限額m的設定是什麼意思?

Kubernetes推薦將CPU限額設定為分數,500m指的是500 millicpu,也就是0.5個CPU,也就是會獲得一個CPU一半的計算能力。

Pod健康檢查

什麼是健康檢查?

Pod裡面的容器可以定義一個健康檢測的探針(Probe),Kubelet會根據這個Probe返回的資訊決定這個容器的狀態,而不是以容器是否執行為標誌。健康檢查是用來保證是否健康存活的重要機制。

通過健康檢測和重啟策略,Kubernetes會對有問題的容器進行重啟。

探針探測的方式?

使用探針檢測容器有四種不同的方式:

  • exec:容器內執行指定命令,如果命令退出時返回碼為0則認為診斷成功

  • grpc:使用grpc進行遠端呼叫,如果響應的狀態位SERVING,則認為檢查成功

  • httpGet:對容器的IP地址上指定埠和路徑執行HTTP Get請求,如果狀態響應碼的值大於等於200且小於400,則認為檢測成功

  • tcpSocket:對容器上的IP地址和埠進行TCP檢查,如果埠開啟,則檢查成功

探測會有三種結果:

  • Success:通過檢查

  • Failure:容器未通過診斷

  • Unknown:診斷失敗,不會採取行動

探針型別?

Kubernetes中有三種探針:

  • livenessProbe:表示容器是否在執行,如果存活狀態探針檢測失敗,kubelet會殺死容器,並根據重啟策略restartPolicy來進行響應的容器操作,如果容器不提供存活探針,預設狀態位Success

  • readinessProbe:表示容器是否準備好提供服務,如果就緒探針檢測失敗,與該Pod相關的服務控制器會下掉該Pod的IP地址(比如Service服務)

  • startupProbe:表示容器中的應用是否已經啟動,如果啟用該探針,其他探針會被禁用。如果啟動探測失敗,kubelet會殺死容器並根據重啟策略進行重啟

存活探針livenessProbe

```yaml

apiVersion: v1

kind: Pod

metadata:

name: liveness-exec

spec:

containers:

  • name: liveness

image: busybox

args:

  • /bin/sh

  • -c

  • touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600

存活探針

livenessProbe:

exec:

command:

  • cat

  • /tmp/healthy

延遲探測,第一次探測需要等待5s

initialDelaySeconds: 5

每隔5s進行一次探測

periodSeconds: 5

```

存活探針主要由幾部分組成:

  • 探針探測的方式:上面的例子中我們使用了exec的方式

  • initialDelaySeconds:延遲探測的時間,預設值0,最小值0

  • periodSeconds:探測的週期,預設值10,最小值1

  • timeoutSeconds:探測超時後等待多少s,預設值為1

  • successThreshold:探測器失敗後,被認為成功的最小連續成功數,預設1,最小值為1,存活探測器和啟動探測器這個值必須為1

  • failureThreshold:當探測失敗時,Kubernetes的重試次數,預設值為3,最小值是1。對存活探測器來說,超過該次數會重啟容器;對於就緒探測器來說,超過該次數Pod會被打上未就緒的標籤

```shell

$ kubectl apply -f exec-liveness.yaml

$ kubectl get pods -o wide

$ kubectl describe pod liveness-exec

```

image.png

image.png

可以看到,在我們提交我們的YAML給Kuberenets以後,我們的Pod成功建立,並且容器成功啟動,30s以後我們再檢視應用Pod的Events,我們的健康檢測會失敗如下:

image.png

可以看到在30s以後,我們進行了三次存活探測都失敗了,然後容器被殺掉重啟。

就緒探針readinessProbe

有些應用在啟動後需要載入大量配置檔案或者資料,或者需要等待外部服務,此時我們並不想殺掉應用讓其重啟,而是不想給他傳送請求,此時就可以用到就緒探針。

```yaml

apiVersion: v1

kind: Pod

metadata:

name: readiness-exec

spec:

containers:

  • name: liveness

image: busybox

args:

  • /bin/sh

  • -c

  • touch /tmp/healthy; sleep 20; rm -rf /tmp/healthy; sleep 600;

就緒探針

readinessProbe:

exec:

command:

  • cat

  • /tmp/healthy

延遲探測,第一次探測需要等待5s

initialDelaySeconds: 5

每隔5s進行一次探測

periodSeconds: 5

```

```shell

$ kubectl apply -f exec-readiness.yaml

$ kubectl get pods -o wide

$ kubectl describe pod readiness-exec

```

image.png

image.png

通過上圖可以看出,我們的readiness-exec的Pod成功啟動並通過了健康檢測並且容器也準備好接收資料(READY為1/1)。但在20s以後,我們再來觀察我們的Pod,此時Pod的狀態如下:

image.png

image.png

通過上圖可以看出,Pod中的容器健康檢測失敗,同時容器就緒個數也變為0.

Pod建立流程

image.png

  1. 使用者首先通過kubectl或其他的API Server客戶端將Pod資源定義(也就是我們上面的YAML)提交給API Server

  2. API Server在收到請求後,會將Pod資訊寫入etcd,並且返回響應給客戶端

  3. Kubernets中的元件都會採用Watch機制,Scheduler發現有新的Pod需要建立並且還沒有排程到一個節點,此時Scheduler會根據Pod中的一些資訊決定最終要排程到的節點,並且將該資訊提交給API Server

  4. API Server在收到該bind資訊後會將內容儲存到etcd

  5. 每個工作節點上的Kubelet都會監聽API Server的變動,發現是否還有屬於自己的Pod但還未進行繫結,一旦發現,Kubelet就會在本節點上呼叫Docker啟動容器並完成Pod一系列相關的設定,然後將結果返回給API Server

  6. API Server在收到Kubelet的返回資訊後,會將資訊寫入etcd

Pod的Status的含義?

  • Pending:Pod已被Kubernetes系統接收,但有一個或多個容器尚未建立執行

  • Running:Pod已經繫結到某個節點,並且所有容器已被建立,且至少有一個容器正在執行,或者處於啟動或重啟狀態

  • Succeed:Pod中所有容器都成功終止,並且不會再重啟

  • Failed:Pod中所有容器都已終止,並且至少有一個容器是因為失敗而終止。

  • Unknown:因為某些原因無法取得Pod的狀態,比如和Pod所在的節點通訊失敗。

```yaml

apiVersion: v1

kind: Pod

metadata:

name: status-change-watch

spec:

restartPolicy: Never

containers:

  • name: readiness

image: busybox

args:

  • /bin/sh

  • -c

  • touch /tmp/healthy; sleep 20; rm -rf /tmp/healthy

  • name: readiness-1

image: busybox

args:

  • /bin/sh

  • -c

  • cat 111

```

```shell

$ kubectl apply -f status-change-watch.yaml

$ kubectl describe pod status-change-watch

```

我們通過上述命令,不斷的觀察Po的狀態,會發現Pod的Status會從Pendingb變為Running,變為Failed。

image.png

image.png

image.png

Pod排程

節點選擇器

```shell

apiVersion: v1

kind: Pod

metadata:

name: node-seletor

spec:

節點選擇器

nodeSelector:

node_env: test

restartPolicy: Never

containers:

  • name: readiness

image: busybox

args:

  • /bin/sh

  • -c

  • touch /tmp/healthy; sleep 10; rm -rf /tmp/healthy

```

節點選擇器可以方便的將某個Pod和固定的Node進行繫結,由欄位spec.nodeSeletor定義,上述YAML中的含義是,Pod在被排程時會被排程到節點上有node_env標籤,且標籤值為test的Node上。

```shell

$ kubectl apply -f node-seletor.yaml

$ kubectl describe pod node-seletor

```

image.png

可以看到,由於我們現在沒有節點有node_env: test標籤,所以排程失敗了。

```shell

為節點打標籤

$ kubectl label nodes k8s-node-01 node_env=test

$ kubectl get nodes --show-labels

$ kubectl describe pod node-seletor

```

image.png

當我們為k8s-node-01節點打上node_env=test以後就發現我們node-seletor成功被排程到了該節點上。

節點親和性

節點親和性類似於節點選擇器,只不過節點親和性相比節點選擇器具有更強的邏輯控制能力。節點親和性欄位由spec.affinity.nodeAffinity定義,主要有兩種型別:

  • requiredDuringSchedulingIgnoredDuringExecution:排程器只有在節點滿足該規則的時候可以將Pod排程到該節點上

  • preferredDuringSchedulingIgnoredDuringExecution:排程器會首先找滿足該條件的節點,如果找不到合適的再忽略該條件進行排程

```yaml

apiVersion: v1

kind: Pod

metadata:

name: node-affinity

spec:

affinity:

節點親和性

nodeAffinity:

requiredDuringSchedulingIgnoredDuringExecution:

nodeSelectorTerms:

  • matchExpressions:

  • key: node_env

operator: In

values:

  • "test"

preferredDuringSchedulingIgnoredDuringExecution:

  • weight: 1

preference:

matchExpressions:

  • key: "cloud-provider"

operator: In

values:

  • "ali"

restartPolicy: Never

containers:

  • name: readiness

image: busybox

args:

  • /bin/sh

  • -c

  • touch /tmp/healthy; sleep 10; rm -rf /tmp/healthy

```

上述親和性規則如下:

  • 節點必須包含node_env標籤,且值是test

  • 節點最好具有cloud-provider標籤,且值是ali

目前operator欄位主要有以下值:

  • In和NotIn

  • Exists和DoesNotExist

  • Gt和Lt

preferredDuringSchedulingIgnoredDuringExecution的weight代表了下面的條件的權重,在排程時會將節點上的權重加起來(如果滿足條件的話),最終權重高的節點優先順序越高,越容易被排程到。

汙點(Taint)和汙點容忍(Toleration)

汙點作用於節點上,沒有對該汙點進行容忍的Pod無法被排程到該節點。

汙點容忍作用於Pod上,允許但不強制Pod被排程到與之匹配的汙點的節點上。

```shell

為節點打汙點

kubectl taint nodes k8s-node-01 key1=value1:NoSchedule

```

上述表示為k8s-node-01節點打上了一個汙點,汙點的key為key1,value為value1,效果是NoSchedule,目前效果主要有以下固定值:

  • NoSchedule:不允許排程

  • PreferNoSchedule:儘量不排程

  • NoExecute:如果該節點上不容忍該汙點的Pod已經在執行會被驅逐,同時如果不會將不容忍該汙點的Pod排程到該節點上

```yaml

apiVersion: v1

kind: Pod

metadata:

name: node-toleration

spec:

tolerations:

  • key: "node-role.kubernetes.io/master"

operator: "Equal"

value: ""

effect: "NoSchedule"

restartPolicy: Never

containers:

  • name: readiness

image: busybox

args:

  • /bin/sh

  • -c

  • touch /tmp/healthy; sleep 10; rm -rf /tmp/healthy

```

目前operator的值主要有以下兩種情況:

  • Equal:value必須和汙點的value相同

  • Exists:此時,Pod上的toleration不能指定value

```shell

$ kubectl apply -f node-toleration.yaml

$ kubectl get pods -o wide

```

image.png

通過上圖可以看出,我們通過汙點容忍,node-toleration這個Pod成功被排程到了master節點上。