k8s 中 Pod 的深入瞭解

語言: CN / TW / HK

k8s中Pod的理解

基本概念

Pod 是 Kubernetes 叢集中能夠被建立和管理的最小部署單元,它是虛擬存在的。Pod 是一組容器的集合,並且部署在同一個 Pod 裡面的容器是親密性很強的一組容器,Pod 裡面的容器,共享網路和儲存空間,Pod 是短暫的。

k8s 中的 Pod 有下面兩種使用方式

1、一個 Pod 中執行一個容器,這是最常見的用法。一個 Pod 封裝一個容器,k8s 直接對 Pod 管理即可;

2、一個 Pod 中同時執行多個容器,通常是緊耦合的我們才會放到一起。同一個 Pod 中的多個容器可以使用 localhost 通訊,他們共享網路和儲存卷。不過這種用法不常見,只有在特定的場景中才會使用。

k8s 為什麼使用 Pod 作為最小的管理單元

k8s 中為什麼不直接操作容器而是使用 Pod 作為最小的部署單元呢?

為了管理容器,k8s 需要更多的資訊,比如重啟策略,它定義了容器終止後要採取的策略;或者是一個可用性探針,從應用程式的角度去探測是否一個程序還存活著。基於這些原因,k8s 架構師決定使用一個新的實體,也就是 Pod,而不是過載容器的資訊新增更多屬性,用來在邏輯上包裝一個或者多個容器的管理所需要的資訊。

如何使用 Pod

1、自主式 Pod

我們可以簡單的快速的建立一個 Pod 類似下面:

$ cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:1.14.2
    ports:
    - containerPort: 80

建立

$ kubectl apply -f pod.yaml -n study-k8s

$ kubectl get pod -n study-k8s
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          2m

自主建立的 Pod ,因為沒有加入控制器來管理,這樣建立的 Pod,被刪除,或者因為意外退出了,不會重啟自愈,直接就會被刪除了。所以,業務中,我們在建立 Pod 的時候都會加入控制器。

2、控制器管理的 Pod

因為我們的業務長場景的需要,我們需要 Pod 有滾動升級,副本管理,叢集級別的自愈能力,這時候我們就不能單獨的建立 Pod, 我們需要通過相應的控制器來建立 Pod,來實現 Pod 滾動升級,自愈等的能力。

對於 Pod 使用,我們最常使用的就是通過 Deployment 來管理。

Deployment 提供了一種對 Pod 和 ReplicaSet 的管理方式。Deployment 可以用來建立一個新的服務,更新一個新的服務,也可以用來滾動升級一個服務。藉助於 ReplicaSet 也可以實現 Pod 的副本管理功能。

滾動升級一個服務,實際是建立一個新的 RS,然後逐漸將新 RS 中副本數增加到理想狀態,將舊 RS 中的副本數減小到 0 的複合操作;這樣一個複合操作用一個 RS 是不太好描述的,所以用一個更通用的 Deployment 來描述。

$ vi deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

執行

$ kubectl apply -f deployment.yaml -n study-k8s
$ kubectl get pods -n study-k8s
NAME                                READY   STATUS    RESTARTS   AGE
nginx                               1/1     Running   0          67m
nginx-deployment-66b6c48dd5-24sbd   1/1     Running   0          59s
nginx-deployment-66b6c48dd5-wxkln   1/1     Running   0          59s
nginx-deployment-66b6c48dd5-xgzgh   1/1     Running   0          59s

因為上面定義了 replicas: 3 也就是副本數為3,當我們刪除一個 Pod 的時候,馬上就會有一個新的 Pod 被建立。

同樣經常使用的到的控制器還有 DaemonSet 和 StatefulSet

DaemonSet:DaemonSet 確保全部(或者某些)節點上執行一個 Pod 的副本。 當有節點加入叢集時, 也會為他們新增一個 Pod 。 當有節點從叢集移除時,這些 Pod 也會被回收。刪除 DaemonSet 將會刪除它建立的所有 Pod。

StatefulSet:用來管理有狀態應用的工作負載,和 Deployment 類似, StatefulSet 管理基於相同容器規約的一組 Pod。但和 Deployment 不同的是, StatefulSet 為它們的每個 Pod 維護了一個有粘性的 ID。這些 Pod 是基於相同的規約來建立的, 但是不能相互替換:無論怎麼排程,每個 Pod 都有一個永久不變的 ID。

靜態 Pod

靜態 Pod 是由 kubelet 進行管理的僅存在與特定 node 上的 Pod,他們不能通過 api server 進行管理,無法與 rc,deployment,ds 進行關聯,並且 kubelet 無法對他們進行健康檢查。

靜態 Pod 始終繫結在某一個kubelet,並且始終執行在同一個節點上。 kubelet會自動為每一個靜態 Pod 在 Kubernetes 的 apiserver 上建立一個映象 Pod(Mirror Pod),因此我們可以在 apiserver 中查詢到該 Pod,但是不能通過 apiserver 進行控制(例如不能刪除)。

為什麼需要靜態 Pod ?

主要是用來對叢集中的元件進行容器化操作,例如 etcd kube-apiserver kube-controller-manager kube-scheduler 這些都是靜態 Pod 資源。

因為這些 Pod 不受 apiserver 的控制,就不會不小心被刪掉的情況,同時 kube-apiserver 也不能自己去控制自己。靜態 Pod 的存在將叢集中的容器化操作提供了可能。

靜態 Pod 的建立有兩種方式,配置檔案和 HTTP 兩種方式,具體參見。 靜態 Pod 的建立

Pod的生命週期

Pod 在執行的過程中會被定義為各種狀態,瞭解一些狀態能幫助我們瞭解 Pod 的排程策略。當 Pod 被建立之後,就會進入健康檢查狀態,當 Kubernetes 確定當前 Pod 已經能夠接受外部的請求時,才會將流量打到新的 Pod 上並繼續對外提供服務,在這期間如果發生了錯誤就可能會觸發重啟機制。

不過 Pod 本身不具有自愈能力,如果 Pod 因為 Node 故障,或者是排程器本身故障,這個 Pod 就會被刪除。所以 Pod 中一般使用控制器來管理 Pod ,來實現 Pod 的自愈能力和滾動更新的能力。

Pod的重啟策略包括

  • Always 只要失敗,就會重啟;

  • OnFile 當容器終止執行,且退出碼不是0,就會重啟;

  • Never 從來不會重啟。

重啟的時間,是以2n來算。比如(10s、20s、40s、…),其最長延遲為 5 分鐘。 一旦某容器執行了 10 分鐘並且沒有出現問題,kubelet 對該容器的重啟回退計時器執行 重置操作。

管理Pod的重啟策略是靠控制器來完成的。

Pod 的幾種狀態

使用的過程中,會經常遇到下面幾種 Pod 的狀態。

Pending :Pod 建立已經提交給 k8s,但有一個或者多個容器尚未建立亦未執行,此階段包括等待 Pod 被排程的時間和通過網路下載映象的時間。這個狀態可能就是在下載映象;

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

Secceeded :Pod 中的所有容器都已經成功終止,並且不會再重啟;

Failed :Pod 中所有的容器均已經終止,並且至少有一個容器已經在故障中終止;

Unkown :因為某些原因無法取得 Pod 的狀態。這種情況通常是因為與 Pod 所在主機通訊失敗。

當 pod 一直處於 Pending 狀態,可通過 kubectl describe pods <node_name> -n namespace 來獲取出錯的資訊

Pod 如何直接暴露服務

Pod 一般不直接對外暴露服務,一個 Pod 只是一個執行服務的例項,隨時可能在節點上停止,然後再新的節點上用一個新的 IP 啟動一個新的 Pod,因此不能使用確定的 IP 和埠號提供服務。這對於業務來說,就不能根據 Pod 的 IP 作為業務排程。kubernetes 就引入了 Service 的概 念,它為 Pod 提供一個入口,主要通過 Labels 標籤來選擇後端Pod,這時候不論後端 Pod 的 IP 地址如何變更,只要 Pod 的 Labels 標籤沒變,那麼 業務通過 service 排程就不會存在問題。

不過使用 hostNetwork 和 hostPort,可以直接暴露 node 的 ip 地址。

hostNetwork

這是一種直接定義 Pod 網路的方式,使用 hostNetwork 配置網路,Pod 中的所有容器就直接暴露在宿主機的網路環境中,這時候,Pod 的 PodIP 就是其所在 Node 的 IP。從原理上來說,當設定 Pod 的網路為 Host 時,是設定了 Pod 中 pod-infrastructure (或pause)容器的網路為 Host,Pod 內部其他容器的網路指向該容器。

cat <<EOF >./pod-host.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      hostNetwork: true
EOF

執行

$ kubectl apply -f pod-host.yaml -n study-k8s
$ kubectl get pods -n study-k8s -o wide
NAME                                READY   STATUS    RESTARTS   AGE     IP             NODE              NOMINATED NODE   READINESS GATES
nginx-deployment-6d47cff9fd-5bzjq   1/1     Running   0          6m25s   192.168.56.111   kube-server8.zs   <none>           <none>

$ curl http://192.168.56.111/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

一般情況下除非知道需要某個特定應用佔用特定宿主機上的特定埠時才使用 hostNetwork: true 的方式。

hostPort

這是一種直接定義Pod網路的方式。

hostPort 是直接將容器的埠與所排程的節點上的埠路由,這樣使用者就可以通過宿主機的 IP 加上埠來訪問 Pod。

cat <<EOF >./pod-hostPort.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
          hostPort: 8000
EOF

執行

$ kubectl apply -f pod-hostPort.yaml -n study-k8s
$ kubectl get pods -n study-k8s -o wide
NAME                                READY   STATUS    RESTARTS   AGE     IP             NODE              NOMINATED NODE   READINESS GATES
nginx-deployment-6d47cff9fd-5bzjq   1/1     Running   0          3m25s   192.168.56.111   kube-server8.zs   <none>           <none>

$ curl http://192.168.56.111:8000/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

這種網路方式可以用來做 nginx [Ingress controller] 。外部流量都需要通過 kubenretes node 節點的 80 和 443 埠。

hostNetwork 和 hostPort 的對比

相同點

hostNetwork 和 hostPort 的本質都是暴露 Pod 所在的節點 IP 給終端使用者。因為 Pod 的生命週期並不固定,隨時都有被重構的可能。可以使用 DaemonSet 或者非親緣性策略,來保證每個 node 節點只有一個Pod 被部署。

不同點

使用 hostNetwork,pod 實際上用的是 pod 宿主機的網路地址空間;

使用 hostPort,pod IP 並非宿主機 IP,而是 cni 分配的 pod IP,跟其他普通的 pod 使用一樣的 ip 分配方式,埠並非宿主機網路監聽埠,只是使用了 DNAT 機制將 hostPort 指定的埠對映到了容器的埠之上。

Label

Label 是 kubernetes 系統中的一個重要概念。它的作用就是在資源上新增標識,用來對它們進行區分和選擇。

Label 可以新增到各種資源物件上,如 Node、Pod、Service、RC 等。Label 通常在資源物件定義時確定,也可以在物件建立後動態新增或刪除。當我們給一個物件打了標籤之後,隨後就可以通過 Label Selector(標籤選擇器)查詢和篩選擁有某些 Label 的資源物件。通過使用 Label 就能實現多維度的資源分組管理功能。

Label Selector 在 Kubernetes 中的重要使用場景如下:

1、 Kube-controller 程序通過資源物件RC上定義的 Label Selector 來篩選要監控的 Pod 副本的數量,從而實現 Pod 副本的數量始終符合預期設定的全自動控制流程;

2、 Kube-proxy 程序通過 Service 的 Label Selector 來選擇對應的 Pod,自動建立起每個 Service 到對應 Pod 的請求轉發路由表,從而實現 Service 的智慧負載均衡機制;

3、通過對某些 Node 定義特定的 Label,並且在 Pod 定義檔案中使用 NodeSelector 這種標籤排程策略,kube-scheduler 程序可以實現 Pod “定向排程”的特性。

同時藉助於 Label,k8s 中可以實現親和(affinity)與反親和(anti-affinity)排程。

親和性排程

什麼是親和(affinity)與反親和(anti-affinity)排程

Kubernetes 支援節點和 Pod 兩個層級的親和與反親和。通過配置親和與反親和規則,可以允許您指定硬性限制或者偏好,例如將前臺 Pod 和後臺 Pod 部署在一起、某類應用部署到某些特定的節點、不同應用部署到不同的節點等等。

在學習親和性排程之前,首先來看下如何進行打標籤的操作

檢視節點及其標籤

$ kubectl get nodes --show-labels

給節點新增標籤

$ kubectl label nodes <your-node-name> nodeName=node9

刪除節點的標籤

$ kubectl label nodes <your-node-name> nodeName-

檢視某個標籤的的分佈情況

$ kubectl get node -L nodeName
NAME              STATUS   ROLES    AGE    VERSION   NODENAME
kube-server7.zs   Ready    <none>   485d   v1.19.9   node7
kube-server8.zs   Ready    <none>   485d   v1.19.9   node8
kube-server9.zs   Ready    master   485d   v1.19.9   node9

Node 親和性排程策略

cat <<EOF >./pod-affinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: nodeName
                operator: In
                values:
                - node7
                - node8
EOF

重點看下下面的

affinity: // 表示親和
        nodeAffinity: // 表示節點親和
          requiredDuringSchedulingIgnoredDuringExecution: 
            nodeSelectorTerms:
            - matchExpressions:
              - key: nodeName
                operator: In
                values:
                - node7
                - node8

分下下具體的規則

requiredDuringSchedulingIgnoredDuringExecution 非常長,不過可以將這個分作兩段來看:

前半段 requiredDuringScheduling 表示下面定義的規則必須強制滿足(require)才會排程Pod到節點上;

後半段 IgnoredDuringExecution 表示已經在節點上執行的 Pod 不需要滿足下面定義的規則,即去除節點上的某個標籤,那些需要節點包含該標籤的 Pod 不會被重新排程。

operator 有下面幾種取值:

  • In:標籤的值在某個列表中;

  • NotIn:標籤的值不在某個列表中;

  • Exists:某個標籤存在;

  • DoesNotExist:某個標籤不存在;

  • Gt:標籤的值大於某個值(字串比較);

  • Lt:標籤的值小於某個值(字串比較)。

需要說明的是並沒有 nodeAntiAffinity(節點反親和),通過 NotIn 和 DoesNotExist 即可實現反親和性地排程。

requiredDuringSchedulingIgnoredDuringExecution 是一種強制選擇的規則。

preferredDuringSchedulingIgnoredDuringExecution 是優先選擇規則,表示根據規則優先選擇那些節點。

使用 preferredDuringSchedulingIgnoredDuringExecution 規則的時候,我們可以給 label 新增權重,這樣 Pod 就能按照設計的規則排程到不同的節點中了。

cat <<EOF >./pod-affinity-weight.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80 
            preference: 
              matchExpressions: 
              - key: nodeName
                operator: In 
                values: 
                - node7
                - node9
          - weight: 20 
            preference: 
              matchExpressions: 
              - key: nodeName
                operator: In 
                values: 
                - node8
EOF

上面的栗子可以看到,可以給 label 新增 weight 權重,在 preferredDuringSchedulingIgnoredDuringExecution 的規則下,就能按照我們設計的權重,部署到 label 對應的節點中。

Pod 親和性排程

除了支援 Node 的親和性排程,k8s 中還支援 Pod 和 Pod 之間的親和。

慄如:將應用的前端和後端部署在同一個節點中,從而減少訪問延遲。

Pod 親和同樣有 requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution 兩種規則。

模擬後端的 Pod 部署

cat <<EOF >./pod-affinity-backend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  labels:
    app: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
EOF

模擬前端的 Pod 部署,使得前端對應的業務使用 Pod 親和性排程和後端 Pod 部署到一起

cat <<EOF >./pod-affinity-frontend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  labels:
    app: frontend
spec:
  replicas: 5
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - topologyKey: kubernetes.io/hostname
            labelSelector:
              matchExpressions: 
              - key: app
                operator: In 
                values: 
                - backend
EOF

這裡把 backend 的 Pod 數量設定成 1,然後 frontend 的 Pod 數量設定成 5。這樣來演示 frontend 向 backend 的親和排程。

$ kubectl get pods -n study-k8s -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP              NODE              NOMINATED NODE   READINESS GATES
backend-5f489d5d4f-xcv4d    1/1     Running   0          22s   10.233.67.179   kube-server8.zs   <none>           <none>
frontend-64846f7fbf-6nsmd   1/1     Running   0          33s   10.233.67.181   kube-server8.zs   <none>           <none>
frontend-64846f7fbf-7pfq7   1/1     Running   0          33s   10.233.67.182   kube-server8.zs   <none>           <none>
frontend-64846f7fbf-dg7wx   1/1     Running   0          33s   10.233.67.178   kube-server8.zs   <none>           <none>
frontend-64846f7fbf-q7jd5   1/1     Running   0          33s   10.233.67.177   kube-server8.zs   <none>           <none>
frontend-64846f7fbf-v4hf9   1/1     Running   0          33s   10.233.67.180   kube-server8.zs   <none>           <none>

這裡有兩個點需要注意下

1、topologyKey 表示的指定的範圍,指定的也是一個 label,通過指定這個 label 來確定安裝的範圍;

2、matchExpressions 指定親和的 Pod,例如上面的栗子就是 app=frontend

不過這裡有個先後順序,首先匹配 topologyKey,然後匹配下面的 matchExpressions 規則。

Pod 的反親和性排程

有時候我們希望 Pod 部署到一起,那麼我們就會也有希望 Pod 不部署在一起的場景,這時候就需要用到反親和性排程。

cat <<EOF >./pod-affinity-frontend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  labels:
    app: frontend
spec:
  replicas: 5
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - topologyKey: kubernetes.io/hostname
            labelSelector:
              matchExpressions: 
              - key: app
                operator: In 
                values: 
                - backend
EOF

使用 podAntiAffinity 即可定義反親和性排程的策略

$ kubectl get pods -n study-k8s -o wide
NAME                        READY   STATUS    RESTARTS   AGE    IP               NODE              NOMINATED NODE   READINESS GATES
backend-5f489d5d4f-xcv4d    1/1     Running   0          108m   10.233.67.179    kube-server8.zs   <none>           <none>
frontend-567ddb4c45-6mf87   1/1     Running   0          23s    10.233.111.125   kube-server7.zs   <none>           <none>
frontend-567ddb4c45-fbj6j   1/1     Running   0          23s    10.233.111.124   kube-server7.zs   <none>           <none>
frontend-567ddb4c45-j5qpb   1/1     Running   0          21s    10.233.72.25     kube-server9.zs   <none>           <none>
frontend-567ddb4c45-qsc8m   1/1     Running   0          21s    10.233.111.126   kube-server7.zs   <none>           <none>
frontend-567ddb4c45-sfwjn   1/1     Running   0          23s    10.233.72.24     kube-server9.zs   <none>           <none>

NodeSelector 定向排程

kubernetes 中 kube-scheduler 負責實現 Pod 的排程,內部系統通過一系列演算法最終計算出最佳的目標節點。如果需要將 Pod 排程到指定 Node 上,則可以通過 Node 的標籤(Label)和 Pod 的 nodeSelector 屬性相匹配來達到目的。

慄如:DaemonSet 中使用到了 NodeSelector 定向排程。

DaemonSet(守護程序),會在每一個節點中執行一個 Pod,同時也能保證只有一個 Pod 執行。

如果有新節點加入叢集,Daemonset 會自動的在該節點上執行我們需要部署的 Pod 副本,如果有節點退出叢集,Daemonset 也會移除掉部署在舊節點的 Pod 副本。

DaemonSet 適合一些系統層面的應用,例如日誌收集,資源監控等,等這類需要每個節點都執行,且不需要太多的例項。

cat <<EOF >./pod-daemonset.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-daemonset
  labels:
    app: nginx-daemonset
spec:
  selector:
    matchLabels:
      app: nginx-daemonset
  template:
    metadata:
      labels:
        app: nginx-daemonset
    spec:
      nodeSelector: # 節點選擇,當節點擁有nodeName=node7時才在節點上建立Pod
        nodeName: node7
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
EOF

可以看到 DaemonSet 也是通過 label 來選擇部署的目標節點

nodeSelector: # 節點選擇,當節點擁有nodeName=node7時才在節點上建立Pod
        nodeName: node7

如果不新增目標節點,那麼就是在所有節點中進行部署了。

資源限制

每個 Pod 都可以對其能使用的伺服器上的計算資源設定限額,當前可以設定限額的計算資源有 CPU 和 Memory 兩種,其中 CPU 的資源單位為 CPU(Core)的數量,是一個絕對值。

對於容器來說一個 CPU 的配額已經是相當大的資源配額了,所以在 Kubernetes 裡,通常以千分之一的 CPU 配額為最小單位,用 m 來表示。通常一個容器的CPU配額被 定義為 100-300m,即佔用0.1-0.3個CPU。與 CPU 配額類似,Memory 配額也是一個絕對值,它的單位是記憶體位元組數。

對計算資源進行配額限定需要設定以下兩個引數:

  • Requests:該資源的最小申請量,系統必須滿足要求。

  • Limits:該資源最大允許使用的量,不能超過這個使用限制,當容器試圖使用超過這個量的資源時,可能會被Kubernetes Kill並重啟。

通常我們應該把 Requests 設定為一個比較小的數值,滿足容器平時的工作負載情況下的資源需求,而把 Limits 設定為峰值負載情況下資源佔用的最大量。下面是一個資源配額的簡單定義:

spec:
  containers:
  - name: db
    image: mysql
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

最小0.25個CPU及64MB記憶體,最大0.5個CPU及128MB記憶體。

當然 CPU 的指定也可以不使用 m。 cpu: "2" ,就表示 2 個 cpu。

新增資源限制的目的

在業務流量請求較低的時候,釋放多餘的資源。當有一些突發性的活動,就能根據資源佔用情況,申請合理的資源。

Pod 的持久化儲存

volume 是 kubernetes Pod 中多個容器訪問的共享目錄。volume 被定義在 pod 上,被這個 pod 的多個容器掛載到相同或不同的路徑下。volume 的生命週期與 pod 的生命週期相同,pod 內的容器停止和重啟時一般不會影響 volume 中的資料。所以一般 volume 被用於持久化 pod 產生的資料。Kubernetes 提供了眾多的 volume 型別,包括 emptyDir、hostPath、nfs、glusterfs、cephfs、ceph rbd 等。

1、emptyDir

emptyDir 型別的 volume 在 pod 分配到 node 上時被建立,kubernetes 會在 node 上自動分配 一個目錄,因此無需指定宿主機 node 上對應的目錄檔案。這個目錄的初始內容為空,當 Pod 從 node 上移除時,emptyDir 中的資料會被永久刪除。 emptyDir Volume 主要用於某些應用程式無需永久儲存的臨時目錄,多個容器的共享目錄等。

容器的 crashing 事件並不會導致 emptyDir 中的資料被刪除。

配置示例

cat <<EOF >./pod-emptyDir.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: liz2019/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}
EOF

2、hostPath

hostPath Volume 為 pod 掛載宿主機上的目錄或檔案,使得容器可以使用宿主機的高速檔案系統進行儲存。缺點是,在 k8s 中,pod 都是動態在各 node 節點上排程。當一個 pod 在當前 node 節點上啟動並通過 hostPath 儲存了檔案到本地以後,下次排程到另一個節點上啟動時,就無法使用在之前節點上儲存的檔案。

配置示例

cat <<EOF >./pod-hostPath.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: liz2019/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data-test
EOF

3、Pod 持久化儲存

1、 pod直接掛載nfs-server

我們能將 NFS (網路檔案系統) 掛載到Pod 中,不像 emptyDir 那樣會在刪除 Pod 的同時也會被刪除,nfs 卷的內容在刪除 Pod 時會被儲存,卷只是被解除安裝。 這意味著 nfs 卷可以被預先填充資料,並且這些資料可以在 Pod 之間共享,NFS 卷可以由多個pod同時掛載。

cat <<EOF >./pod-nfs.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: liz2019/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: test-nfs
  volumes:
  - name: test-nfs
    nfs:
      path: /data-test
      server: 192.268.56.111
EOF

2、pv 和 pvc

具體什麼是 pv 和 pvc,可參見 pv和pvc

建立 pv

cat <<EOF >./pv-demp.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
  labels:
    name: pv001
spec:
  nfs:
    path: /data/volumes/v1
    server: nfs
  accessModes: ["ReadWriteMany","ReadWriteOnce"]
  capacity:
    storage: 2Gi
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv002
  labels:
    name: pv002
spec:
  nfs:
    path: /data/volumes/v2
    server: nfs
  accessModes: ["ReadWriteOnce"]
  capacity:
    storage: 1Gi
EOF

建立 pvc

cat <<EOF >./pvc-demp.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: testpvc
  namespace: default
spec:
  accessModes: ["ReadWriteMany"]
  resources:
    requests:
      storage: 1Gi
EOF

業務繫結 pvc

cat <<EOF >./pod-pvc-cache.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: liz2019/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: test-pvc-cache
  volumes:
  - name: test-pvc-cache
    persistentVolumeClaim:
      claimName: testpvc
EOF

Pod 的探針

k8s 中當 Pod 的執行出現異常的時候,需要能夠檢測到 Pod 的這種狀態,將流量路由到其他正常的 Pod 中,同時去恢復損壞的元件。

預設情況下,Kubernetes 會觀察 Pod 生命週期,並在容器從掛起(pending)狀態轉移到成功(succeeded)狀態時,將流量路由到 Pod。Kubelet 會監控崩潰的應用程式,並重新啟動 Pod 進行恢復。

但是 Pod 的健康並不代表,Pod 中所有的容器都已經專準備好了,例如,這時候的應用可能正在啟動,處於連線資料庫的狀態中,處於編譯的狀態中,這時候打進來的流量,得到的返回肯定就是異常的了。

k8s 中引入了活躍(Liveness)、就緒(Readiness)和啟動(Startup)探針來解決上面的問題。

存活探針:主要來檢測容器中的應用程式是否正常執行,如果檢測失敗,就會使用 Pod 中設定的 restartPolicy 策略來判斷,Pod是否要進行重啟, 例如,存活探針可以探測到應用死鎖。

就緒探針:可以判斷容器已經啟動就緒能夠接收流量請求了,當一個 Pod 中所有的容器都就緒的時候,才認為該 Pod 已經就緒了。就緒探針就能夠用來避免上文中提到的,應用程式未完全就緒,然後就有流量打進來的情況發生。

啟動探針:kubelet 使用啟動探針來了解應用容器何時啟動。啟動探針主要是用於對慢啟動容器進行存活性檢測,避免它們在啟動執行之前就被殺掉。

如果提供了啟動探針,則所有其他探針都會被禁用,直到此探針成功為止。

如果啟動探測失敗,kubelet 將殺死容器,而容器依其重啟策略進行重啟。

在配置探針之前先來看下幾個主要的配置

initialDelaySeconds :容器啟動後要等待多少秒後才啟動存活和就緒探針, 預設是 0 秒,最小值是 0;

periodSeconds :執行探測的時間間隔(單位是秒)。預設是 10 秒。最小值是 1;

timeoutSeconds :探測的超時後等待多少秒。預設值是 1 秒。最小值是 1;

successThreshold :探針在失敗後,被視為成功的最小連續成功數。預設值是 1。 存活和啟動探測的這個值必須是 1。最小值是 1;

failureThreshold :將探針標記為失敗之前的重試次數。對於 liveness 探針,這將導致 Pod 重新啟動。對於 readiness 探針,將標記 Pod 為未就緒(unready)。

目前 ReadinessProbe 和 LivenessProbe 都支援下面三種探測方法

ExecAction :在容器中執行指定的命令,如果能成功執行,則探測成功;

HTTPGetAction :通過容器的IP地址、埠號及路徑呼叫HTTP Get方法,如果響應的狀態碼200-400,則認為容器探測成功;

TCPSocketAction :通過容器IP地址和埠號執行TCP檢查,如果能建立TCP連線,則探測成功;

gRPC 活躍探針 :在 Kubernetes v1.24 [beta] 中引入了,gRPC 活躍探針,如果應用實現了 gRPC 健康檢查協議 ,kubelet 可以配置為使用該協議來執行應用活躍性檢查。不過使用的時候需要先開啟。

這裡來看下使用 HTTPGetAction 的方式,來看下存活探針和就緒探針的配置,其他的可參考 配置存活、就緒和啟動探針

cat <<EOF >./pod-test-go-liveness.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: test-go-liveness
  name: test-go-liveness
  namespace: study-k8s
spec:
  replicas: 5
  selector:
    matchLabels:
      app: go-web
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: go-web
    spec:
      containers:
      - image: liz2019/test-go-liveness
        name: test-go-liveness
        livenessProbe: # 活躍探針
          httpGet:
            path: /healthz
            port: 8001
          initialDelaySeconds: 15
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe: # 存活探針
          httpGet:
            path: /healthz
            port: 8001
          initialDelaySeconds: 5 # 容器啟動後多少秒啟動探針
          periodSeconds: 10 # 執行探針檢測的時間建個間隔
          timeoutSeconds: 5 探測的超時後等待多少秒
status: {}
EOF

可以看到上面同時設定了存活探針和就緒探針;

對於映象中的程式碼設定了超過十秒即返回錯誤,這樣上面的示例就能模擬探針檢測失敗,然後重啟的效果了

func healthz(w http.ResponseWriter, r *http.Request) {

	duration := time.Now().Sub(started)

	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	if duration.Seconds() > 10 {
		w.WriteHeader(500)
		w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
	} else {
		w.WriteHeader(200)
		w.Write([]byte("ok"))
	}
}

檢視 Pod 的重啟情況

kubectl get pods -n study-k8s
NAME                                READY   STATUS             RESTARTS   AGE
test-go-liveness-66755487cd-ck4mz   0/1     CrashLoopBackOff   6          11m
test-go-liveness-66755487cd-f54lz   0/1     CrashLoopBackOff   6          11m
test-go-liveness-66755487cd-hts8k   0/1     CrashLoopBackOff   7          11m
test-go-liveness-66755487cd-jzsmb   0/1     Running            7          11m
test-go-liveness-66755487cd-k9hdk   1/1     Running            7          11m

下面在簡單看下啟動探針的配置

ports:
- name: liveness-port
  containerPort: 8080
  hostPort: 8080

livenessProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 1
  periodSeconds: 10

startupProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 30 # 將探針標記為失敗之前的重試次數
  periodSeconds: 10 # 執行探測的時間間隔(單位是秒)

上面配置了啟動探針,那麼程式就會有 30 * 10 = 300s 來完成啟動過程。 一旦啟動探測成功一次,存活探測任務就會接管對容器的探測,對容器死鎖作出快速響應。 如果啟動探測一直沒有成功,容器會在 300 秒後被殺死,並且根據 restartPolicy 來 執行進一步處置。

使用 HPA 實現 Pod 的自動擴容

我們通過手動更改 RS 副本集的大小就能實現 Pod 的擴容,但是這是我們手動操作的,人工不可能24小時不間斷的檢測業務的複雜變化。所以需要自動擴容的機制,能夠根據業務的變化,自動的實現 Pod 的擴容和縮容。

Kubernetes 提供 Horizontal Pod Autoscaling(Pod 水平自動伸縮) ,簡稱 HPA。通過使用 HPA 可以自動縮放在 Replication Controller,Deployment 或者 Replica Set 中的 Pod。HPA 將自動縮放正在執行的 Pod 的數量,以實現最高效率。

HPA 中影響 Pod 數量的因素包括:

  • 使用者定義的允許執行的 Pod 的最小和最大數量;

  • 資源指標中報告的觀察到的 CPU 或記憶體使用情況;

  • 第三方指標應用程式(例如 Prometheus,Datadog 等)提供的自定義指標。

HPA 是如何工作的

HPA 是用來控制 Pod 水平伸縮的控制器,基於設定的擴縮容規則,實時採集監控指標資料,根據使用者設定的指標閾值計算副本數,進而調整目標資源的副本數量,完成擴縮容操作。

演算法的細節

Pod 水平自動擴縮控制器根據當前指標和期望指標來計算擴縮比例

期望副本數 = ceil[當前副本數 * (當前指標 / 期望指標)]

例如,如果當前指標值為 200m,而期望值為 100m,則副本數將加倍, 因為 200.0 / 100.0 == 2.0 如果當前值為 50m,則副本數將減半, 因為 50.0 / 100.0 == 0.5。如果比率足夠接近 1.0(在全域性可配置的容差範圍內,預設為 0.1), 則控制平面會跳過擴縮操作。

如果 HorizontalPodAutoscaler 指定的是 targetAverageValue 或 targetAverageUtilization, 那麼將會把指定 Pod 度量值的平均值做為當前指標。

HPA 控制器執行擴縮操作並不是馬上完成的,會有一個擴縮操作的時間視窗。首先每次的擴縮操作建議會被記錄,在擴縮操作的時間視窗內,會根據規則選出一個縮容的策略。這樣就能讓系統更平滑的進行擴縮操作,消除短時間內指標波動帶來的影響。時間視窗的值可通過 kube-controller-manager 服務的啟動引數 --horizontal-pod-autoscaler-downscale-stabilization 進行配置, 預設值為 5 分鐘。

來個栗子實踐下

cat <<EOF >./pod-hpa.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: go-web
  name: go-web
  namespace: study-k8s
spec:
  replicas: 5
  selector:
    matchLabels:
      app: go-web
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: go-web
    spec:
      containers:
      - image: liz2019/test-docker-go-hub
        name: go-app-container
        resources: 
          requests:
            cpu: 0.1
            memory: 50Mi
          limits:
            cpu: 0.2
            memory: 100Mi
status: {}
---

apiVersion: v1
kind: Service
metadata:
  name: go-web-svc
  labels:
    run: go-web-svc
spec:
  selector:
    app: go-web
  type: NodePort # 服務型別
  ports:
    - protocol: TCP
      port: 8000
      targetPort: 8000
      nodePort: 30001  # 對外暴露的埠
      name: go-web-http
EOF

配置 HPA 策略

cat <<EOF >./pod-hpa-config.yaml
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: go-web-hpa
  namespace: study-k8s
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: go-web
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
      
        type: Utilization
        averageUtilization: 50
EOF

關於 HPA 的版本支援

autoscaling/v1 只支援CPU一個指標的彈性伸縮;

autoscaling/v2beta1 支援自定義指標;

autoscaling/v2beta2 支援外部指標;

總結

1、Pod 是 Kubernetes 叢集中能夠被建立和管理的最小部署單元;

2、通常使用控制起來管理 Pod,來實現 Pod 的滾動升級,副本管理,叢集級別的自愈能力;

3、Deployment 可以用來建立一個新的服務,更新一個新的服務,也可以用來滾動升級一個服務。藉助於 ReplicaSet 也可以實現 Pod 的副本管理功能;

4、靜態 Pod 是由 kubelet 進行管理的僅存在與特定 node 上的 Pod,他們不能通過 api server 進行管理,無法與 rc,deployment,ds 進行關聯,並且 kubelet 無法對他們進行健康檢查;

5、靜態 Pod 主要是用來對叢集中的元件進行容器化操作,例如 etcd kube-apiserver kube-controller-manager kube-scheduler 這些都是靜態 Pod 資源;

6、Pod 一般不直接對外暴露服務,一個 Pod 只是一個執行服務的例項,隨時可能在節點上停止,然後再新的節點上用一個新的 IP 啟動一個新的 Pod,因此不能使用確定的 IP 和埠號提供服務;

7、Pod 中可以使用 hostNetwork 和 hostPort,可以直接暴露 node 的 ip 地址;

8、Label 是 kubernetes 系統中的一個重要概念。它的作用就是在資源上新增標識,用來對它們進行區分和選擇;

9、Kubernetes 支援節點和 Pod 兩個層級的親和與反親和。通過配置親和與反親和規則,可以允許您指定硬性限制或者偏好,例如將前臺 Pod 和後臺 Pod 部署在一起、某類應用部署到某些特定的節點、不同應用部署到不同的節點等等;

10、為了合理的利用 k8s 中的資源,一般會對 Pod 新增資源限制;

11、k8s 中當 Pod 的執行出現異常的時候,需要能夠檢測到 Pod 的這種狀態,將流量路由到其他正常的 Pod 中,同時去恢復損壞的元件,這種情況可以通過 K8s 中的探針去解決;

12、面對業務情況的變化,使用 HPA 實現 Pod 的自動擴容;

參考

【初識Kubernetes(K8s):各種資源物件的理解和定義】https://blog.51cto.com/andyxu/2329257

【Kubernetes系列學習文章 - Pod的深入理解(四)】https://cloud.tencent.com/developer/article/1443520

【詳解 Kubernetes Pod 的實現原理】https://draveness.me/kubernetes-pod/

【Kubernetes 之Pod學習】https://www.cnblogs.com/kevingrace/p/11309409.html

【親和與反親和排程】https://support.huaweicloud.com/intl/zh-cn/basics-cce/kubernetes_0018.html

【hpa】https://kubernetes.io/zh-cn/docs/tasks/run-application/horizontal-pod-autoscale/