1. 程式人生 > >5.深入k8s:StatefulSet控制器

5.深入k8s:StatefulSet控制器

> 轉載請宣告出處哦~,本篇文章釋出於luozhiyun的部落格:https://www.luozhiyun.com ![image-20200807220814361](https://img.luozhiyun.com/20200807220926.png) 在上一篇中,講解了容器持久化儲存,從中我們知道什麼是PV和PVC,這一篇我們講通過StatefulSet來使用它們。如果覺得我講的不錯的,可以發個郵件鼓勵一下我噢~ 我們在第三篇講的Deployment控制器是應用於無狀態的應用的,所有的Pod啟動之間沒有順序,Deployment可以任意的kill一個Pod不會影響到業務資料,但是這到了有狀態的應用中就不管用了。 而StatefulSet就是用來對有狀態應用提供支援的控制器。 StatefulSet把真實世界裡的應用狀態,抽象為了兩種情況: 1. 拓撲狀態。應用的多個例項之間不是完全對等的關係。這些應用例項,必須按照某些順序啟動,比如應用的主節點 A 要先於從節點 B 啟動。並且,新創建出來的 Pod,必須和原來 Pod 的網路標識一樣。 2. 儲存狀態。應用的多個例項分別綁定了不同的儲存資料,對於這些應用例項來說,Pod A 第一次讀取到的資料,和隔了十分鐘之後再次讀取到的資料,應該是同一份,哪怕在此期間 Pod A 被重新建立過。 StatefulSet 的核心功能,就是通過某種方式記錄這些狀態,然後在 Pod 被重新建立時,能夠為新 Pod 恢復這些狀態。 ### 拓撲狀態 在k8s中,Service是用來將一組 Pod 暴露給外界訪問的一種機制。Service可以通過DNS的方式,代理到某一個Pod,然後通過DNS記錄的方式解析出被代理 Pod 的 IP 地址。 如下: ```yaml apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx ``` 這個Service會通過Label Selector選擇所有攜帶了 app=nginx 標籤的 Pod,都會被這個 Service 代理起來。 它所代理的所有 Pod 的 IP 地址,都會被繫結一個這樣格式的 DNS 記錄,如下所示: ``` ...svc.cluster.local ``` 所以通過這個DNS記錄,StatefulSet就可以使用到DNS 記錄來維持 Pod 的拓撲狀態。 如下: ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: serviceName: "nginx" replicas: 2 # by default is 1 selector: matchLabels: app: nginx # has to match .spec.template.metadata.labels template: metadata: labels: app: nginx # has to match .spec.selector.matchLabels spec: containers: - name: nginx image: nginx:1.9.1 ports: - containerPort: 80 name: web ``` 這裡使用了serviceName=nginx,表明StatefulSet 控制器會使用nginx 這個Service來進行網路代理。 我們可以如下建立: ```shell $ kubectl create -f svc.yaml $ kubectl get service nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ClusterIP None 80/TCP 10s $ kubectl create -f statefulset.yaml $ kubectl get statefulset web NAME DESIRED CURRENT AGE web 2 1 19s ``` 然後我們可以觀察pod的建立情況: ```shell $ kubectl get pods -w -l app=nginx NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 76m web-1 1/1 Running 0 76m ``` 我們通過-w命令可以看到pod建立情況,StatefulSet所建立的pod編號都是從0開始累加,在 web-0 進入到 Running 狀態、並且細分狀態(Conditions)成為 Ready 之前,web-1 會一直處於 Pending 狀態。 然後我們使用exec檢視pod的hostname: ```shell $ kubectl exec web-0 -- sh -c 'hostname' web-0 $ kubectl exec web-1 -- sh -c 'hostname' web-1 ``` 然後我們可以啟動一個一次性的pod用 nslookup 命令,解析一下 Pod 對應的 Headless Service: ```shell $ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh $ nslookup web-0.nginx Server: 10.68.0.2 Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx Address 1: 172.20.0.56 web-0.nginx.default.svc.cluster.local $ nslookup web-1.nginx Server: 10.68.0.2 Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local Name: web-1.nginx Address 1: 172.20.0.57 web-1.nginx.default.svc.cluster.local ``` 如果我們刪除了這兩個pod,然後觀察pod情況: ```shell $ kubectl delete pod -l app=nginx $ kubectl get pod -w -l app=nginx web-0 1/1 Terminating 0 83m web-1 1/1 Terminating 0 83m web-0 0/1 Pending 0 0s web-1 0/1 Terminating 0 83m web-0 0/1 ContainerCreating 0 0s web-0 1/1 Running 0 1s web-1 0/1 Pending 0 0s web-1 0/1 ContainerCreating 0 0s web-1 1/1 Running 0 1s ``` 當我們把這兩個 Pod 刪除之後,Kubernetes 會按照原先編號的順序,創建出了兩個新的 Pod。並且,Kubernetes 依然為它們分配了與原來相同的“網路身份”:web-0.nginx 和 web-1.nginx。 但是網路結構雖然沒變,但是pod對應的ip是改變了的,我們再進入到pod進行DNS解析: ```shell $ nslookup web-0.nginx Server: 10.68.0.2 Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx Address 1: 172.20.0.59 web-0.nginx.default.svc.cluster.local $ nslookup web-1.nginx Server: 10.68.0.2 Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local Name: web-1.nginx Address 1: 172.20.0.60 web-1.nginx.default.svc.cluster.local ``` ### 儲存狀態 在講儲存狀態的時候,需要大家掌握上一節有關pv和pvc的知識才好往下繼續,建議大家看完再來看本節。 在上一節中,我們瞭解到Kubernetes 中 PVC 和 PV 的設計,實際上類似於“介面”和“實現”的思想。而 PVC、PV 的設計,也使得 StatefulSet 對儲存狀態的管理成為了可能。 比如我們宣告一個如下的StatefulSet: ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: serviceName: "nginx" replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.9.1 ports: - containerPort: 80 name: web volumeMounts: - name: local-volume-a mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: local-volume-a spec: accessModes: - ReadWriteMany storageClassName: "local-volume" resources: requests: storage: 512Mi selector: matchLabels: key: local-volume-a-0 ``` 在這個StatefulSet中添加了volumeClaimTemplates欄位,用來宣告對應的PVC的定義;也就是說這個PVC中使用的storageClass必須是local-volume,需要的儲存空間是512Mi,並且這個pvc對應的pv的標籤必須是key: local-volume-a-0。 然後我們準備一個PV: ```yaml apiVersion: v1 kind: PersistentVolume metadata: name: local-volume-pv-0 labels: key: local-volume-a-0 spec: capacity: storage: 0.5Gi volumeMode: Filesystem accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: local-volume local: path: /mnt/disks/vol1 nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/hostname operator: In values: - node1 ``` 我把這個PV建立在node1節點上,並且將本地磁碟掛載宣告為PV。 然後我們建立這個PV: ```shell $ kubectl apply -f local-pv-web-0.yaml $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE local-volume-pv-0 512Mi RWX Retain Available default/local-vo ``` 然後我們在建立這個StatefulSet的時候,會自動建立PVC: ```shell $ kubectl apply -f statefulset2.yaml $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE local-volume-a-web-0 Bound local-volume-pv-0 512Mi RWX local-volume 15m ``` 建立的PVC名字都是由:--< 編號 >構成,編號從0開始,並且我們可以看到上面的PV已經處於Bound**狀態**。 這個時候我們進入到Pod中,寫入一個檔案: ```shell $ kubectl exec -it web-0 -- /bin/bash $ echo helloword >/usr/share/nginx/html/index.html ``` 這樣就會在Pod 的 Volume 目錄裡寫入一個檔案,如果我們把這個Pod刪除,那麼在被刪除之後這個Pod還是會被創建出來,並且還會再和原來的PV:local-volume-pv-0繫結起來。 也就是說當StatefulSet 控制器發現一個名叫 web-0 的 Pod 消失了的時候,控制器就會重新建立一個新的、名字還是叫作 web-0 的 Pod 來,“糾正”這個不一致的情況。並且刪除Pod時並不會刪除這個 Pod 對應的 PVC 和 PV。需要注意的是,在這個新的 Pod 物件的定義裡,它宣告使用的 PVC 的名字,還是叫作local-volume-a-web-0。 通過這種方式,Kubernetes 的 StatefulSet 就實現了對應用儲存狀態的管理。 ### 更新策略 在 Kubernetes 1.7 及之後的版本中,可以為 StatefulSet 設定 `.spec.updateStrategy` 欄位。 #### OnDelete 如果 StatefulSet 的 `.spec.updateStrategy.type` 欄位被設定為 OnDelete,當您修改 `.spec.template` 的內容時,StatefulSet Controller 將不會自動更新其 Pod。您必須手工刪除 Pod,此時 StatefulSet Controller 在重新建立 Pod 時,使用修改過的 `.spec.template` 的內容建立新 Pod。 例如我們執行下面的語句更新上面例子中建立的web: ```shell $ kubectl set image statefulset web nginx=nginx:1.18.0 $ kubectl describe pod web-0 .... Containers: nginx: Container ID: docker://7e45cd509db74a96b4f6ca4d9f7424b3c4794f56e28bfc3fbf615525cd2ecadb Image: nginx:1.9.1 .... ``` 然後我們發現pod的nginx版本並沒有發生改變,需要我們手動刪除pod之後才能生效。 ```shell $ kubectl delete pod web-0 pod "web-0" deleted $ kubectl describe pod web-0 ... Containers: nginx: Container ID: docker://0f58b112601a39f3186480aa97e72767b05fdfa6f9ca02182d3fb3b75c159ec0 Image: nginx:1.18.0 ... ``` #### Rolling Updates `.spec.updateStrategy.type` 欄位的預設值是 RollingUpdate,該策略為 StatefulSet 實現了 Pod 的自動滾動更新。在更新完`.spec.tempalte` 欄位後StatefulSet Controller 將自動地刪除並重建 StatefulSet 中的每一個 Pod。 刪除和重建的順序也是有講究的: * 刪除的時候從序號最大的開始刪,每刪除一個會更新一個。 * 只有更新完的pod已經是ready狀態了才往下繼續更新。 #### 為 RollingUpdate 進行分割槽 當為StatefulSet 的 `RollingUpdate` 欄位的指定 `partition` 欄位的時候,則所有序號大於或等於 `partition` 值的 Pod 都會更新。序號小於 `partition` 值的所有 Pod 都不會更新,即使它們被刪除,在重新建立時也會使用以前的版本。 如果 `partition` 值大於其 `replicas` 數,則更新不會傳播到其 Pod。這樣可以實現金絲雀釋出Canary Deploy或者灰度釋出。 如下,因為我們的web是2個pod組成,所以可以將`partition`設定為1: ```shell $ kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":1}}}}' ``` 在這裡,我使用了 kubectl patch 命令。它的意思是,以“補丁”的方式(JSON 格式的)修改一個 API 物件的指定欄位。 下面我們執行更新: ```shell $ kubectl set image statefulset web nginx=nginx:1.19.1 statefulset.apps/web image updated ``` 並在另一個終端中watch pod的變化: ```shell $ kubectl get pods -l app=nginx -w NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 13m web-1 1/1 Running 0 93s web-1 0/1 Terminating 0 2m16s web-1 0/1 Pending 0 0s web-1 0/1 ContainerCreating 0 0s web-1 1/1 Running 0 16s ``` 可見上面只有一個web-1進行了版本的釋出。 ### 總結 StatefulSet把有狀態的應用抽象為兩種情況:拓撲狀態和儲存狀態。 拓撲狀態指的是應用的多個例項之間不是完全對等的關係,包含啟動的順序、建立之後的網路標識等必須保證。 儲存狀態指的是不同的例項綁定了不同的儲存,如Pod A在它的生命週期中讀取的資料必須是一致的,哪怕是重啟之後還是需要讀取到同一個儲存。 然後講解了一下StatefulSet釋出更新該如何做,`updateStrategy`策略以及通過`partition`如果實現金絲雀發