5.深入k8s:StatefulSet控制器
阿新 • • 發佈:2020-08-08
> 轉載請宣告出處哦~,本篇文章釋出於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`如果實現金絲雀發