一、簡介
實例之間的不等關系以及實例對外數據有依賴關系的應用,就被稱為"有狀態應用"。
所謂實例之間的不等關系即對分布式應用來說,各實例,各應用之間往往有比較大的依賴關系,比如某個應用必須先於其他應用啟動,否則其他應用將不能啟動等。
對外數據有依賴關系的應用,最顯著的就是數據庫應用,對於數據庫應用,我們是需要持久化保存其數據的,如果是無狀態應用,在數據庫重啟數據和應用就失去了聯系,這顯然是違背我們的初衷,不能投入生產的。
所以,為了解決Kubernetes中有狀態應用的有效支持,Kubernetes使用StatefulSet來編排管理有狀態應用。
StatefulSet類似於ReplicaSet,不同之處在於它可以控制Pod的啟動順序,它為每個Pod設置唯一的標識。其具有一下功能:
- 穩定的,唯一的網絡標識符
- 穩定的,持久化存儲
- 有序的,優雅部署和縮放
- 有序的,自動滾動更新
StatefulSet的設計很容易理解,它把現實世界抽象為以下兩種情況:
(1)、拓撲狀態。這就意味着應用之間是不對等關系,應用要按某種順序啟動,即使應用重啟,也必須按其規定的順序重啟,並且重啟后其網絡標識必須和原來的一樣,這樣才能保證原訪問者能通過同樣的方法訪問新的Pod;
(2)、存儲狀態 。這就意味着應用綁定了存儲數據,不論什么時候,不論什么情況,對應用來說,只要存儲里的數據沒有變化,讀取到的數據應該是同一份;
所以StatefulSet的核心功能就是以某種方式記錄Pod的狀態,然后在Pod被重新創建時,通過某種方法恢復其狀態。
二、Headless Service
在介紹StatefulSet之前先了解一下什么是Headless Service。
我們知道,在Kubernetes中,Service是為一組Pod提供外部訪問的一種方式。通常,我們使用 Service訪問Pod有一下兩種方式:
(1)、通過Cluster IP,這個Clustre IP就相當於VIP,我們訪問這個IP,就會將請求轉發到后端Pod上;
(2)、通過DNS方式,通過這種方式首先得確保Kubernetes集群中有DNS服務。這個時候我們只要訪問"my-service.my-namespace.svc,cluster.local",就可以訪問到名為my-service的Service所代理的后端Pod;
而對於第二種方式,有下面兩種處理方法:
(1)、Normal Service,即解析域名,得到的是Cluster IP,然后再按照方式一訪問;
(2)、Headless Service,即解析域名,得到的是后端某個Pod的IP地址,這樣就可以直接訪問;
對於第二種處理方法我們可以看到這里並不需要Cluster IP,而是通過域名直接解析后端Pod的IP地址進行訪問。
下面即為一個簡單的Headless Service的YAML文件定義:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
通過上面的YAML文件可以看出和我們普通的Service定義沒有太大的區別,唯一的不同就是clusterIP設置為None,也就是不需要cluster,我們通過kubectl apply -f創建這個Service,然后查看這個Service的Cluster IP為None。
這樣創建的Service就是一個Headless Service,它沒有VIP,所以會以DNS記錄的方式暴露它所代理的Pod。
三、使用StatefulSet
在創建StatefulSet之前先創建PV,如下:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01
labels:
release: stable
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/data
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv02
labels:
release: stable
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/data
然后執行kubectl apply -f 啟動PV,如下:
[root@master statefulset]# kubectl apply -f pv.yaml
persistentvolume/pv01 created
persistentvolume/pv02 created
[root@master statefulset]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv01 1Gi RWO Recycle Available 10s
pv02 1Gi RWO Recycle Available 9s
可以看到兩個PV的狀態都為可用狀態,然后編寫StatefulSet的YAML文件:
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
role: stateful
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
role: stateful
spec:
containers:
- name: nginx
image: cnych/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
注意上面的 YAML 文件中和volumeMounts進行關聯的是一個新的屬性:volumeClaimTemplates,該屬性會自動聲明一個 pvc 對象和 pv 進行管理,而serviceName: "nginx"表示在執行控制循環的時候,用nginx這個Headless Service來保存Pod的可解析身份。
然后這里我們開啟兩個終端窗口。在第一個終端中,使用 kubectl get 來查看 StatefulSet 的 Pods 的創建情況。
$ kubectl get pods -w -l role=stateful
在另一個終端中,使用 kubectl create 來創建定義在 statefulset-demo.yaml 中的 Headless Service 和 StatefulSet。
$ kubectl create -f statefulset-demo.yaml
service "nginx" created
statefulset.apps "web" created
然后我們觀察Pod的啟動順序:
[root@master ~]# kubectl get pods -w -l role=stateful
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 3m12s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 1s
web-1 0/1 ContainerCreating 0 1s
web-1 1/1 Running 0 5s
通過 上面的創建過程,我們可以看出,StatefulSet給它所管理的Pod進行了編號,其命名規則為[statefulset-name]-[index],其index從0開始,與StatefulSet的每個Pod實例一一對應,絕不重復。更重要的是其Pod的創建過程是順序的,如上web-0進入running狀態后web-1才進入pending狀態。
我們通過命令來查看其創建結果:
[root@master statefulset]# kubectl get statefulset web
NAME READY AGE
web 2/2 17m
[root@master statefulset]# kubectl get pods -l role=stateful
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 17m
web-1 1/1 Running 0 14m
[root@master statefulset]# kubectl get svc nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 17m
當兩個Pod都進入running狀態后,就可以查看其各自的網絡身份了,我們通過kubectl exec來查看,如下:
[root@master statefulset]# kubectl exec web-0 -- sh -c 'hostname'
web-0
[root@master statefulset]# kubectl exec web-1 -- sh -c 'hostname'
web-1
可以看到這兩個pod的hostname和pod的名字是一致的,都被分配為對應的編號,接下來我們用DNS的方式來訪問Headless Service。
我們先用如下命令啟動一個Pod:
kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
然后在這個Pod里用nslookup解析一個Pod對應的Headless Service:
kubectl run -i --tty --image centos dns-test --restart=Never --rm /bin/sh
sh-4.2# yum install bind-utils -y
sh-4.2# nslookup web-0.nginx
Server: 10.68.0.2
Address: 10.68.0.2#53
Name: web-0.nginx.default.svc.cluster.local
Address: 172.20.2.63
sh-4.2# nslookup web-1.nginx
Server: 10.68.0.2
Address: 10.68.0.2#53
Name: web-1.nginx.default.svc.cluster.local
Address: 172.20.2.64
從nslookup的結果分析,在訪問web-0.nginx的時候解析的是web-0這個Pod的IP,另一個亦然。
這時候如果我們把兩個Pod刪掉,再觀察其重啟Pod的過程:
[root@master statefulset]# kubectl delete pod -l role=stateful
pod "web-0" deleted
pod "web-1" deleted
然后查看其重啟順序如下:
[root@master ~]# kubectl get pods -w -l role=stateful
NAME READY STATUS RESTARTS AGE
web-0 0/1 ContainerCreating 0 0s
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 1/1 Running 0 2s
我們可以看到,我們把Pod刪除后,其重啟順序還是按原先的編號重啟,並且其網絡標識和原來依然一樣。
通過這種嚴格的對應規則,StatefulSet就保證了Pod的網絡標識的穩定性,通過這個方法,就可以把Pod的拓撲狀態按照Pod的名字+編號的方式固定起來。此外,Kubernetes還為每一個Pod提供了一個固定並且唯一的訪問入口,即這個Pod的DNS記錄。
我們還可以查看一下PV和PVC的綁定情況:
[root@master statefulset]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv01 1Gi RWO Recycle Bound default/www-web-0 129m
pv02 1Gi RWO Recycle Bound default/www-web-1 129m
[root@master statefulset]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-web-0 Bound pv01 1Gi RWO 124m
www-web-1 Bound pv02 1Gi RWO 116m
由此,我們對StatefulSet梳理如下:
(1)、StatefulSet直接管理的是Pod。這是因為StatefulSet里的Pod實例不像ReplicaSet中的Pod實例完全一樣,它們是有細微的區別,比如每個Pod的名字、hostname等是不同的,而且StatefulSet區分這些實例的方式就是為Pod加上編號;
(2)、Kubernetes通過Headless Service為這個編號的Pod在DNS服務器中生成帶同樣編號的記錄。只要StatefulSet能保證這個Pod的編號不變,那么Service中類似於web-0.nginx.default.svc.cluster.local這樣的DNS記錄就不會變,而這條記錄所解析的Pod IP地址會隨着Pod的重新創建自動更新;
(3)、StatefulSet還可以為每個Pod分配並創建一個和Pod同樣編號的PVC。這樣Kubernetes就可以通過Persitent Volume機制為這個PVC綁定對應的PV,從而保證每一個Pod都擁有獨立的Volume。這種情況下即使Pod被刪除,它所對應的PVC和PV依然會保留下來,所以當這個Pod被重新創建出來過后,Kubernetes會為它找到同樣編號的PVC,掛載這個PVC對應的Volume,從而獲取到以前Volume以前的數據;
四、總結
StatefulSet這個控制器的主要作用之一,就是使用Pod模板創建Pod的時候,對它們進行編號,並且按照編號順序完成作業,當StatefulSet的控制循環發現Pod的實際狀態和期望狀態不一致的時候,也會按着順序對Pod進行操作。
當然 StatefulSet 還擁有其他特性,在實際的項目中,我們還是很少回去直接通過 StatefulSet 來部署我們的有狀態服務的,除非你自己能夠完全能夠 hold 住,對於一些特定的服務,我們可能會使用更加高級的 Operator 來部署,比如 etcd-operator、prometheus-operator 等等,這些應用都能夠很好的來管理有狀態的服務,而不是單純的使用一個 StatefulSet 來部署一個 Pod就行,因為對於有狀態的應用最重要的還是數據恢復、故障轉移等等。