Kubernetes如何通過StatefulSet支援有狀態應用?
Kubernetes如何通過StatefulSet支援有狀態應用?
為什麼Deployment不能編排所有型別應用?
Deployment認為一個應用中所有的Pod是完全一樣的,所以他們之間沒有順序,也無所謂執行在哪臺宿主機上。需要的時候,Deployment就可以通過Pod模板建立新的Pod;不需要的時候,Deployment就可以"殺掉"任意一個Pod。
但是,在實際的場景中,並不是所有的應用都可以滿足這樣的要求。
尤其是分散式應用,它的多個例項之間,往往有依賴關係,比如:主從關係、主備關係 。還有就是資料儲存類應用,它的多個例項,往往都會在本地磁碟上儲存一份資料。而這些例項一旦被殺掉,即便重建出來,例項與資料之間的對應關係也已經丟失,從而導致應用失敗。
所以,這種例項之間有不對等關係,以及例項對外部資料有依賴關係的應用,就被稱為“有狀態 應用”(Stateful Application)。
得益於“控制器模式”的設計思想,Kubernetes 專案很早就在 Deployment 的基礎上,擴充套件 出了對“有狀態應用”的初步支援。這個編排功能,就是:StatefulSet。
StatefulSet 的設計其實非常容易理解。它把真實世界裡的應用狀態,抽象為了兩種情況:
- 拓撲狀態。這種情況意味著,應用的多個例項之間不是完全對等的關係。這些應用例項,必 須按照某些順序啟動,比如應用的主節點 A 要先於從節點 B 啟動。而如果你把 A 和 B 兩個 Pod 刪除掉,它們再次被創建出來時也必須嚴格按照這個順序才行。並且,新創建出來的 Pod,必須和原來 Pod 的網路標識一樣,這樣原先的訪問者才能使用同樣的方法,訪問到 這個新 Pod。
- 儲存狀態。這種情況意味著,應用的多個例項分別綁定了不同的儲存資料。對於這些應用實 例來說,Pod A 第一次讀取到的資料,和隔了十分鐘之後再次讀取到的資料,應該是同一 份,哪怕在此期間 Pod A 被重新建立過。這種情況最典型的例子,就是一個數據庫應用的 多個儲存例項。
所以,StatefulSet 的核心功能,就是通過某種方式記錄這些狀態,然後在 Pod 被重新建立時, 能夠為新 Pod 恢復這些狀態。
Headless Service
在開始講述 StatefulSet 的工作原理之前,我們必須先了解一個 Kubernetes 專案中非常實用的概念:Headless Service。
Service 是 Kubernetes 專案中用來將 一組 Pod 暴露給外界訪問的一種機制。比如,一個 Deployment 有 3 個 Pod,那麼我就可以定義一個 Service。然後,使用者只要能訪問到這個 Service,它就能訪問到某個具體的 Pod。
Service被訪問有兩種方式:
- 以Service的VIP(Virtual IP,虛擬IP) 方式。比如:當我訪問10.0.23.1這個Service的IP時,10.0.23.1其實就是一個VIP,它會把請求轉發到該Service所代理的某一個Pod上。
- 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 中同樣也支援兩種升級策略:onDelete
和 RollingUpdate
,同樣可以通過設定 .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 裡的資料。