1. 程式人生 > 其它 >Kubernetes如何通過StatefulSet支援有狀態應用?

Kubernetes如何通過StatefulSet支援有狀態應用?

Kubernetes如何通過StatefulSet支援有狀態應用?

為什麼Deployment不能編排所有型別應用?

Deployment認為一個應用中所有的Pod是完全一樣的,所以他們之間沒有順序,也無所謂執行在哪臺宿主機上。需要的時候,Deployment就可以通過Pod模板建立新的Pod;不需要的時候,Deployment就可以"殺掉"任意一個Pod。

但是,在實際的場景中,並不是所有的應用都可以滿足這樣的要求。

尤其是分散式應用,它的多個例項之間,往往有依賴關係,比如:主從關係、主備關係 。還有就是資料儲存類應用,它的多個例項,往往都會在本地磁碟上儲存一份資料。而這些例項一旦被殺掉,即便重建出來,例項與資料之間的對應關係也已經丟失,從而導致應用失敗。

所以,這種例項之間有不對等關係,以及例項對外部資料有依賴關係的應用,就被稱為“有狀態 應用”(Stateful Application)。

得益於“控制器模式”的設計思想,Kubernetes 專案很早就在 Deployment 的基礎上,擴充套件 出了對“有狀態應用”的初步支援。這個編排功能,就是:StatefulSet。

StatefulSet 的設計其實非常容易理解。它把真實世界裡的應用狀態,抽象為了兩種情況:

  1. 拓撲狀態。這種情況意味著,應用的多個例項之間不是完全對等的關係。這些應用例項,必 須按照某些順序啟動,比如應用的主節點 A 要先於從節點 B 啟動。而如果你把 A 和 B 兩個 Pod 刪除掉,它們再次被創建出來時也必須嚴格按照這個順序才行。並且,新創建出來的 Pod,必須和原來 Pod 的網路標識一樣,這樣原先的訪問者才能使用同樣的方法,訪問到 這個新 Pod。
  2. 儲存狀態。這種情況意味著,應用的多個例項分別綁定了不同的儲存資料。對於這些應用實 例來說,Pod A 第一次讀取到的資料,和隔了十分鐘之後再次讀取到的資料,應該是同一 份,哪怕在此期間 Pod A 被重新建立過。這種情況最典型的例子,就是一個數據庫應用的 多個儲存例項。

所以,StatefulSet 的核心功能,就是通過某種方式記錄這些狀態,然後在 Pod 被重新建立時, 能夠為新 Pod 恢復這些狀態。

Headless Service

在開始講述 StatefulSet 的工作原理之前,我們必須先了解一個 Kubernetes 專案中非常實用的概念:Headless Service。

Service 是 Kubernetes 專案中用來將 一組 Pod 暴露給外界訪問的一種機制。比如,一個 Deployment 有 3 個 Pod,那麼我就可以定義一個 Service。然後,使用者只要能訪問到這個 Service,它就能訪問到某個具體的 Pod。

Service被訪問有兩種方式:

  1. 以Service的VIP(Virtual IP,虛擬IP) 方式。比如:當我訪問10.0.23.1這個Service的IP時,10.0.23.1其實就是一個VIP,它會把請求轉發到該Service所代理的某一個Pod上。
  2. Headless Service, 這種情況下,訪問“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一個 Pod 的 IP 地址。

可以看到,這裡的區別在於,Headless Service 不需要分配一個 VIP,而是可以直接以 DNS 記錄 的方式解析出被代理 Pod 的 IP 地址。

那麼,這樣的設計又有什麼作用呢? 想要回答這個問題,我們需要從 Headless Service 的定義方式看起。 下面是一個標準的 Headless Service 對應的 YAML 檔案:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
    - port: 80
      name: web
  clusterIP: None
  selector:
    app: nginx

可以看到,所謂的 Headless Service,其實仍是一個標準 Service 的 YAML 檔案。只不過,它的 clusterIP 欄位的值是:None,即:這個 Service,沒有一個 VIP 作為“頭”。這也就是 Headless 的含義。所以,這個 Service 被建立後並不會被分配一個 VIP,而是會以 DNS 記錄 的方式暴露出它所代理的 Pod。

而它所代理的Pod,依然採用的是Label Selector機制選擇出來的,即所有攜帶了app=nginx標籤的Pod,都會被這個Service代理起來。

當我們按照這樣的方式建立了一個 Headless Service 之後,它所代理的所有 Pod 的 IP 地址,都會被繫結一個這樣格式的 DNS 記錄,如下所示:

<pod-name>.<svc-name>.<namespace>.svc.cluster.local

這個 DNS 記錄,正是 Kubernetes 專案為 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。

有了這個“可解析身份”,只要知道了一個 Pod 的名字,以及它對應的 Service 的名字,就可以非常確定地通過這條 DNS 記錄訪問到 Pod 的 IP 地址。

StatefulSet的拓撲狀態

那麼,StatefulSet又是如何使用DNS記錄來維持Pod的拓撲狀態呢?

我們來編寫一個StatefulSet的yaml檔案,如下所示:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17.1
          ports:
            - containerPort: 80
              name: web

這個yaml檔案,和我們deployment的唯一區別就是多了個serviceName=nginx欄位,這個欄位的作用,就是告訴StatefulSet控制器,在執行控制迴圈的時候,請使用nginx這個Headless Service來保證Pod的"可解析身份"。

所以當我們通過kubectl create建立上面這個Service和StatefulSet 之後,就會看到如下兩個物件:

# kubectl get service nginx
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    2m54s

# kubectl get statefulset web
NAME   READY   AGE
web    2/2     72s

我們通過檢視StatefulSet的Events來檢視Pod的建立過程:

# kubectl describe statefulset web

Events:
  Type    Reason            Age   From                    Message
  ----    ------            ----  ----                    -------
  Normal  SuccessfulCreate  66s   statefulset-controller  create Pod web-0 in StatefulSet web successful
  Normal  SuccessfulCreate  64s   statefulset-controller  create Pod web-1 in StatefulSet web successful

我們不難看到,StatefulSet 給它所管理的所有 Pod 的名字, 進行了編號,編號規則是:-。 而且這些編號都是從 0 開始累加,與 StatefulSet 的每個 Pod 例項一一對應,絕不重複。 更重要的是,這些 Pod 的建立,也是嚴格按照編號順序進行的。

當這兩個 Pod 都進入了 Running 狀態之後,我們就可以檢視到它們各自唯一的“網路身份”了。我們使用 kubectl exec 命令進入到容器中檢視它們的 hostname:

# kubectl exec web-0 -- sh -c 'hostname'
web-0
# kubectl exec web-1 -- sh -c 'hostname'
web-1

可以看到,這兩個 Pod 的 hostname 與 Pod 名字是一致的,都被分配了對應的編號。

接下來,我們再試著以 DNS 的方式,訪問一下這個 Headless Service:

# kubectl run -it --image busybox:1.28.3 test --restart=Never --rm /bin/sh

通過這條命令,我們啟動了一個一次性的 Pod,因為–-rm 意味著 Pod 退出後就會被刪除掉。然後,在這個 Pod 的容器裡面,我們嘗試用 nslookup 命令,解析一下 Pod 對應的 Headless Service:

/ # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 192.168.166.138 web-0.nginx.default.svc.cluster.local

/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 192.168.166.140 web-1.nginx.default.svc.cluster.local

從 nslookup 命令的輸出結果中,我們可以看到,在訪問 web-0.nginx 的時候,最後解析到的,正是 web-0 這個 Pod 的 IP 地址;而當訪問 web-1.nginx 的時候,解析到的則是 web-1 的 IP 地址。

這時候,我們在另外一個 Terminal 裡把這兩個“有狀態應用”的 Pod 刪掉:

# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

在當前 Terminal 裡 Watch 一下這兩個 Pod 的狀態變化,就會發現一個有趣的現象:

# kubectl get pod -w -l app=nginx
NAME    READY   STATUS        RESTARTS   AGE
web-0   0/1     Terminating   0          3m5s
web-1   0/1     Terminating   0          2m55s
web-0   0/1     Terminating   0          3m10s
web-0   0/1     Terminating   0          3m10s
web-0   0/1     Pending       0          0s
web-1   0/1     Terminating   0          3m
web-0   0/1     Pending       0          0s
web-1   0/1     Terminating   0          3m
web-0   0/1     ContainerCreating   0          1s
web-0   0/1     ContainerCreating   0          1s
web-0   1/1     Running             0          2s
web-1   0/1     Pending             0          0s
web-1   0/1     Pending             0          0s
web-1   0/1     ContainerCreating   0          0s
web-1   0/1     ContainerCreating   0          0s
web-1   1/1     Running             0          2s

可以看到,當我們把這兩個 Pod 刪除之後,Kubernetes 會按照原先編號的順序,創建出了兩個新的 Pod。並且,Kubernetes 依然為它們分配了與原來相同的“網路身份”:web-0.nginx 和 web-1.nginx。

通過這種嚴格的對應規則,StatefulSet 就保證了 Pod 網路標識的穩定性。 比如,如果 web-0 是一個需要先啟動的主節點,web-1 是一個後啟動的從節點,那麼只要這個 StatefulSet 不被刪除,你訪問 web-0.nginx 時始終都會落在主節點上,訪問 web-1.nginx 時,則始終都會落在從節點上,這個關係絕對不會發生任何變化。

所以,如果我們再用 nslookup 命令,檢視一下這個新 Pod 對應的 Headless Service :

/ # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 192.168.166.147 web-0.nginx.default.svc.cluster.local

/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 192.168.166.145 web-1.nginx.default.svc.cluster.local

我們可以看到,在這個 StatefulSet 中,這兩個新 Pod 的“網路標識”(比如:web-0.nginx 和 web-1.nginx),再次解析到了正確的 IP 地址(比如:web-0 Pod 的 IP 地址 192.168.166.147)。

通過這種方法,Kubernetes 就成功地將 Pod 的拓撲狀態(比如:哪個節點先啟動,哪個節點後啟動),按照 Pod 的“名字 + 編號”的方式固定了下來。此外,Kubernetes 還為每一個 Pod 提供了一個固定並且唯一的訪問入口,即:這個 Pod 對應的 DNS 記錄。 這些狀態,在 StatefulSet 的整個生命週期裡都會保持不變,絕不會因為對應 Pod 的刪除或者 重新建立而失效。

不過,相信你也已經注意到了,儘管 web-0.nginx 這條記錄本身不會變,但它解析到的 Pod 的 IP 地址,並不是固定的。這就意味著,對於“有狀態應用”例項的訪問,你必須使用 DNS 記錄 或者 hostname 的方式,而絕不應該直接訪問這些 Pod 的 IP 地址。

StatefulSet 這個控制器的主要作用之一,就是使用 Pod 模板建立 Pod 的時候, 對它們進行編號,並且按照編號順序逐一完成建立工作。而當 StatefulSet 的“控制迴圈”發現 Pod 的“實際狀態”與“期望狀態”不一致,需要新建或者刪除 Pod 進行“調諧”的時候,它會嚴格按照這些 Pod 編號的順序,逐一完成這些操作。

與此同時,通過 Headless Service 的方式,StatefulSet 為每個 Pod 建立了一個固定並且穩定的 DNS 記錄,來作為它的訪問入口。

StatefulSet的儲存狀態

在開始之前,我們先準備兩個1G儲存卷(PV):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /tmp/pv001

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv002
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /tmp/pv002

然後直接建立pv即可:

# kubectl create -f pv.yaml 
persistentvolume/pv001 created
persistentvolume/pv002 created

# kubectl get pv
NAME    CAPACITY  ACCESS MODES   RECLAIM POLICY  STATUS  CLAIM  STORAGECLASS   REASON  AGE
pv001   1Gi       RWO            Retain          Available          				   13s
pv002   1Gi       RWO            Retain          Available                              13s

可以看到成功建立了兩個 PV 物件,狀態是:Available

然後接下來宣告一個如下所示的StatefulSet資源清單:(nginx-sts.yaml)

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17.1
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: ww
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: ww
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

這次,我們為這個 StatefulSet 額外添加了一個 volumeClaimTemplates 欄位。從名字就可以看出來,它跟 Deployment 裡 Pod 模板(PodTemplate)的作用類似。也就是說,凡是被這個StatefulSet管理的 Pod,都會宣告一個對應的 PVC;而這個 PVC 的定義,就來自於 volumeClaimTemplates 這個模板欄位。

更重要的是,這個 PVC 的名字,會被分配一個與這個 Pod 完全一致的編號。 這個自動建立的 PVC,與 PV 繫結成功後,就會進入 Bound 狀態,這就意味著這個 Pod 可以掛載並使用這個 PV 了。

PVC 其實就是一種特殊的 Volume。只不過一個 PVC 具體是什麼型別的 Volume,要在跟某個 PV 繫結之後才知道。 當然,PVC 與 PV 的繫結得以實現的前提是,已經在系統裡建立好了符合條件的 PV(比如,我們在前面用到的 pv.yaml)

所以,我們在使用 kubectl create 建立了 StatefulSet 之後,就會看到 Kubernetes 叢集裡出 現了兩個 PVC:

# kubectl get pvc -l app=nginx
NAME       STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
ww-web-0   Bound    pv001    1Gi        RWO                           8m45s
ww-web-1   Bound    pv002    1Gi        RWO                           8m41s

可以看到,這些 PVC,都"<PVC名字>-<StatefulSet名字>-< 編號 >”的方式命名,並且處於 Bound 狀態。

由於我們這裡用volumeClaimTemplates宣告的模板是掛載點的方式,並不是 volume,所有實際上是把 PV 的儲存掛載到容器中,所以會覆蓋掉容器中的資料,在容器啟動完成後我們可以手動在 PV 的儲存裡面新建 index.html 檔案來保證容器的正常訪問,當然也可以進入到容器中去建立,這樣更加方便:

for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done

如上所示,通過 kubectl exec 指令,我們在每個 Pod 的 Volume 目錄裡,寫入了一個 index.html 檔案。這個檔案的內容,正是 Pod 的 hostname。比如,我們在 web-0 的 index.html 裡寫入的內容就是 "hello web-0"。

# for i in 0 1; do kubectl exec -it web-$i -- sh -c "cat /usr/share/nginx/html/index.html"; done
hello web-0
hello web-1

如果使用kubectl delete刪除這兩個pod,這些volume檔案會不會丟失呢?

# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

我們知道,上面刪除的兩個pod會被按照編號的迴圈重新建立,那麼我們寫入的index.html檔案是否還在?

# for i in 0 1; do kubectl exec -it web-$i -- sh -c "cat /usr/share/nginx/html/index.html"; done
hello web-0
hello web-1

這個請求依然會返回:hello web-0,hello web-1。也就是說,原先與名叫 web-0 的 Pod 繫結的 PV,在這個 Pod 被重新建立之後,依然同新的名叫 web-0 的 Pod 繫結在了一起。對於 Pod web-1 來說,也是完全一樣的情況。

這是怎麼做到的呢?

首先,當把一個 Pod,比如 web-0,刪除之後,這個 Pod 對應的 PVC 和 PV,並不會被刪除,而這個 Volume 裡已經寫入的資料,也依然會儲存在遠端儲存服務裡。

此時,StatefulSet 控制器發現,一個名叫 web-0 的 Pod 消失了。所以,控制器就會重新建立 一個新的、名字還是叫作 web-0 的 Pod 來,“糾正”這個不一致的情況。 需要注意的是,在這個新的 Pod 物件的定義裡,它宣告使用的 PVC 的名字,還是叫作:ww-web-0。

這個 PVC 的定義,還是來自於 PVC 模板(volumeClaimTemplates),這是 StatefulSet 建立 Pod 的標準流程。

所以,在這個新的 web-0 Pod 被創建出來之後,Kubernetes 為它查詢名叫 ww-web-0 的 PVC 時,就會直接找到舊 Pod 遺留下來的同名的 PVC,進而找到跟這個 PVC 繫結在一起的 PV。 這樣,新的 Pod 就可以掛載到舊 Pod 對應的那個 Volume,並且獲取到儲存在 Volume 裡的資料。

通過這種方式,Kubernetes 的 StatefulSet 就實現了對應用儲存狀態的管理。

更新策略

在 StatefulSet 中同樣也支援兩種升級策略:onDeleteRollingUpdate,同樣可以通過設定 .spec.updateStrategy.type 進行指定。

  • OnDelete: 該策略表示當更新了 StatefulSet 的模板後,只有手動刪除舊的 Pod 才會建立新的 Pod。
  • RollingUpdate:該策略表示當更新 StatefulSet 模板後會自動刪除舊的 Pod 並建立新的Pod,如果更新發生了錯誤,這次“滾動更新”就會停止。不過需要注意 StatefulSet 的 Pod 在部署時是順序從 0~n 的,而在滾動更新時,這些 Pod 則是按逆序的方式即 n~0 依次刪除並建立。

另外SatefulSet 的滾動升級還支援 Partitions特性,通過.spec.updateStrategy.rollingUpdate.partition 進行設定,在設定 partition 後,SatefulSet 的 Pod 中序號大於或等於 partition 的 Pod 會在 StatefulSet 的模板更新後進行滾動升級,而其餘的 Pod 保持不變。

......
updateStrategy:
  rollingUpdate: # 如果更新的策略是OnDelete,那麼rollingUpdate就失效
    partition: 2 # 表示從第2個分割槽開始更新,預設是0
  type: RollingUpdate /OnDelete # 滾動更新/刪除之後更新

總結

1、 StatefulSet 的控制器直接管理的是 Pod。這是因為,StatefulSet 裡的不同 Pod 例項, 不再像 ReplicaSet 中那樣都是完全一樣的,而是有了細微區別的。

比如,每個 Pod 的 hostname、名字等都是不同的、攜帶了編號的。而 StatefulSet 區分這些例項的方式,就是通過在 Pod 的名字裡加上事先約定好的編號。

2、Kubernetes 通過 Headless Service,為這些有編號的 Pod在 DNS 伺服器中生成帶有同樣編號的 DNS 記錄。只要 StatefulSet 能夠保證這些 Pod 名字裡的編號不變。

那麼 Service 裡類似於web-0.nginx.default.svc.cluster.local這樣的 DNS 記錄也就不會變,而這條記錄解 析出來的 Pod 的 IP 地址,則會隨著後端 Pod 的刪除和再建立而自動更新。

這當然是 Service 機制本身的能力,不需要 StatefulSet 操心。

3、StatefulSet 還為每一個 Pod 分配並建立一個同樣編號的 PVC。這樣,Kubernetes 就可以通過 Persistent Volume 機制為這個 PVC 繫結上對應的 PV,從而保證了每一個 Pod 都擁有 一個獨立的 Volume。

在這種情況下,即使 Pod 被刪除,它所對應的 PVC 和 PV 依然會保留下來。所以當這個 Pod 被重新創建出來之後,Kubernetes 會為它找到同樣編號的 PVC,掛載這個 PVC 對應的 Volume,從而獲取到以前儲存在 Volume 裡的資料。