k8s工作負載控制器--Statefulset

misakivv發表於2024-09-06

目錄
  • 一、概述
  • 二、引入"有狀態"需求
    • 1、管理無狀態服務的 Deployment 實現了什麼
      • 1.1、建立 Deployment
      • 1.2、驗證 Pod 數量
      • 1.3、配置更新策略(更新映象版本)
      • 1.4、觀察更新過程
      • 1.5、驗證更新後 Pod 的狀態
      • 1.6、回滾 Deployment
    • 2、新需求分析
  • 三、StatefulSet:面向有狀態應用管理的控制器
    • 1、建立 Service、Statefulset
    • 2、Service、StatefulSet 狀態
    • 3、Pod、PVC 狀態
    • 4、Pod 的版本控制
    • 5、更新映象
    • 6、檢視新版本的狀態
    • 7、如何檢視 Pod 是否複用了之前的網路標識
    • 8、如何檢視 Pod 是否複用了之前的儲存盤
  • 四、架構設計
    • 1、管理模式
      • 1.1、ControllerRevision
      • 1.2、PVC
      • 1.3、Pod
    • 2、OwnerReference
      • 2.1、刪除 StatefulSet
    • 3、StatefulSet 控制器工作流程
    • 4、擴容能力
    • 5、擴縮容管理策略
    • 6、釋出模擬
  • 五、欄位分析
    • 1、spec 欄位解析
    • 2、升級策略欄位解析

一、概述

StatefulSet 是 Kubernetes 中用於管理有狀態應用程式的工作負載資源物件。它提供了一種管理有狀態服務的方式,確保每個 Pod 都有一個唯一的、持久的身份,並支援持久化儲存。它在 Kubernetes v1.9 版本中成為 GA 版本。StatefulSet 設計用於管理和部署有狀態服務,其管理的 Pod 擁有固定的名稱(通常是 - 形式)和有序的啟動和停止順序。

StatefulSet 中,Pod 名稱可以用作網路識別符號,並且通常與 Headless Service 結合使用。Headless Service 沒有 Cluster IP,直接暴露 Pod 的 IP 地址或 DNS 名稱。解析 Headless Service 的名稱時,會返回所有 Pod 的 IP 地址或 DNS 名稱。

此外,StatefulSet 在 Headless Service 的基礎上為每個 Pod 建立了一個 DNS 域名,這些域名通常形如 ...svc.cluster.local。

StatefulSet 支援持久化儲存,它可以使用多種持久化儲存型別,包括本地儲存、網路檔案系統(NFS)等。

二、引入"有狀態"需求

1、管理無狀態服務的 Deployment 實現了什麼

10400250317bt89glg2001gnmnc

首先它支援定義一組 Pod 的期望數量,Controller 會為我們維持 Pod 的數量在期望的版本以及期望的數量;

第二它支援配置 Pod 釋出方式,配置完成後 Controller 會按照我們給出的策略來更新 Pod,同時在更新的過程中,也會保證不可用 Pod 數量在我們定義的範圍內;

第三,如果我們在釋出的過程中遇到問題,Deployment 也支援一鍵來回滾。

簡單來說,Deployment 認為:它管理的所有相同版本的 Pod 都是一模一樣的副本。也就是說,在 Deployment Controller 看來,所有相同版本的 Pod,不管是裡面部署的應用還是行為,都是完全相同的。

1.1、建立 Deployment

cat >> web-app.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
      annotations:
        deployment.kubernetes.io/revision: "1"
    spec:
      containers:
      - name: web-server
        image: nginx:1.15.0
        ports:
        - containerPort: 80
EOF
kubectl apply -f web-app.yaml --record=true

1.2、驗證 Pod 數量

透過 Deployment 控制器,我們可以驗證 Pod 的數量是否維持在期望的數量(3 個)。

kubectl get pods -l app=web-app

img

1.3、配置更新策略(更新映象版本)

kubectl set image deploy web-app web-server=nginx:1.16.0 --record=true

1.4、觀察更新過程

kubectl rollout status deploy web-app

img

1.5、驗證更新後 Pod 的狀態

kubectl get pods -l app=web-app -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[*].image,STATUS:.status.phase'

img

1.6、回滾 Deployment

以下就是回滾到指定版本的流程:

檢視歷史版本資訊 --> 檢視指定版本詳細資訊 --> 回滾到指定版本 --> 驗證資訊

kubectl rollout history deploy web-app

kubectl rollout history deploy web-app --revision=1

kubectl rollout undo deploy web-app --to-revision=1

kubectl get pods -l app=web-app -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[*].image,STATUS:.status.phase'

img

以上就是管理無狀態服務的 Deployment 實現了什麼功能展示


2、新需求分析

以下需求來自於一些有狀態應用:

  • Pod 之間並非相同的副本,每一個 Pod 都有一個獨立標識
  • Pod 獨立標識要能夠對應到一個固定的網路標識,並在釋出升級後繼續保持
  • 每一個 Pod 有一塊獨立的儲存盤,並在釋出升級後還能繼續掛載原有的盤(保留資料)

這些有狀態應用的需求都是 Deploment 無法滿足的,因此引入了管理有狀態應用的 StatefulSet

三、StatefulSet:面向有狀態應用管理的控制器

img

  • 首先,每個 Pod 會有 Order 序號,會按照序號來建立,刪除和更新 Pod;
  • 其次,透過配置一個 headless Service,使每個 Pod 有一個唯一的網路標識 (hostname);
  • 第三,透過配置 pvc 模板,就是 pvc template,使每個 Pod 有一塊或者多塊 pv 儲存盤;
  • 最後,支援一定數量的灰度釋出。比如現在有三個副本的 StatefulSet,我們可以指定只升級其中的一個或者兩個,更甚至是三個到新版本。透過這樣的方式,來達到灰度升級的目的。

1、建立 Service、Statefulset

cat >>service-nginx.yaml << EOF
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  clusterIP: None
  ports:
    - name: web
      port: 80
  selector:
    app: nginx
EOF

這是一個 Service 的配置,我們透過配置 headless Service,其實想要達到的目標是:期望 StatefulSet 裡面的 Pod 有獨立的網路標識。這裡的 Service name 叫 nginx。

cat >> nginx-web.yaml << EOF
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-web
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.16.0
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www-storage
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www-storage
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi
EOF

這是一個 StatefulSet 的配置,在 spec 中有個 serviceName 也叫 nginx。透過這個 serviceName 來指定這個 StatefulSet 要對應哪一個 Service

這個 spec 中還定義了 selectortemplateselector 是一個標籤選擇器,selector 定義的標籤選擇邏輯,必須匹配 templatemetadatalabels 包含 app: nginx。在 template 中定義一個 nginx container,這個 container 用的 image 版本是 1.16.0 版本,對外暴露的 80 埠作為一個 web 服務。

最後,template.spec 裡面定義了一個 volumeMounts,這個 volumeMounts 並不是來源於 spec 中的一個 Volumes,而是來自於 volumeClaimTemplates,也就是 pvc 模板。我們在 pvc 模板中定義了一個叫 www-storage 的 pvc 名稱。這個 pvc 名稱,我們也會寫到 volumeMounts 作為一個 volume name,掛載到 /usr/share/nginx/html 這個目錄下。透過這樣的方式來達到每個 Pod 都有獨立的一個 pvc,並且掛載到容器中對應目錄的一個需求。

kubectl apply -f service-nginx.yaml -f nginx-web.yaml

2、Service、StatefulSet 狀態

kubectl get svc nginx
 
kubectl get endpoints nginx
 
kubectl get sts nginx-web

img

透過將上文中的兩個物件建立之後,我們可以透過 get 命令可以看到 Service nginx 資源已經建立成功。

同時可以透過檢視 endpoints 看到,這個後端已經註冊了三個 IP 和埠,這三個 IP 對應了 Pod 的 IP,埠對應了之前 spec 中配置的 80 埠。

最後透過 get sts (StatefulSet 縮寫) nginx-web。從結果可以看到有一列 READY,值為 3/3。分母 3 是 StatefulSet 中期望的數量,而分子 3 表示 Pod 已經達到期望 READY 的狀態數量。

3、Pod、PVC 狀態

kubectl get pods -l app=nginx
 
kubectl get pvc

img

透過 get pvc 可以看到 NAME 那一列名稱,字首為 www-storage,中間是 nginx-web,字尾是一個序號。透過分析可以知道 www-storagevolumeClaimTemplates 中定義的 name,中間為 StatefulSet 定義的 name,末尾的序號對應著 Pod 的序號,也就是三個 PVC 分別被三個 Pod 繫結。透過這樣一種方式,達到不同的 Pod 享有不同的 PVC;PVC 也會繫結相應的一個 PV, 來達到不同的 Pod 繫結不同 PV 的目的。

4、Pod 的版本控制

不同於 Deployment 使用 ReplicaSet 來管理版本和維持副本數,StatefulSet controller 直接管理下屬的 Pod ,而 Pod 中用一個 label 來標識版本

kubectl get pods -L controller-revision-hash

img

之前我們學到 Deployment 使用 ReplicaSet 來管理 Pod 的版本和所期望的 Pod 數量,但是在 StatefulSet 中,是由 StatefulSet Controller 來管理下屬的 Pod,因此 StatefulSet 透過 Pod 的 label 來標識這個 Pod 所屬的版本,這裡叫 controller-revision-hash。這個 label 標識和 Deployment 以及 StatefulSet 在 Pod 中注入的 Pod template hash 是類似的。

如上圖所示,透過 get pod 檢視到 controller-revision-hash,這裡的 hash 就是第一次建立 Pod 對應的 template 版本,可以看到字尾是6b558f6465。這裡先記錄一下,接下來會做 Pod 升級,再來看一下 controller-revision-hash 會不會發生改變。

5、更新映象

img

kubectl set image statefulset nginx-web nginx=nginx:1.15.0

img

6、檢視新版本的狀態

kubectl get pods -L controller-revision-hash
 
kubectl get sts nginx-web -o json | jq '.status'

imgimg

透過 get pod 命令查詢 Revision hash,可以看到三個 Pod 後面的 controller-revision-hash 都已經升級到了新的 Revision hash,字尾由6b558f6465 變成了 84d64959d6。透過這三個 Pod 建立的時間可以發現:序號為 2 的 Pod 建立的是最早的,之後是序號是 1 和 0。這表示在升級的過程中,真實的升級順序為 2-1-0,透過這麼一個倒序的順序來逐漸把 Pod 升級為新版本,並且我們升級的 Pod,還複用了之前 Pod 使用的 PVC。所以之前在 PV 儲存盤中的資料,仍然會掛載到新的 Pod 上。

透過在 StatefulSet 的 status 中看到的資料,這裡有幾個重要的欄位:

  • currentReplica:表示當前版本的數量
  • currentRevision: 表示當前版本號
  • updateReplicas:表示新版本的數量
  • updateRevision:表示當前要更新的版本號

當然這裡也能看到 currentReplicaupdateReplica,以及 currentRevisionupdateRevision 都是一樣的,這就表示所有 Pod 已經升級到了所需要的版本。

7、如何檢視 Pod 是否複用了之前的網路標識

其實 headless Service 配置的 hostname 只是和 Pod name 掛鉤的,所以只要升級後的 Pod 名稱和舊的 Pod 名稱相同,那麼就可以沿用之前 Pod 使用的網路標識。

8、如何檢視 Pod 是否複用了之前的儲存盤

kubectl get pvc

img

關於儲存盤,可以檢視 PVC 的狀態,它們的建立時間一直沒有改變,還是第一次建立 Pod 時的時間,所以現在升級後的 Pod 使用的還是舊 Pod 中使用的 PVC。

kubectl get pods nginx-web-0 -o yaml

img

或者可以檢視其中的某一個 Pod,這個 Pod 裡面同樣有個宣告的 volumes,這個 persistentVolumeClaim 裡的名稱 www-storage-nginx-web-0,對應著 PVC 列表中看到的序號為 0 的 PVC,之前是被舊的 Pod 所使用。升級過程中 Controller 刪除舊 Pod,並且建立了一個同名的新 Pod,新的 Pod 仍然複用了舊 Pod 所使用的 PVC。

透過這種方式來達到升級前後,網路儲存都能複用的目的。

四、架構設計

1、管理模式

StatefulSet 一般會建立三種型別的資源

1.1、ControllerRevision

透過這個資源,StatefulSet 可以很方便地管理不同版本的 template 模板。

kubectl get controllerrevision -l app=nginx
 
kubectl get pods -l app=nginx --show-labels

img

比如上面例子中的 container nginx,在建立之初擁有的第一個 template 版本,會建立一個對應的 ControllerRevision。而當修改了 image 版本之後,StatefulSet Controller 會建立一個新的 ControllerRevision,可以理解為每一個 ControllerRevision 對應了每一個版本的 Template,也對應了每一個版本的 ControllerRevision hash。其實在 Pod label 中定義的 ControllerRevision hash,就是 ControllerRevision 的名字。透過這個資源 StatefulSet Controller 來管理不同版本的 template 資源。

1.2、PVC

如果在 StatefulSet 中定義了 volumeClaimTemplates,StatefulSet 會在建立 Pod 之前,先根據這個模板建立 PVC,並把 PVC 加到 Pod volume 中。

如果使用者在 spec 的 pvc 模板中定義了 volumeClaimTemplatesStatefulSet 在建立 Pod 之前,根據模板建立 PVC,並加到 Pod 對應的 volume 中。當然也可以在 spec 中不定義 pvc template,那麼所建立出來的 Pod 就不會掛載單獨的一個 pv。

1.3、Pod

StatefulSet 按照順序建立、刪除、更新 Pod,每個 Pod 有唯一的序號。

2、OwnerReference

StatefulSet Controller 是 Owned 三個資源:ControllerRevisionPodPVC

這裡不同的地方在於,當前版本的 StatefulSet 只會在 ControllerRevisionPod 中新增 OwnerReference,而不會在 PVC 中新增 OwnerReference。擁有 OwnerReference 的資源,在管理的這個資源進行刪除的預設情況下,會關聯級聯刪除下屬資源。因此預設情況下刪除 StatefulSet 之後,StatefulSet 建立的 ControllerRevision 和 Pod 都會被刪除,但是 PVC 因為沒有寫入 OwnerReference,PVC 並不會被級聯刪除。

10400250317c5m1qo0201ile94k

2.1、刪除 StatefulSet

kubectl delete sts nginx-web
 
kubectl get pods -l app=nginx
 
kubectl get pvc
 
kubectl get controllerrevision -l app=nginx

img

可以看到由 StatefulSet 建立的 ControllerRevisionPod 均被刪除,但是 PVC 未被刪除

3、StatefulSet 控制器工作流程

10400250317c6hbpag202r992i0

首先透過註冊 InformerEvent Handler(事件處理),來處理 StatefulSetPod 的變化。在 Controller 邏輯中,每一次收到 StatefulSet 或者是 Pod 的變化,都會找到對應的 StatefulSet 放到佇列。緊接著從佇列取出來處理後,先做的操作是 Update Revision,也就是先檢視當前拿到的 StatefulSet 中的 template,有沒有對應的 ControllerRevision。如果沒有,說明 template 已經更新過,Controller 就會建立一個新版本的 Revision,也就有了一個新的 ControllerRevision hash 版本號。

然後 Controller 會把所有版本號拿出來,並且按照序號整理一遍。這個整理的過程中,如果發現有缺少的 Pod,它就會按照序號去建立,如果發現有多餘的 Pod,就會按照序號去刪除。當保證了 Pod 數量和 Pod 序號滿足 Replica 數量之後,Controller 會去檢視是否需要更新 Pod。也就是說這兩步的區別在於,Manger pods in order 去檢視所有的 Pod 是否滿足序號;而後者 Update in order 檢視 Pod 期望的版本是否符合要求,並且透過序號來更新。

Update in order 其更新過程如上圖所示,其實這個過程比較簡單,就是刪除 Pod。刪除 Pod 之後,其實是在下一次觸發事件,Controller 拿到這個 success 之後會發現缺少 Pod,然後再從前一個步驟 Manger pod in order 中把新的 Pod 建立出來。在這之後 Controller 會做一次 Update status,也就是之前透過命令列看到的 status 資訊。

透過整個這樣的一個流程,StatefulSet 達到了管理有狀態應用的能力。

4、擴容能力

10400250317c70sci02013q2ask

假設 StatefulSet 初始配置 replicas 為 1,有一個 Pod0。那麼將 replicas 從 1 修改到 3 之後,其實我們是先建立 Pod1,預設情況是等待 Pod1 狀態 READY 之後,再建立 Pod2。

透過上圖可以看到每個 StatefulSet 下面的 Pod 都是從序號 0 開始建立的。因此一個 replicas 為 N 的 StatefulSet,它建立出來的 Pod 序號為 [0,N),也就是當 N>0 的時候,序號為 0 到 N-1。

5、擴縮容管理策略

StatefulSet.spec 中,有一個欄位名為 podManagementPolicy ,可選策略為 OrderedReadyParallel,預設為前者

OrderedReady:擴縮容按照 order 順序執行。擴容時,必須前面序號的 Pod 都 Ready 了才能擴下一個,縮容時按照倒序刪除

Parallel:並行擴縮容,不需要等前面的 Pod Ready/刪除後再處理下一個

上面建立的例子,沒有在 spec 中定義 podMangementPolicy。那麼 Controller 預設 OrderedReady 作為策略,然後在 OrderedReady 情況下,擴縮容就嚴格按照 Order 順序來執行,必須要等前面的 Pod 狀態為 Ready 之後,才能擴容下一個 Pod。在縮容的時候,倒序刪除,序號從大到小進行刪除。

img

在這個例子中,從 Pod0 擴容到 Pod0、Pod1、Pod2 的時候,必須先建立 Pod1,等 Pod1 Ready 之後再建立 Pod2。其實還存在一種可能性:比如在建立 Pod1 的時候,Pod0 因為某些原因,可能是宿主機的原因或者是應用本身的原因,Pod0 變成 NotReady 狀態。這時 Controller 也不會建立 Pod2,所以不只是我們所建立 Pod 的前一個 Pod 要 Ready,而是前面所有的 Pod 都要 Ready 之後,才會建立下一個 Pod。上圖中的例子,如果要建立 Pod2,那麼 Pod0、Pod1 都要 ready。

6、釋出模擬

10400250317ccbdls0205g16o2g

假設這裡的 StatefulSet template1 對應邏輯上的 Revision1,這時 StatefulSet 下面的三個 Pod 都屬於 Revision1 版本。在我們修改了 template,比如修改了映象之後,Controller 是透過倒序的方式逐一升級 Pod。上圖中可以看到 Controller 先建立了一個 Revision2,對應的就是建立了 ControllerRevision2 這麼一個資源,並且將 ControllerRevision2 這個資源的 name 作為一個新的 Revision hash。在把 Pod2 升級為新版本後,逐一刪除 Pod0、Pod1,再去建立 Pod0、Pod1。

它的邏輯其實很簡單,在升級過程中 Controller 會把序號最大並且符合條件的 Pod 刪除掉,那麼刪除之後在下一次 Controller 在做 reconcile 的時候,它會發現缺少這個序號的 Pod,然後再按照新版本把 Pod 建立出來。

五、欄位分析

git clone git@github.com:kubernetes/kubernetes.git

1、spec 欄位解析

img

  • Replica 主要是期望的數量;
  • Selector 是事件選擇器,必須匹配 spec.template.metadata.labels 中定義的條件;
  • Template:Pod 模板,定義了所要建立的 Pod 的基礎資訊模板;
  • VolumeClaimTemplates:PVC 模板列表,如果在 spec 中定義了這個,PVC 會先於 Pod 模板 Template 進行建立。在 PVC 建立完成後,把建立出來的 PVC name 作為一個 volume 注入到根據 Template 建立出來的 Pod 中。

img

  • ServiceName:對應 Headless Service 的名字。當然如果有人不需要這個功能的時候,會給 Service 定一個不存在的 value,Controller 也不會去做校驗,所以可以寫一個 fake 的 ServiceName。但是這裡推薦每一個 Service 都要配置一個 Headless Service,不管 StatefulSet 下面的 Pod 是否需要網路標識;
  • PodMangementPolicy:Pod 管理策略。前面提到過這個欄位的可選策略為 OrderedReadyParallel,預設情況下為前者;
  • UpdataStrategy:Pod 升級策略。
  • RevisionHistoryLimit:保留歷史 ControllerRevision 的數量限制(預設為 10)。需要注意的一點是,這裡清楚的版本,必須沒有相關的 Pod 對應這些版本,如果有 Pod 還在這個版本中,這個 ControllerRevision 是不能被刪除的。

2、升級策略欄位解析

img

可以看到 StatefulSetUpdateStrategy 有個 type 欄位,這個 type 定義了兩個型別:一個是 RollingUpdate;一個是 OnDelete

  • RollingUpdate 其實跟 Deployment 中的升級是有點類似的,就是根據滾動升級的方式來升級;
  • OnDelete 是在刪除的時候升級,叫做禁止主動升級,Controller 並不會把存活的 Pod 做主動升級,而是透過 OnDelete 的方式。比如說當前有三個舊版本的 Pod,但是升級策略是 OnDelete,所以當更新 spec 中映象的時候,Controller 並不會把三個 Pod 逐一升級為新版本,而是當我們縮小 Replica 的時候,Controller 會先把 Pod 刪除掉,當我們下一次再進行擴容的時候,Controller 才會擴容出來新版本的 Pod。

RollingUpdateStatefulSetSetStrategy 中,可以看到有個欄位叫 Partition。這個 Partition 表示滾動升級時,保留舊版本 Pod 的數量。不是灰度新版本的數量

舉個例子:假設當前有個 replicas 為 10 的 StatefulSet,當我們更新版本的時候,如果 Partition 是 8,並不是表示要把 8 個 Pod 更新為新版本,而是表示需要保留 8 個 Pod 為舊版本,只更新 2 個新版本作為灰度。當 Replica 為 10 的時候,配置 Partition 為 8 的時候,其實還是保留 [0,8) 這 8 個 Pod 為舊版本,只有 [8,10) 進入新版本。

總結一下,假設 replicas=NPartition=M (M<N) ,則最終舊版本 Pod 為 [0,M) ,新版本 Pod 為 [M,N)。透過這樣一個 Partition 的方式來達到灰度升級的目的,這是目前 Deployment 所不支援的。

相關文章