Kubernetes筆記(4) - Pod

語言: CN / TW / HK

Pod是Kubernetes系統的基礎單元,是資源物件模型中可由使用者建立或部署的最小元件,也是在Kubernetes系統上執行容器化應用的資源物件。

容器與Pod的關係

Docker推薦採用單容器單程序的方式執行,但由於容器間的隔離機制,各容器程序間又無法實現IPC(Inter-Process Communication)通訊。這就導致功能相關的容器之間通訊困難,比如主容器與負責日誌收集的容器之間的通訊。而Pod資源抽象正是用來解決此類問題的元件,Pod物件是一組容器的集合,這些容器共享Network、UTS(UNIX Time-sharing System)及IPC名稱空間,因此具有相同的域名、主機名和網路介面,並可通過IPC直接通訊。 為一個Pod物件中的各容器提供網路名稱空間等共享機制的是底層基礎容器pause。 儘管Pod支援執行多個容器,但作為最佳實踐,除非多個程序之間具有密切的關係,否則都應該將其構建到多個Pod中,這樣多個Pod可被排程至多個不同的主機執行,提高了資源利用率,也便於規模的伸縮。

Sidecar pattern(邊車模式)

多個程序之間具有密切的關係時,一般按照邊車模型來組織多個容器,邊車即為Pod的主應用容器提供協同的輔助應用容器,典型的應用場景是將主應用容器中的日誌使用agent收集至日誌伺服器中時,可以將agent執行為輔助應用容器。

管理Pod物件的容器

Pod的配置清單舉例:

apiVersion: v1
kind: Pod
metadata:
 name: pod-example
spec:
 containers:
 - name: myapp
   image: ikubernetes/myapp:v2

其中spec欄位下,containers為及其子欄位name為必選項,image在手動場景Pod時必選,在但在被高級別管理資源如Deployment控制時可選,因為這個欄位可能會被覆蓋。

定義映象的獲取策略

Pod的核心功能是執行容器,而 通過image.imagePullPolicy可以自定義映象的獲取策略。

  • Always:映象標籤為“latest”或映象不存在時總是從指定的倉庫中獲取映象
  • IfNotPresent:僅當本地映象缺失時才從目標倉庫下載映象
  • Never:禁止從倉庫下載映象,即僅使用本地映象
spec:
 containers:
 - name: myapp
   image: ikubernetes/myapp:v2
     imagePullPolicy: Always  

對於標籤為“latest”的映象檔案,其預設的映象獲取策略即為“Always”,而對於其他標籤的映象,其預設策略則為“IfNotPresent”。

暴露埠

在Pod中暴露埠與為Docker容器暴露埠的意義不一樣: 在Docker的網路模型中,使用預設網路的容器化應用需通過NAT機制將其“暴露”(expose)到外部網路中才能被其他節點之上的容器客戶端所訪問; 而在K8S中,各Pod的IP地址已經處於同一網路平面,無論是否為容器暴露埠,都不會影響叢集中其他節點之上的Pod客戶端對其進行訪問,所以暴露的埠只是資訊性資料,而且顯式指定容器埠也方便呼叫。

spec:
 containers:
 - name: myapp
   image: ikubernetes/myapp:v2
   ports:
   - name: http
     containerPort: 80
     protocol: TCP

這裡的配置指定暴露容器上的TCP埠80,並將其命名為http。 Pod物件的IP地址僅在當前叢集內可達,它們無法直接接收來自叢集外部客戶端的請求流量,儘管它們的服務可達性不受工作節點邊界的約束,但依然受制於叢集邊界。如何讓叢集外部訪問到Pod物件,將在後面學習。

自定義執行的容器化應用

command欄位能夠指定不同於映象預設執行的應用程式,並且可以同時使用args欄位進行引數傳遞,它們將覆蓋映象中的預設定義。不過,如果僅為容器定義了args欄位,那麼它將作為引數傳遞給映象中預設指定執行的應用程式;如果僅為容器定義了command欄位,那麼它將覆蓋映象中定義的程式及引數,並以無引數方式執行應用程式。

spec:
 containers:
 - name: myapp
   image: ikubernetes/myapp:v2 
   imagePullPolicy: Never     
   command: ["/bin/sh"]
   args: ["-c", "while true; do sleep 30; done"]

環境變數

環境變數也是向容器化應用傳遞配置的一種方式,向Pod物件中的容器環境變數傳遞資料的方法有兩種:env和envFrom,這裡只介紹第一種方式,第二種方式將在介紹ConfigMap和Secret資源時進行說明。環境變數通常由name和value欄位構成。

spec:
 containers:
 - name: myapp
   image: ikubernetes/myapp:v2 
   env:
   - name: REDIS_HOST
     value: do.macOS
   - name: LOG_LEVEL
     value: info

標籤與標籤選擇器

標籤的管理

標籤選擇器可以對附帶標籤的資源物件進行挑選,並進行所需要的操作。一個物件可擁有不止一個標籤,而同一個標籤也可被新增至多個資源之上。 可以為資源附加多個不同緯度的標籤以實現靈活的資源分組管理功能,例如,版本標籤、環境標籤、分層架構標籤等,用於交叉標識同一個資源所屬的不同版本、環境及架構層級等。 定義標籤示例:

apiVersion: v1
kind: Pod
metadata:
 name: pod-example
 labels:
   env: qa
   tier: frontend

資源建立後,在kubectl get pods命令中新增--show-labels選項就可顯示lables資訊。 -L <key1>, <key2>選項可增加對應的列資訊。 直接管理活動物件的標籤:

kubectl label pods/pod-example release=beta

為pod-example添加了release=beta,如果要修改已經存在的減值對,需要新增--overwrite選項。

標籤選擇器

標籤選擇器用於表達標籤的查詢條件或選擇標準,Kubernetes API目前支援兩個選擇器:基於

  • equality-based,可用操作符有“=”“==”和“! =”三種,前兩種等價
  • set-based,支援in、notin和exists三種操作符,此外還有可以只指定KEY來篩選所有存在此鍵名標籤的資源,!KEY則篩選所有不存在此鍵名標籤的資源 使用標籤選擇器時遵循以下邏輯:
  • 同時指定的多個選擇器之間的邏輯關係為“與”操作
  • 使用空值的標籤選擇器意味著每個資源物件都將被選中
  • 空的標籤選擇器將無法選出任何資源。

Kubernetes的諸多資源物件必須以標籤選擇器的方式關聯到Pod資源物件,例如Service、Deployment和ReplicaSet型別的資源等,可以在spec欄位通過巢狀的“selector”欄位來指定選擇器,有兩種方式:

  • matchLabels:通過直接給定鍵值對來指定標籤選擇器
  • matchExpressions:基於表示式指定的標籤選擇器列表,每個選擇器都形如“{key:KEY_NAME, operator: OPERATOR,values: [VALUE1, VALUE2, …]}”,選擇器列表間為“邏輯與”關係;使用In或NotIn操作符時,其values不強制要求為非空的字串列表,而使用Exists或DostNotExist時,其values必須為空。 格式舉例:
selector:
 matchLabels:
   component: redis
 matchExpressions:
   - {key: tier, operator: In, values: [cache]}
   - {key: environment, operator: Exists, values:}

資源註解

標籤之外,Pod與其他各種資源還能使用資源註解(annotation),也是鍵值型別的資料,不過它不能用於標籤及挑選Kubernetes物件,僅可用於為資源提供“元資料”資訊。另外,註解中的元資料不受字元數量的限制,可以為結構化或非結構化形式,而且對字元型別也沒有限制。 Annotation中放置構建、發行或映象相關的資訊,指向日誌、監控、分析或審計倉庫的地址,或者由客戶端庫或工具程式生成的用於除錯目的的資訊:如名稱、版本、構建資訊等資訊。

檢視資源註解 使用kubectl get -o yamlkubectl describe命令均能顯示資源的註解資訊。

kubectl describe pods pod-example | grep "Annotations"

管理資源註解 在配置清單中定義annotations:

apiVersion: v1
kind: Pod
metadata:
 name: pod-example
 annotations:
   created-by: "cluster admin"

追加annotations:

kubectl annotate pods pod-example created-by2="admin"

Pod物件的生命週期

Pod的生命週期如圖:

Phase

Pod物件總是應該處於其生命程序中以下幾個Phase(階段)之一:

  • Pending:API Server建立了Pod資源物件並已存入etcd中,但它尚未被排程完成,或者仍處於從倉庫下載映象的過程中。‰
  • Running:Pod已經被排程至某節點,並且所有容器都已經被kubelet建立完成。‰
  • Succeeded:Pod中的所有容器都已經成功終止並且不會被重啟。‰
  • Failed:所有容器都已經終止,但至少有一個容器終止失敗,即容器返回了非0值的退出狀態或已經被系統終止。‰
  • Unknown:API Server無法正常獲取到Pod物件的狀態資訊,通常是由於其無法與所在工作節點的kubelet通訊所致。

Pod的建立過程

Pod的建立過程是指Pod自身及其主容器及其輔助容器建立的過程。

  1. 使用者通過kubectl或其他API客戶端提交PodSpec給API Server。
  2. API Server嘗試著將Pod物件的相關資訊存入etcd中,待寫入操作執行完成,API Server即會返回確認資訊至客戶端。
  3. API Server開始反映etcd中的狀態變化。
  4. 所有的Kubernetes元件均使用“watch”機制來跟蹤檢查API Server上的相關的變動。
  5. kube-scheduler(排程器)通過其“watcher”覺察到API Server建立了新的Pod物件但尚未繫結至任何工作節點。
  6. kube-scheduler為Pod物件挑選一個工作節點並將結果資訊更新至API Server。
  7. 排程結果資訊由API Server更新至etcd儲存系統,而且API Server也開始反映此Pod物件的排程結果。
  8. Pod被排程到的目標工作節點上的kubelet嘗試在當前節點上呼叫Docker啟動容器,並將容器的結果狀態回送至API Server。
  9. API Server將Pod狀態資訊存入etcd系統中。
  10. 在etcd確認寫入操作成功完成後,API Server將確認資訊傳送至相關的kubelet,事件將通過它被接受。

Pod生命週期中的重要行為

除了建立應用容器(主容器及其輔助容器)之外,使用者還可以為Pod物件定義其生命週期中的多種行為,如用於初始化的容器、存活性探測及就緒性探測等

用於初始化的容器

用於初始化的容器(init container)是應用程式的主容器啟動之前要執行的容器,常用於為主容器執行一些預置操作,典型的應用如:

  • 用於執行特定的工具程式,出於安全等方面的原因,這些程式不適於包含在主容器映象中。
  • 提供主容器映象中不具備的工具程式或自定義程式碼。
  • 為容器映象的構建和部署人員提供了分離、獨立工作的途徑,使得他們不必協同起來製作單個映象檔案。
  • 初始化容器和主容器處於不同的檔案系統檢視中,因此可以分別安全地使用敏感資料,例如Secrets資源。
  • 初始化容器要先於應用容器序列啟動並執行完成,因此可用於延後應用容器的啟動直至其依賴的條件得到滿足。

在資源清單中通過initContainers欄位定義:

spec:
 containers:
 - name: myapp
   image: ikubernetes/myapp:v2 
 initContainers:
 - name: init-something
   image: busybox
   command: ['sh', '-c', 'sleep 10'] 
生命週期鉤子函式

Kubernetes為容器提供了兩種生命週期鉤子:

  • postStart:在容器建立完成之後立即執行,但是Kubernetes無法確保它一定會在容器中的ENTRYPOINT之前執行。
  • preStop:在容器終止操作之前立即執行,它以同步的方式呼叫,因此在其完成之前會阻塞刪除容器的操作。

鉤子函式的實現方式有“Exec”和“HTTP”兩種,前一種在鉤子事件觸發時直接在當前容器中執行由使用者定義的命令,後一種則是在當前容器中向某URL發起HTTP請求。鉤子函式定義在容器的spec.lifecycle欄位。

容器的重啟策略

容器程式發生崩潰或容器申請超出限制的資源等原因都可能會導致Pod物件的終止,此時是否應該重建該Pod物件則取決於其重啟策略(restartPolicy)屬性的定義。

  • Always:只要Pod物件終止就將其重啟,此為預設設定。
  • OnFailure:僅在Pod物件出現錯誤時方才將其重啟。
  • Never:從不重啟。

容器在重啟失敗後,之後的重啟將有一段時間的延遲,且延遲時間越來越長,依次為10秒、20秒、40秒、80秒、160秒、300秒。

Pod的終止過程

  1. 使用者傳送刪除Pod物件的命令。
  2. API伺服器中的Pod物件會隨著時間的推移而更新,在寬限期內(預設為30秒),Pod被視為“dead”。
  3. 將Pod標記為“Terminating”狀態。
  4. (與第3步同時執行)kubelet在監控到Pod物件轉為“Terminating”狀態的同時啟動Pod關閉過程。
  5. (與第3步同時執行)端點控制器監控到Pod物件的關閉行為時將其從所有匹配到此端點的Service資源的端點列表中移除。
  6. 如果當前Pod物件定義了preStop鉤子處理器,則在其標記為“terminating”後即會以同步的方式啟動執行;如若寬限期結束後,preStop仍未執行結束,則第2步會被重新執行並額外獲取一個時長為2秒的小寬限期。
  7. Pod物件中的容器程序收到TERM訊號。
  8. 寬限期結束後,若存在任何一個仍在執行的程序,那麼Pod物件即會收到SIGKILL訊號。
  9. Kubelet請求API Server將此Pod資源的寬限期設定為0從而完成刪除操作,它變得對使用者不再可見。

如果在等待程序終止的過程中,kubelet或容器管理器發生了重啟,那麼終止操作會重新獲得一個滿額的刪除寬限期並重新執行刪除操作。

Pod存活性探測

kubelet可基於存活性探測判定何時需要重啟一個容器。可通過spec.containers.livenessProbe定義,支援三種探測方法:

  • exec
  • httpGet
  • tcpSocket

exec

exec型別的探針通過在目標容器中執行由使用者自定義的命令來判定容器的健康狀態,若命令狀態返回值為0則表示“成功”通過檢測,其它值均為“失敗”狀態。它只有一個可用屬性“command”,用於指定要執行的命令,示例:

apiVersion: v1
kind: Pod
metadata:
 name: liveness-exec-demo
 labels:
   test: liveness-exec-demo
spec:
 containers:
 - name: liveness-exec-demo
   image: busybox 
   args: ["/bin/sh", "-c", " touch /tmp/healthy;sleep 60; rm -rf /tmp/healthy;sleep 600"]
   livenessProbe:
     exec:
       command: ["test", "-e", "/tmp/healthy"]

這段配置清單基於busybox映象啟動一個容器,並執行args定義的命令,此命令在容器啟動時建立/tmp/healthy檔案,並於60秒之後將其刪除。存活性探針執行“test -e/tmp/healthy”命令檢查/tmp/healthy檔案的存在性,若檔案存在則返回狀態碼0,表示成功通過測試。 所以60秒後使用describe命令可以看到容器被重啟的event。

httpGet

httpGet方式是向目標容器發起一個HTTP GET請求,根據其響應碼進行結果判定,2xx或3xx時表示檢測通過。 可配置欄位有:

  • host,請求的主機地址,預設為Pod IP,也可以在httpHeaders中使用“Host:”來定義。
  • port,請求的埠,必選欄位。
  • httpHeaders,自定義的請求報文頭。
  • path,請求的HTTP資源路徑。
  • scheme:建立連線使用的協議,僅可為HTTP或HTTPS,預設為HTTP。

示例

apiVersion: v1
kind: Pod
metadata:
 name: liveness-http-demo
 labels:
   test: liveness-http-demo
spec:
 containers:
 - name: liveness-http-demo
   image: nginx:1.12-alpine
   ports:
   - name: http
     containerPort: 80
   lifecycle:
     postStart:
       exec:
         command: ["/bin/sh", "-c", " echo Healthy > /usr/share/nginx/html/healthz"]
   livenessProbe:
     httpGet:
       path: /healthz
       port: http
       scheme: HTTP

這個配置清單通過postStart hook建立了一個專用於httpGet測試的頁面檔案healthz。而為httpGet探測指定的路徑為“/healthz”,地址預設為Pod IP,埠使用了容器中定義的埠名稱http。 啟動容器後健康檢查是正常的,但執行如下命令刪除healthz頁面後,可在event中看到Container liveness-http-demo failed liveness probe, will be restarted

kubectl exec liveness-http-demo rm /usr/share/nginx/html/healthz

一般應為HTTP探測操作定義專用的URL路徑,此URL路徑對應的Web資源應該以輕量化的方式在內部對應用程式的各關鍵元件進行全面檢測以確保它們可正常向客戶端提供完整的服務。

tcpSocket

基於TCP的存活性探測用於向容器的特定埠發起TCP請求並嘗試建立連線,連線建立成功即為通過檢測。相比較來說,它比基於HTTP的探測要更高效、更節約資源,但精準度較低。 可配置欄位有:

  • host,請求連線的目標IP地址,預設為Pod IP。
  • port,請求連線的目標埠,必選欄位。

舉例:

spec:
 containers:
 - name: liveness-tcp-demo
   image: nginx:1.12-alpine 
   livenessProbe:
     tcpSocket:
       port: 80

存活性探測行為屬性

對於配置了liveness的pod,通過describe命令可以看到類似這樣的資訊,有delay、timeout等配置,由於之前沒有指定所以都為預設值:

Liveness:       tcp-socket :80 delay=0s timeout=1s period=10s #success=1 #failure=3
  • initialDelaySeconds,存活性探測延遲時長,即容器啟動多久之後再開始第一次探測操作,顯示為delay屬性,預設為0秒,整型
  • timeoutSeconds,存活性探測的超時時長,顯示為timeout屬性,預設為1s,整型,最小1s
  • periodSeconds,存活性探測的頻度,顯示為period屬性,整型,預設為10s,最小值為1s;過高的頻率會對Pod物件帶來較大的額外開銷,而過低的頻率又會使得對錯誤的反應不及時
  • successThreshold,處於失敗狀態時,探測操作至少連續多少次的成功才被認為是通過檢測,顯示為#success屬性,預設值為1,最小值也為1,整型
  • failureThreshold:處於成功狀態時,探測操作至少連續多少次的失敗才被視為是檢測不通過,顯示為#failure屬性,預設值為3,最小值為1,整型。

另外,liveness檢測僅對當前服務有效,比如但後端服務(如資料庫或快取服務)導致故障時,重啟當前服務並不能解決問題,但它卻會被一次次重啟,直到後端服務恢復正常為止。

Pod就緒性探測

Pod物件啟動後,容器應用通常需要一段時間才能完成其初始化過程,例如載入配置或資料,甚至有些程式還需要預熱的過程。因此應該避免在Pod物件啟動後立即讓其處理客戶端請求,而是等待容器初始化工作執行完成並轉為Ready狀態,尤其是存在其他提供相同服務的Pod物件的場景更是如此。 就緒性探測是用來判斷容器就緒與否的週期性操作,探測操作返回“success”狀態時,就認為容器已經就緒。 與liveness探測類似,它也支援三種方式,但定義時使用的屬性名為readinessProbe。 舉例:

apiVersion: v1
kind: Pod
metadata:
 name: readiness-tcp-demo
 labels:
   test: readiness-tcp-demo
spec:
 containers:
 - name: readiness-tcp-demo
   image: nginx:1.12-alpine 
   readinessProbe:
     tcpSocket:
       port: 80

未定義就緒性探測的Pod物件在Pod進入“Running”狀態後將立即就緒。生產實踐中,必須為需要時間進行初始化容器以及關鍵性Pod資源中的容器定義就緒性探測。

資源需求及資源限制

K8S中可由容器或Pod請求或消費的“計算資源”是指CPU和記憶體,其中CPU屬於可壓縮(compressible)型資源,可按需收縮,而記憶體則是不可壓縮型資源,對其執行收縮操作可能會導致無法預知的問題。 目前資源隔離屬於容器級別,所以CPU和記憶體資源的配置需要在Pod中的容器上進行,支援兩種屬性:

  • requests,定義了其請求的確保可用值,即容器執行可能用不到這些額度的資源,但用到時必須要確保有如此多的資源可用;
  • limits,限制資源可用的最大值

在K8S中,1個單位的CPU相當於虛擬機器上的1顆虛擬CPU(vCPU)或物理機上的一個超執行緒(Hyperthread,或稱為一個邏輯CPU),它支援分數計量方式,一個核心(1 core)相當於1000個微核心(millicores),因此500m相當於是0.5個核心。記憶體的計量方式與日常使用方式相同,預設單位是位元組,也可以使用E(Ei)、P(Pi)、T(Ti)、G(Gi)、M(Mi)和K(Ki)作為單位字尾。

資源需求

apiVersion: v1
kind: Pod
metadata:
 name: stress-demo
spec:
 containers:
 - name: stress-demo
   image: ikubernetes/stress-ng
   command: ["/usr/bin/stress-ng", "-m 1", "-c 1", "--metrics-brief"]
   resources:
     requests:
       memory: "128Mi"
       cpu: "200m"

以上的配置清單定義了容器的資源需求為128M記憶體、200m(0.2)個CPU核心。它執行stress-ng(一個多功能系統壓力測工具)映象啟動一個程序(-m 1)進行記憶體效能壓力測試,再啟動一個專用的CPU壓力測試程序(-c 1)。 然後使用kubectl exec stress-demo -- top命令來檢視資源的使用情況,在我的電腦(6核,記憶體16G)上顯示的記憶體佔用為262m,CPU佔用2*17%(約等於2/6,因為兩個測試執行緒分佈於兩個CPU核心以滿載的方式執行),都遠高於requests中定義的值,這是因為當前資源充裕,一旦 資源緊張時,節點僅保證容器有五分之一個CPU核心可用,對於有著6個核心的節點來說,它的佔用率約為3.33%,多佔用的資源會被壓縮。記憶體為非可壓縮型資源,所以此Pod在記憶體資源緊張時可能會因OOM被殺死(killed)。 如果沒有定義requests,那麼在CPU資源緊張時,可能會被其它Pod壓縮至極低的水平,甚至會達到Pod不能夠被排程執行的境地,而不可壓縮型的記憶體資源,則可能因OOM導致程序被殺死。因此在Kubernetes系統上執行關鍵型業務相關的Pod時必須使用requests屬性為容器定義資源的確保可用量。

叢集中的每個節點擁有的CPU和記憶體資源是固定的,Kubernetes的排程器在排程Pod時,會根據容器的requests屬性來判定哪些節點可接收運行當前的Pod資源,而對於一個節點的資源來說,每執行一個Pod物件,其requests中定義的請求量都要被預留,直到給所有Pod物件分配完為止。

資源限制

通過定義資源需求可以保證容器的最少資源量,如果要限制容器使用資源的上限,則需要定義資源限制。 如果定義了資源限制,則容器程序無法獲得超出其CPU配額的可用時間,而程序申請分配超出其limits定義的記憶體資源時,它將被OOM killer殺死。

Pod的服務質量類別

Kubernetes允許節點資源對limits的過載使用,這意味著節點無法同時滿足其上的所有Pod物件以資源滿載的方式執行。於是就需要確定Pod物件的優先順序,在記憶體資源緊缺時,先終止低優先順序的Pod物件。 Pod物件的優先順序是根據requests和limits屬性確定的,分為三個級別或QoS(Quality of Service):

  • Guaranteed,Pod中所有容器對所有資源型別都定義了Limits和Requests,而且Limits值等於Requests值且不為0,Requests值未定義時預設等於Limits,優先順序最高。
  • BestEffort,沒有為任何一個容器設定requests或limits屬性的Pod資源屬於這一類,優先順序最低。
  • Burstable,不為Guaranteed和BestEffort時,優先順序中等。

以上只適用於記憶體資源緊缺時,CPU資源無法得到滿足時,Pod僅僅是暫時獲取不到相應的資源而已。

學習資料

《Kubernetes實戰進階》 馬永亮著

分享到: