圖解Kubernetes的Pod核心資源-來白嫖啊

2022-09-05 12:02:10

推薦手機閱讀原文:https://mp.weixin.qq.com/s/nR6P6eidE1r5A2viLCFHWA
推薦手機閱讀閱讀:https://mp.weixin.qq.com/s/nR6P6eidE1r5A2viLCFHWA
推薦手機閱讀閱讀:https://mp.weixin.qq.com/s/nR6P6eidE1r5A2viLCFHWA

一、Pod定義

如下圖,在K8S中資源排程的基本單位是Pod

Pod其實是一個抽象的概念,Pod裡是我們的業務容器(docker/containerd)。像大家聽過的Deployment、StatefulSet、CronJob等資源排程物件所排程的資源都是Pod。

為了更好的理解Pod的概念,大家可以將Pod理解成VM 虛擬機器器,將Pod中的容器理解成VM中的程序。既然這樣理解,就意味著Pod中的容器程序可以直接通過localhost+埠號實現網路互通,也意味著Pod中的容器可以實現類似直接讀取彼此產出到磁碟上的檔案的效果。

如上圖:容器1存取:127.0.0.1:8082可以存取到容器2。

在實際用應用中,比如我們有兩個服務:服務A和服務B,並且他倆之間只能通過本地迴環網路卡通訊,那我們在就應該將它們分配進同一個pod中。

那什麼是資源排程? 簡單來說就是將:為Pod挑選一個合適的物理節點,然後將Pod中的容器啟動好對外提供服務。


二、Pod入門yaml描述檔案

簡單的,只要將我們想啟動的Docker容器填入Pod資源物件的containers欄位中,再通過kubectl命令建立Pod,K8S會為Pod選擇一個合適的Node,並在該Node上啟動使用者指定的容器。

如下是一個Pod的Yaml描述檔案,Pod中定義了兩個容器分別是:nginx、shell

apiVersion: v1  # 必選,API的版本號
kind: Pod       # 必選,型別Pod
metadata:       # 必選,後設資料
  name: daemon-pod   # 必選,符合RFC 1035規範的Pod名稱
spec:
  containers:
  - name: nginx
    image: nginx:latest    # 必選,容器所用的映象的地址
  - name: shell
    image: busybox
    stdin: true
    tty: true

使用kubectl命名建立Pod,可以看到Ready數為2、Node欄位說明該Pod執行在叫node02的宿主機上

驗證,找到node02

登入node02,檢視node02上是否有相應的docker容器

可以看到K8S不光啟動了nginx、busybox容器,還多啟動一個叫pause的容器,大家也叫它infra容器。

Infra容器的作用:Pod中的所有容器會共用一個NetworkNamespace,因為在建立pod中的業務容器前,會先建立pause容器佔用NetworkNamespace,後續建立的業務容器都加入到pause的網路中,相當於在Docker run命令中新增引數:--net=container:pause,這也是為什麼Pod中的所有容器的ip其實都是pod的ip。

如下圖,進入shell容器中,看到它的ip其實就是上圖中的pod ip。

在nginx容器中,也能看到容器的ip就是pod的ip

三、共用NetworkNamespace

如下圖是K8S建立Pod時,Pod的網路協定棧的初始化過程

簡單解讀,理解pause容器是K8S網路模型中的精髓~

  1. kubelet通過CRI協定向底層的容器執行時(docker/containerd)下發命令,讓它建立一個叫pause 的初始化容器,這個pause容器裡執行著一個極簡的C程式,具體的邏輯就是將自己阻塞中,目的是讓pause容器快速佔用並一直持有一個networkname
  2. 建立pause容器時,會攜帶引數--net=node意為不初始化網路協定棧,說白了就是除了自帶的lo迴環網路卡外,不新增其他的網路卡。
  3. kubelet通過CNI協定為pause容器初始化網路協定,也就是給它新增網路並分配IP
  4. Pod中定義的業務容器都加入pause容器的network namespace,它們都使用統一分配給pause的IP

疑問:為什麼pause容器的網路協定棧不由容器執行時建立它時立即分配好呢?

答:這是個好問題,這麼做也是呼應了K8S網路的核心目標思想:

  1. IP分配,換句話說K8S要保證在整個叢集中每個POD要獨立不重複的IP地址

  2. IP路由,換句話說K8S要保證在整個叢集中各個POD的IP是要互通的

這也是它為什麼設計這個流程的原因

四、共用PID

預設情況下Pod中的各容器是不會共用同一個統一個PID Namespace的,需要手動新增引數shareProcessNamespace: true

apiVersion: v1  
kind: Pod       
metadata:       
  name: daemon-pod   
spec:
  # 共用pid namespace
  shareProcessNamespace: true
  containers:
  - name: nginx
    image: nginx:latest    
  - name: shell
    image: busybox
    stdin: true
    tty: true

驗證:如下圖,在shell容器中可以看到nginx程序(通過這更好的將pod理解成虛擬機器器),同理登陸pod重點任意容器也能看到pid=1的程序是pause程序。

此時pause容器為Pod提供1號程序,在Uninx中,程序為1的程序被稱作init程序。

這個init程序很特殊,因為它會維護一張程序表並不斷的檢測其他程序的狀態,當出現:子程序因父程序的異常退出而變成「孤兒程序」或者是叫「殭屍程序」時,init程序(pause)會收養這個遊離的程序,然後在它退出時,釋放它佔用的資源,否則會可能會出現大量的殭屍程序佔用作業系統的程序表項。

在k8s1.8之前,預設是啟用共用pid namespace

在k8s1.8之後,則需要像本小節一樣,通過引數shareProcessNamespace顯示的開啟

問:既然共用了pause的pid有這麼多好處,為啥後續版本的k8s不再預設開啟了呢?

答:一方面:k8s推薦的做法是,單個pod裡面放盡量少的容器,如只放你的業務容器,這樣殭屍程序帶來的影響幾乎可以忽略不計,共用與不共用,意義不大。

另一方面:像一些特殊的如systemd映象,啟動需要獲取pid1,否則無法啟動

五、容器生命週期

可以通過lifecycle欄位在容器建立完成後以及關閉前執行指定的動作,如建立使用者/建立目錄,啟動指令碼等

apiVersion: v1 
kind: Pod    
metadata:   
  name: nginx   
spec: 
  containers:  
  - name: nginx 
    image: nginx:latest 
    lifecycle:
      postStart: # 容器建立完成後執行的指令, 可以是exec httpGet TCPSocket
        exec:
          command:
          - sh
          - -c
          - 'mkdir /data/ '
      preStop: # 關閉前的操作
        httpGet:      
              path: /
              port: 80
      #  exec:
      #    command:
      #    - sh
      #    - -c
      #    - sleep 9
  restartPolicy: Always

spec.containers.lifecycle.postStart引數可以指定容器在建立完成後執行一段指令

回憶一下,容器還有個command引數即:spec.containers.command也可以指定一段指令。注意點如下:

  • 這倆command執行的先後並不能100%保證。
  • spec.containers.lifecycle.postStart的執行依賴於容器建立後的環境

spec.initContainers.command的不會依賴業務容器的環境,執行時間也會先於如上兩個command。

六、初始化容器

6.1、簡介

業務容器的啟動依賴很多環境設定,如:

  • wget等可以從網路上下載檔案的命令
  • 或者是有些命令需要以root的許可權執行初始化,如修改檔案的許可權、修改核心引數

如果有攻擊性的程式獲得了使用這些命令的許可權,就會有很大的安全隱患,為了安全,我們是不希望業務容器中包含這些危險的命令。

這時可以使用initcontainer完成這種工作,因為initcontainer執行結束後會退出,沒有後續的安全隱患。

6.2、與普通容器的區別

  • 初始化容器會依次執行,上一個執行結束,下一個才會執行。
  • 初始化容器不成功結束,不會啟動業務容器,K8S會不斷的重啟該Pod。但是如果Pod的restartPolicy設定為Never,K8S就不再重啟該Pod了。
  • init容器不支援:lifecycle、livenessProbe、readinessProbe、startupProbe探針

6.3、實驗

準備yaml,initContainer和業務容器共用掛載volume的方式,讓業務容器共用initContainer的初始化檔案

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: test-init-container
  name: test-init-container
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-init-container
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: test-init-container
    spec:
      volumes:
      - name: data
        emptyDir: {}
      initContainers:
      - name: init01
        image: busybox
        volumeMounts:
        - name: data
          mountPath: /tmp
        command:
        - sh
        - -c
        - touch /tmp/test-init-container.txt
      containers:
      - image: nginx
        name: nginx
        volumeMounts:
        - name: data
          mountPath: /tmp

檢視Pod執行的Event可以看到先執行了初始化容器的相關操作

[root@master01 initContainer]# kubectl describe pod test-init-container-79d689d7d8-fgz2s
... 
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  6m47s  default-scheduler  Successfully assigned default/test-init-container-79d689d7d8-fgz2s to master01
  Normal  Pulling    6m47s  kubelet            Pulling image "busybox"
  Normal  Pulled     6m43s  kubelet            Successfully pulled image "busybox" in 3.781619189s
  Normal  Created    6m43s  kubelet            Created container init01
  Normal  Started    6m43s  kubelet            Started container init01
  Normal  Pulling    6m42s  kubelet            Pulling image "nginx"
  Normal  Pulled     6m11s  kubelet            Successfully pulled image "nginx" in 31.093619016s
  Normal  Created    6m11s  kubelet            Created container nginx
  Normal  Started    6m11s  kubelet            Started container nginx

驗收

[root@master01 initContainer]# kubectl exec -ti test-init-container-79d689d7d8-fgz2s -- sh
Defaulted container "nginx" out of: nginx, init01 (init)
# ls /tmp
test-init-container.txt

七、Pod探針

名稱 作用 引入版本
startupProbe 1. 用於判斷容器內的應用程序是否成功啟動。
2. 若設定了startupProbe。直到它檢測通過前,會禁用其他探針
3. startupProbe檢測未通過,會使用restartPolicy重啟策略重啟
4. startupProbe探測成功後將不再探測。
1.16
readinessProbe 1. 使用者探測容器內的程式是否健康,是否可以接收流量
2. 探測成功表示該容器已經完全啟動,可接收流量。
3. 若未設定,預設返回success
livenessProbe 1. 用於判斷容器是否執行
2. 若探測失敗kubelet根據重啟策略重啟容器
3. 若未設定,預設返回success

檢測方式一:ExecAction

原理如下:執行一個指令碼,返回0表示成功,返回非0表示異常

[root@master01 yamls]# touch 1
[root@master01 yamls]# cat 1
[root@master01 yamls]# echo $?
0
[root@master01 yamls]# cat 123123.txt
cat: 123123.txt: 沒有那個檔案或目錄
[root@master01 yamls]# echo $?
1
#!/bin/bash
#K8S 的存活探針
function liveness()
{
    result=`nmap 127.0.0.1 -p $Target_PORT | grep $Target_PORT/tcp | awk '{print $2}'`
    if [ "$result" != "open" ];then
        echo 'port not open'
        return 1
    fi
}                    
liveness

檢測方式二:TcpSocketAction

通過Tcp連線檢查容器內的埠是否是連通的,如果連通,認為容器健康。原理如下:

# 連通的情況
[root@master01 yamls]# telnet 10.10.10.101 2380
Trying 10.10.10.101...
Connected to 10.10.10.101.
Escape character is '^]'.
^CConnection closed by foreign host.

# 非連通的情況
[root@master01 yamls]# telnet 10.10.10.101 2381
Trying 10.10.10.101...
telnet: connect to address 10.10.10.101: Connection refused

檢測方式三:HttpGetAction

通過應用程式暴露的Http介面,來檢查應用程式是否健康,若返回的狀態碼在[200,400)之間,認為程式健康。

7.1、livenessProbe

目的:判斷容器是否啟動了,檢測失敗後會重啟容器

參考

livenessProbe:
      failureThreshold: 5
      httpGet:
        path: /health
        port: 8080
        scheme: HTTP
      initialDelaySeconds: 60
      periodSeconds: 10
      successThreshold: 1
      timeoutSeconds: 5

7.2、readinessProbe

目的:探測容器中的應用是否是正常的

檢測通過:表示應用可以接收流量,READY狀態變成1/1

[root@master01 yamls]# kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          54s

檢測失敗,READY狀態變成0/1,且RESTARTS且0,表示不會重啟容器

[root@master01 yamls]# kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   0/1     Running   0          54s

參考

readinessProbe:
      failureThreshold: 3
      httpGet:
        path: /ready
        port: 8181
        scheme: HTTP
      periodSeconds: 10
      successThreshold: 1
      timeoutSeconds: 1

7.3、startupProbe

在有了livenessProbe和readinessProbe之後,為啥還整一個startupProbe出來呢?

可用來應對極端情況:應用啟動時各種載入設定,導致啟動的特別慢。 最終導致livenessProbe檢查失敗,當livenessProbe檢查失敗時,k8s會重啟容器。 重啟之後應用啟動還是慢,livenessProbe還是失敗,就進入了死迴圈

startupProbe其實就是將等待探測應用正常啟動的步驟從livenessProbe中提取出來,放在livenessProbe步驟前。若設定了startupProbe,livenessProbe和readinessProbe會先被禁用,等startupProbe通過後,livenessProbe和readinessProbe才會生效。

實驗:

apiVersion: v1  # 必選,API的版本號
kind: Pod       # 必選,型別Pod
metadata:       # 必選,後設資料
  name: nginx   # 必選,符合RFC 1035規範的Pod名稱
spec:   # 必選,用於定義容器的詳細資訊
  containers:   # 必選,容器列表
  - name: nginx # 必選,符合RFC 1035規範的容器名稱
    image: nginx:latest    # 必選,容器所用的映象的地址
    ports:  # 可選,容器需要暴露的埠號列表
    - name: http          # 埠名稱,如果需要暴露多個埠,需要保證每個port的name不能重複
      containerPort: 80 # 埠號
      protocol: TCP     # 埠協定,預設TCP
    startupProbe: 
      httpGet:  # httpGet檢測方式,生產環境建議使用httpGet實現介面級健康檢查,健康檢查由應用程式提供。
        path: /api/successStart # 檢查路徑
        port: 80
  restartPolicy: Always 

因為沒有/api/successStart介面,所以startupProbe檢測不通過

如下:pod的status為running,但是Ready為0

[root@master01 yamls]# kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   0/1     Running   0          19s

檢視詳情:

startupProbe會按照繼續按策略探測,當失敗的次數達到預期後,會重啟,如下重啟了4次了

[root@master01 yamls]# kubectl get pod
NAME    READY   STATUS             RESTARTS      AGE
nginx   0/1     CrashLoopBackOff   4 (27s ago)   2m57s

可以將HttpGet檢測方式換成TcpSocket去檢測80埠,startupProbe校驗即可通過

apiVersion: v1 
kind: Pod       
metadata:       
  name: nginx   
spec:  
  containers:  
  - name: nginx
    image: nginx:latest   
    startupProbe: 
      tcpSocket:
        port: 80
  restartPolicy: Always 
[root@master01 yamls]# kubectl get pod
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          14s

八、Pod退出流程

當我們關閉或者刪除pod時,Pod的狀態會變成:Terninating

[root@master01 yamls]# kubectl get pod
NAME    READY   STATUS     RESTARTS   AGE
nginx   0/1     Terninating   0          2m16s

另外你會發現,執行delete命令時,會等待一段時間

[root@master01 yamls]# kubectl delete pod nginx
pod "nginx" deleted

這個等待的時間是k8s給pod留出來的一段寬限期,可以通過kubectl edit pod xxx檢視pod的設定,預設情況下有一個叫terminationGracePeriodSeconds: 30的引數,值為30秒,表示在pod被delete之後,有30秒的寬限期。

在這個寬限期中會做收尾的工作如:

  • 將pod所屬的service的endpoint對應記錄刪除
  • 執行lifecycle 的 preStop 定義的命令

上文說過lifecycle,重新貼出來,如下:

    lifecycle:
      postStart: # 容器建立完成後執行的指令, 可以是exec httpGet TCPSocket
        exec:
          command:
          - sh
          - -c
          - 'mkdir /data/ '
      preStop: # 容器關閉前的操作
        httpGet:      
              path: /
              port: 80
        exec:
          command:
          - sh
          - -c
          - sleep 9

九、HPA

9.1、簡介

全稱:Horizontal Pod Autoscaler

可以根據CPU、記憶體使用率或者是自定義的指標完成對Pod的自動擴縮容

檢視K8S叢集的HPA相關API

  • v1版本是穩定版,只支援CPU指標
    • v2beta1支援CPU、記憶體、自定義指標
  • v2beta2支援CPU、記憶體、自定義指標、額外指標ExternelMetrics(公有云廠商提供)

9.2、使用

使用限制:

  • 不能對如DaemonSet型別的資源進行擴所容。
  • 必須安裝了metrics-server
  • 必須設定requests引數

準備測試環境

# 建立模版yaml
kubectl create deployment nginx-dp --image=nginx --dry-run=client -oyaml > nginx-dp.yaml

# 更新resources
      containers:
      - image: nginx
        name: nginx
        resources:
          requests:
            cpu: 10m

# 建立deployment
kubectl apply -f nginx-dp.yaml

# 檢視pod的CPU指標
[root@master01 yamls]# kubectl top pod
NAME                        CPU(cores)   MEMORY(bytes)
busybox                     0m           0Mi
nginx-dp-84c4fd8fc6-s7mnx   0m           6Mi

# 為dp暴露一個service
kubectl expose deployment nginx-dp --port=80

可以通過如下命令使用HPA

# 當CPU使用率超過10%就擴容,擴容最大數Pod數為10,最小數為1
kubectl autoscale deployment nginx-dp --cpu-percent=10 --min=1 --max=3

壓測,可以觀察到pod會被自動擴容

while true; do wget -q -O- http://192.168.217.66 > /dev/null;done

注意點:如果CPU或者是Memory的飆升的源頭是資料庫壓力,那麼我們對pod進行擴容不僅沒有好轉,返回適得其反。

十、靜態Pod

10.1、簡介

靜態Pod是由kubelet直接管理的,且只能在該kubelet所在的Node上執行。

靜態Pod不受ApiServer管理,無法與ReplicationController、Deployment、DaemonSet進行關聯。

kubelet也無法對靜態Pod進行健康檢查。

有兩種方式建立靜態Pod,分別是使用:靜態檔案/Http,若使用靜態組態檔建立pod,需要在kubelet的啟動引數statucPodPath中指定靜態Pod的yaml描述檔案的位置。

10.2、實驗

只要將pod的yaml檔案放入指定的目錄中,過一會便能通過kubectl檢視到pod。

嘗試通過kubectl刪除Pod,會一直處於pending狀態,這是因為kubectl會通過apiserver下發刪除的命令,而apiserver無法管理靜態pod。故,若想刪除靜態Pod,需要將對應的pod的yaml檔案移出statucPodPath

十一、更多Pod屬性

十二、對比DockerCompose、DockerSwarm

像Docker公司推出的叢集排程工具:Docker Compose或是Docker Swarm它們排程的基本單位都是Docker容器。

點選檢視白日夢的筆記:玩轉Docker容器排程-DockerCompose、DockerSwarm

而在K8S中叢集排程的基本單位是上文中長篇介紹的Pod,他們兩者維度是不同的。Pod顯然是站在更高的維度上。

因為在容器編排領域中,難點不是為容器選擇一個合適的節點然後將容器啟動好。難點是解決應用間複雜的相互依賴關係。

比如下:

  • 不同應用之間通過本地檔案相互通訊
  • 不同應用之間通過Http協定/RPC協定相互通訊
  • 不同應用之間通過localhost+埠號互通
  • 在所有宿主機上均啟動一個Pod副本

前文說了,大家可以把Pod理解成傳統意義上的虛擬機器器,Pod中的容器相當於虛擬機器器中的不同應用,Pod中有哪些容器由開發人員說了算。

這樣其實就是變相的將容器編排最為複雜環節的皮球重新踢給了開發人員,由他們自己描述好,也就實現了天然的解決了應用的複雜依賴關係編排這一難點。K8S排程時也要以Pod為基本單位去選擇一個相對合適宿主機,批次的啟動好Pod中的容器就行。

十三、參考

Kubernetes官網

《Kubernetes權威指南》

《Kubernetes網路原理與實踐》