深入淺出 Kubernetes:StatefulSet 概念理解與實踐
一 背景知識及相關概念
StatefulSet 的設計其實非常容易理解。它把真實世界裏的應用狀態,抽象爲了兩種情況:
拓撲狀態。這種情況意味着,應用的多個實例之間不是完全對等的關係。這些應用實例,必須按照某些順序啓動,比如應用的主節點 A 要先於從節點 B 啓動。而如果你把 A 和 B 兩個 Pod 刪除掉,它們再次被創建出來時也必須嚴格按照這個順序才行。並且,新創建出來的 Pod,必須和原來 Pod 的網絡標識一樣,這樣原先的訪問者才能使用同樣的方法,訪問到這個新 Pod。
存儲狀態。這種情況意味着,應用的多個實例分別綁定了不同的存儲數據。對於這些應用實例來說,Pod A 第一次讀取到的數據,和隔了十分鐘之後再次讀取到的數據,應該是同一份,哪怕在此期間 Pod A 被重新創建過。這種情況最典型的例子,就是一個數據庫應用的多個存儲實例。
StatefulSet 的核心功能,就是通過某種方式記錄這些狀態,然後在 Pod 被重新創建時,能夠爲新 Pod 恢復這些狀態。
這個 Service 又是如何被訪問的呢?
第一種方式,是以 Service 的 VIP(Virtual IP,即:虛擬 IP)方式。比如:當我訪問 172.20.25.3 這個 Service 的 IP 地址時,172.20.25.3 其實就是一個 VIP,它會把請求轉發到該 Service 所代理的某一個 Pod 上。
第二種方式,就是以 Service 的 DNS 方式。比如:這時候,只要我訪問“my-svc.my-namespace.svc.cluster.local”這條 DNS 記錄,就可以訪問到名叫 my-svc 的 Service 所代理的某一個 Pod。
二 StatefulSet 的兩種結構
2.1 拓撲結構
讓我們來看一下以下例子:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- port: 80
name: web
clusterIP: None
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web-server-gysl
labels:
app: nginx
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
restartPolicy: Always
containers:
- name: web-server
image: nginx:1.16.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: web-port
這些 Pod 的創建,也是嚴格按照編號順序進行的。比如,在 web-server-gysl-0 進入到 Running 狀態、並且細分狀態(Conditions)成爲 Ready 之前,web-server-gysl-1 會一直處於 Pending 狀態。
使用以下命令測試:
kubectl run -i --tty --image toolkit:v1.0.0821 dns-test --restart=Never --rm /bin/bash
[root@dns-test /]# nslookup web-server-gysl-0.nginx
Server: 10.0.0.2
Address: 10.0.0.2#53
Name: web-server-gysl-0.nginx.default.svc.cluster.local
Address: 172.20.25.3
[root@dns-test /]# nslookup web-server-gysl-1.nginx
Server: 10.0.0.2
Address: 10.0.0.2#53
Name: web-server-gysl-1.nginx.default.svc.cluster.local
Address: 172.20.72.7
[root@dns-test /]# nslookup nginx
Server: 10.0.0.2
Address: 10.0.0.2#53
Name: nginx.default.svc.cluster.local
Address: 172.20.72.7
Name: nginx.default.svc.cluster.local
Address: 172.20.25.3
由於最近版本的 busybox 有坑,我自己製作了一個 DNS 測試工具,Dockerfile 如下:
FROM centos:7.6.1810
RUN yum -y install bind-utils
CMD ["/bin/bash","-c","while true;do sleep 60000;done"]
回到 Master 節點看一下:
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-server-gysl-0 1/1 Running 0 43m 172.20.25.3 172.31.2.12 <none> <none>
web-server-gysl-1 1/1 Running 0 42m 172.20.72.7 172.31.2.11 <none> <none>
$ kubectl get svc -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
nginx ClusterIP None <none> 80/TCP 43m app=nginx
當我們在集羣內部分別 ping 域名 web-server-gysl-0.nginx.default.svc.cluster.local 和 web-server-gysl-1.nginx.default.svc.cluster.local 時,正常返回了對應的 Pod IP, 在 ping 域名 nginx.default.svc.cluster.local 時,則隨機返回2個 Pod IP 中的一個。完全印證了上文所述內容。
在上述操作過程中,我隨機刪除了這些 Pod 中的某一個或幾個,稍後再次來查看的時候,新創建的 Pod 依然按照之前的編號進行了編排。
此外,我將 StatefulSet 的一個 Pod 所在的集羣內節點下線,再次查看 Pod 的情況,系統在其他節點上以原 Pod 的名稱迅速創建了新的 Pod。編號都是從 0 開始累加,與 StatefulSet 的每個 Pod 實例一一對應,絕不重複。
2.2 存儲結構
由於測試環境資源有限,原計劃使用 rook-ceph 來進行實驗的,無奈使用 NFS 來進行實驗。 Ceph 創建 PV 的相關 yaml 如下:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-gysl
labels:
type: local
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
rbd:
monitors:
- '172.31.2.11:6789'
- '172.31.2.12:6789'
pool: data
image: data
fsType: xfs
readOnly: true
user: admin
keyring: /etc/ceph/keyrin
NFS 實驗相關 yaml:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-gysl-0
labels:
environment: test
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
nfs:
path: /data-0
server: 172.31.2.10
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-gysl-1
labels:
environment: test
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
nfs:
path: /data-1
server: 172.31.2.10
---
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
name: nfs
provisioner: fuseim.pri/ifs
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: statefulset-pvc-gysl
spec:
replicas: 2
serviceName: "gysl-web"
selector:
matchLabels:
app: pod-gysl
template:
metadata:
name: web-pod
labels:
app: pod-gysl
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- name: web-port
containerPort: 80
volumeMounts:
- name: www-vct
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www-vct
annotations:
volume.beta.kubernetes.io/storage-class: "nfs"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: nfs
---
apiVersion: v1
kind: Service
metadata:
name: gysl-web
spec:
type: NodePort
selector:
app: pod-gysl
ports:
- name: web-svc
protocol: TCP
nodePort: 31688
port: 8080
targetPort: 80
通過以下命令向相關 Pod 寫入驗證內容:
for node in 0 1;do kubectl exec statefulset-pvc-gysl-$node -- sh -c "echo \<h1\>Node: ${node}\</h1\>>/usr/share/nginx/html/index.html";done
觀察實驗結果:
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
statefulset-pvc-gysl-0 1/1 Running 0 51m 172.20.85.2 172.31.2.11 <none> <none>
statefulset-pvc-gysl-1 1/1 Running 0 32m 172.20.65.4 172.31.2.12 <none> <none>
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-vct-statefulset-pvc-gysl-0 Bound pv-nfs-gysl-0 1Gi RWO nfs 51m
www-vct-statefulset-pvc-gysl-1 Bound pv-nfs-gysl-1 1Gi RWO nfs 49m
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs-gysl-0 1Gi RWO Recycle Bound default/www-vct-statefulset-pvc-gysl-0 nfs 51m
pv-nfs-gysl-1 1Gi RWO Recycle Bound default/www-vct-statefulset-pvc-gysl-1 nfs 51m
$ cat /data-0/index.html
<h1>Node: 0</h1>
$ cat /data-1/index.html
<h1>Node: 1</h1>
$ curl 172.31.2.11:31688
<h1>Node: 0</h1>
$ curl 172.31.2.11:31688
<h1>Node: 1</h1>
$ curl 172.31.2.12:31688
<h1>Node: 1</h1>
$ curl 172.31.2.12:31688
<h1>Node: 0</h1>
kubectl run -i --tty --image toolkit:v1.0.0821 test --restart=Never --rm /bin/bash
[root@test /]# curl statefulset-pvc-gysl-0.gysl-web
<h1>Node: 0</h1>
[root@test /]# curl statefulset-pvc-gysl-0.gysl-web
<h1>Node: 0</h1>
[root@test /]# curl statefulset-pvc-gysl-1.gysl-web
<h1>Node: 1</h1>
[root@test /]# curl statefulset-pvc-gysl-1.gysl-web
<h1>Node: 1</h1>
[root@test /]# curl gysl-web:8080
<h1>Node: 1</h1>
[root@test /]# curl gysl-web:8080
<h1>Node: 1</h1>
[root@test /]# curl gysl-web:8080
<h1>Node: 0</h1>
[root@test /]# curl gysl-web:8080
<h1>Node: 0</h1>
從實驗結果中我們可以看出 Pod 與 PV、PVC 的對應關係,結合上文中的 yaml 我們不難發現:
- Pod 與對應的 PV 存儲是一一對應的,在創 Pod 的同時, StatefulSet根據對應的規創建了相應的 PVC,PVC 選擇符合條件的 PV 進綁定。當 Pod 被刪除之後,數據依然保存在 PV 中,當被刪除的 Pod 再次被創建時, 該 Pod 依然會立即與原來的 Pod 進行綁定,保持原有的對應關係。
- 在集羣內部,可以通過 pod 名加對應的服務名訪問指定的 Pod 及其綁定的 PV。 如果通過服務名來訪問 StatefulSet ,那麼服務名的功能類似於 VIP 的功能。