這篇文章主要介紹StatefulSet控制器的設(shè)計原理和有狀態(tài)應(yīng)用的具體實踐趋艘。
一. StatefulSet的設(shè)計原理
首先我們先來了解下Kubernetes的一個概念:有狀態(tài)服務(wù)與無狀態(tài)服務(wù)。
無狀態(tài)服務(wù)(Stateless Service):該服務(wù)運行的實例不會在本地存儲需要持久化的數(shù)據(jù),并且多個實例對于同一個請求響應(yīng)的結(jié)果是完全一致的。這種方式適用于服務(wù)間相互沒有依賴關(guān)系套蒂,如Web應(yīng)用舒岸,在Deployment控制器停止掉其中的一個Pod不會對其他Pod造成影響。
有狀態(tài)服務(wù)(Stateful Service):服務(wù)運行的實例需要在本地存儲持久化數(shù)據(jù)塘幅,比如數(shù)據(jù)庫或者多個實例之間有依賴拓撲關(guān)系,比如:主從關(guān)系尿贫、主備關(guān)系电媳。如果停止掉依賴中的一個Pod,就會導(dǎo)致數(shù)據(jù)丟失或者集群崩潰庆亡。這種實例之間有不對等關(guān)系匾乓,以及實例對外部數(shù)據(jù)有依賴關(guān)系的應(yīng)用,就被稱為“有狀態(tài)應(yīng)用”(Stateful Application)又谋。
其中無狀態(tài)服務(wù)在我們前面文章中使用的Deployment編排對象已經(jīng)可以滿足拼缝,因為無狀態(tài)的應(yīng)用不需要很多要求,只要保持服務(wù)正常運行就可以彰亥,Deployment刪除掉任意中的Pod也不會影響服務(wù)的正常咧七,但面對相對復(fù)雜的應(yīng)用,比如有依賴關(guān)系或者需要存儲數(shù)據(jù)任斋,Deployment就無法滿足條件了继阻,Kubernetes項目也提供了另一個編排對象StatefulSet。
StatefulSet將有狀態(tài)應(yīng)用抽象為兩種情況:
拓撲狀態(tài)废酷。這種情況意味著穴翩,應(yīng)用的多個實例之間不是完全對等的關(guān)系。這些應(yīng)用實例锦积,必須按照某些順序啟動芒帕,比如應(yīng)用的主節(jié)點 A 要先于從節(jié)點 B 啟動。而如果你把 A 和 B 兩個 Pod 刪除掉丰介,它們再次被創(chuàng)建出來時也必須嚴(yán)格按照這個順序才行背蟆。并且鉴分,新創(chuàng)建出來的 Pod,必須和原來 Pod 的網(wǎng)絡(luò)標(biāo)識一樣带膀,這樣原先的訪問者才能使用同樣的方法志珍,訪問到這個新 Pod。
存儲狀態(tài)垛叨。這種情況意味著伦糯,應(yīng)用的多個實例分別綁定了不同的存儲數(shù)據(jù)。對于這些應(yīng)用實例來說嗽元,Pod A 第一次讀取到的數(shù)據(jù)敛纲,和隔了十分鐘之后再次讀取到的數(shù)據(jù),應(yīng)該是同一份剂癌,哪怕在此期間 Pod A 被重新創(chuàng)建過淤翔。這種情況最典型的例子,就是一個數(shù)據(jù)庫應(yīng)用的多個存儲實例佩谷。
StatefulSet 的核心功能旁壮,就是通過某種方式記錄這些狀態(tài),然后在 Pod 被重新創(chuàng)建時谐檀,能夠為新 Pod 恢復(fù)這些狀態(tài)抡谐。它包含Deployment控制器ReplicaSet的所有功能,增加可以處理Pod的啟動順序桐猬,為保留每個Pod的狀態(tài)設(shè)置唯一標(biāo)識童叠,同時具有以下功能:
- 穩(wěn)定的、唯一的網(wǎng)絡(luò)標(biāo)識符
- 穩(wěn)定的课幕、持久化的存儲
- 有序的、優(yōu)雅的部署和縮放
二. 有狀態(tài)服務(wù)的拓撲狀態(tài)
在上面我們提到有狀態(tài)應(yīng)用大致抽象為拓撲狀態(tài)和存儲狀態(tài)五垮,拓撲狀態(tài)是為應(yīng)用多實例中有相互依賴的服務(wù)而實現(xiàn)的乍惊。首先我們先來了解StatefulSet如何保證唯一的網(wǎng)絡(luò)標(biāo)識,這就需要涉及到Kubernetes 項目中非常實用的概念:Headless Service
放仗。
Service資源對象我們在k8s(一) 基本概念與組件原理已經(jīng)提及到它是k8s項目中用來將一組 Pod 暴露給外界訪問的一種機制润绎。Service也分為兩種方式分別是:
- 第一種方式,是以 Service 的 VIP(Virtual IP诞挨,即:虛擬 IP)方式莉撇。比如:當(dāng)我訪問 10.0.23.1 這個 Service 的 IP 地址時,10.0.23.1 其實就是一個 VIP惶傻,它會把請求轉(zhuǎn)發(fā)到該 Service 所代理的某一個 Pod 上棍郎,相當(dāng)于前面的負載均衡代理。
- 第二種方式银室,就是以 Service 的 DNS 方式涂佃。比如:這時候励翼,只要我訪問“my-svc.my-namespace.svc.cluster.local”這條 DNS 記錄,就可以訪問到名叫 my-svc 的 Service 所代理的某一個 Pod辜荠。
而第二種以DNS的方式又分為兩種處理方法:
- Normal Service:正常使用汽抚,訪問“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 這個 Service 的 VIP伯病,然后將請求通過VIP地址轉(zhuǎn)發(fā)到代理的Pod造烁。
- Headless Service:無頭服務(wù),訪問“my-svc.my-namespace.svc.cluster.local”解析到的午笛,直接就是 my-svc 代理的某一個 Pod 的 IP 地址惭蟋。可以看到季研,這里的區(qū)別在于敞葛,Headless Service 不需要分配一個 VIP,而是可以直接以 DNS 記錄的方式解析出被代理 Pod 的 IP 地址与涡。
定義Headless Service 資源文件
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
,這樣Headless Service 就不需要分配VIP地址驼卖,可以直接通過DNS的方式直接訪問到Pod的IP地址氨肌。創(chuàng)建Headless Service 后它所代理的所有 Pod 的 IP 地址,都會被綁定一個這樣格式的 DNS 記錄:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
這個 DNS 記錄酌畜,正是 Kubernetes 項目為 Pod 分配的唯一的“可解析身份”(Resolvable Identity)怎囚。有了這個“可解析身份”,只要你知道了一個 Pod 的名字桥胞,以及它對應(yīng)的 Service 的名字恳守,就可以通過這條 DNS 記錄訪問到 Pod 的 IP 地址。
定義StatefulSet資源文件
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.9.1
ports:
- containerPort: 80
name: web
上面這個YAML文件比我們之前部署的nginx-deployment增加了ServiceName:nginx
的字段定義贩虾,即:告訴 StatefulSet 控制器催烘,在執(zhí)行控制循環(huán)(Control Loop)的時候,請使用 nginx 這個 Headless Service 來保證 Pod 的“可解析身份”缎罢。
下面我們創(chuàng)建這些資源:
$ kubectl create -f svc.yaml
$ kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 10s
$ kubectl create -f statefulset.yaml
$ kubectl get statefulset web
NAME DESIRED CURRENT AGE
web 2 1 19s
$ kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 12m
web-1 1/1 Running 0 10m
可以看到伊群,Pod的名字后面都以數(shù)字順序為后綴,這個是因為StatefulSet 對它所管理 Pod 名字進行了編號策精,編號規(guī)則是:-
舰始,編號是從 0 開始累加,與 StatefulSet 的每個 Pod 實例一一對應(yīng)咽袜,不會重復(fù)丸卷。這些 Pod 的創(chuàng)建,也是嚴(yán)格按照編號順序進行的询刹。比如及老,在 web-0 進入到 Running 狀態(tài)抽莱、并且細分狀態(tài)(Conditions)成為 Ready 之前,web-1 會一直處于 Pending 狀態(tài)骄恶。
當(dāng)這兩個 Pod 都進入了 Running 狀態(tài)之后食铐,就可以查看到它們各自唯一的“網(wǎng)絡(luò)身份”了。我們使用kubectl exec
命令分別進入到容器中查看它們的 hostname僧鲁。
$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1
然后我們啟動一個一次性的pod來驗證是否可以通過DNS解析到Pod的IP地址
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 10.244.1.7
$ nslookup web-1.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 10.244.2.7
可以看到我們解析web-0.nginx
這個DNS已經(jīng)正確的返回了Pod的IP地址虐呻,現(xiàn)在我們再打開一個終端來刪除這兩個Pod,看看會發(fā)生什么樣的變化寞秃?
$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
查看Statefulset重新創(chuàng)建的Pod是否可以正常解析斟叼?
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 10.244.1.8
$ nslookup web-1.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 10.244.2.8
可以發(fā)現(xiàn)重新創(chuàng)建的pod也是按照原先的順序來創(chuàng)建的,雖然Pod的IP地址有變化春寿,但是用之前的DNS解析還是沒有變化的朗涩,保持了原先的唯一網(wǎng)絡(luò)標(biāo)識,StatefulSet 也就保證了 Pod 網(wǎng)絡(luò)標(biāo)識的穩(wěn)定性绑改。至此Kubernetes 就成功地將 Pod 的拓撲狀態(tài)(比如:哪個節(jié)點先啟動谢床,哪個節(jié)點后啟動),按照 Pod 的“名字 + 編號”的方式固定了下來厘线。
三. 有狀態(tài)服務(wù)的存儲狀態(tài)
下面我們來繼續(xù)探究StatefulSet對存儲狀態(tài)的管理機制识腿,在前面我們創(chuàng)建Pod需要使用存儲的時候,只需要在資源文件中添加spec.volumes
字段聲明使用volume就可以造壮,比如設(shè)置為hostpath或者emptyDir 渡讼。但實際環(huán)境中開發(fā)人員并不清楚我們那些Volume可以使用,所以存儲我們就需要使用Kubernetes的另一個資源對象PVC(Persistent Volume Claim)的功能耳璧。
- PVC 的全稱是:PersistentVolumeClaim(持久化卷聲明)成箫,PVC 是用戶存儲的一種聲明,PVC 和 Pod 比較類似旨枯,Pod 消耗的是節(jié)點蹬昌,PVC 消耗的是 PV 資源,Pod 可以請求 CPU 和內(nèi)存召廷,而 PVC 可以請求特定的存儲空間和訪問模式。對于真正使用存儲的用戶不需要關(guān)心底層的存儲實現(xiàn)細節(jié)账胧,只需要直接聲明使用 PVC 即可竞慢,不會暴露后端存儲的信息。
- PV 的全稱是:PersistentVolume(持久化卷)治泥,是對底層的共享存儲的一種抽象筹煮,PV 由運維人員進行創(chuàng)建和配置,它和具體的底層的共享存儲技術(shù)的實現(xiàn)方式有關(guān)居夹,比如 Ceph败潦、GlusterFS本冲、NFS 等,都是通過插件機制完成與共享存儲的對接劫扒。
PVC消耗的是PV資源檬洞,PV消耗的是后端共享存儲。當(dāng)我們創(chuàng)建一個PVC時沟饥,kubernetes 就會自動為PVC綁定一個符合條件的 PV添怔,這里我們PV我們后續(xù)會在持久化存儲文章中詳細講解,在這個例子中不做過多描述贤旷,這里我們已經(jīng)提前創(chuàng)建完成广料。
繼續(xù)以上面拓撲狀態(tài)的應(yīng)用為例,我們在資源文件中聲明使用PVC:
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.9.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce # Volume 的掛載方式是可讀寫幼驶,并且只能被掛載在一個節(jié)點上而非被多個節(jié)點共享艾杏。
resources:
requests:
storage: 1Gi #Volume 大小至少是 1 GiB。
我們?yōu)檫@個 StatefulSet 額外添加了一個volumeClaimTemplates
字段盅藻。它跟 Deployment 里 Pod 模板(PodTemplate)的作用類似购桑。也就是說,凡是被這個 StatefulSet 管理的 Pod萧求,都會聲明一個對應(yīng)的 PVC其兴;而這個 PVC 的定義,就來自于 volumeClaimTemplates
這個模板字段夸政。更重要的是元旬,這個 PVC 的名字,會被分配一個與這個 Pod 完全一致的編號守问。其中這個自動創(chuàng)建的 PVC匀归,與 PV 綁定成功后,就會進入 Bound 狀態(tài)耗帕,這就意味著這個 Pod 可以掛載并使用這個 PV 了穆端。
我們來創(chuàng)建這個資源對象:
$ kubectl create -f statefulset.yaml
$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s
www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
可以看到PVC的命名方式為<PVC名字>-<StatefulSet名字>-<Pod編號>
,PVC的狀態(tài)已經(jīng)顯示為Bound仿便,說明已經(jīng)綁定到符合條件的PV体啰,現(xiàn)在我們測試驗證下數(shù)據(jù)是否會丟失 ?
分別往Pod的Volume目錄中寫入內(nèi)容為hostname的index.html文件嗽仪,然后分別訪問pod的Nginx進程荒勇,查看返回信息是否正確
$ for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
$ for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done
hello web-0
hello web-1
然后我們手動刪除這兩個Pod
$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
被刪除之后,這兩個 Pod 會被按照編號的順序被重新創(chuàng)建出來闻坚。然后我們在新創(chuàng)建的容器里通過訪問“http://localhost”的方式去訪問 web-0 里的 Nginx 服務(wù):
# 在被重新創(chuàng)建出來的Pod容器里訪問http://localhost
$ kubectl exec -it web-0 -- curl localhost
hello web-0
可以發(fā)現(xiàn)請求依然會返回:“hello web-0”沽翔,也就是說,原先與:“web-0” 的 Pod 綁定的 PV,在這個 Pod 被重新創(chuàng)建之后仅偎,依然和新的“ web-0 ” Pod 綁定在了一起跨蟹。對于 Pod web-1 來說,也是完全一樣的情況橘沥。
這是因為我們刪除pod后窗轩,之前綁定的PV和PVC并不會刪除,數(shù)據(jù)仍然保存在后端的遠程存儲如Ceph中威恼,StatefulSet發(fā)現(xiàn)Pod消失后品姓,會自動創(chuàng)建一個新的Pod,名字編號也是和之前相同箫措,而這個新的Pod聲明使用的還是之前的PVC名字腹备,所以在這個新的 Pod 創(chuàng)建出來之后,Kubernetes 會為它查找之前綁定的PVC 斤蔓,就會直接找到舊 Pod 遺留下來的同名的 PVC植酥,進而找到跟這個 PVC 綁定在一起的 PV。這樣新的 Pod 就可以掛載到舊 Pod 對應(yīng)的那個 Volume弦牡,并且獲取到保存在 Volume 里的數(shù)據(jù)友驮。通過這種方式,Kubernetes 的 StatefulSet 就實現(xiàn)了對應(yīng)用存儲狀態(tài)的管理驾锰。
總結(jié):
- StatefulSet 的控制器直接管理的是 Pod卸留。通過在 Pod 的名字里加上事先約定好的編號,保證應(yīng)用拓撲狀態(tài)的服務(wù)穩(wěn)定椭豫。
- Kubernetes 通過 Headless Service耻瑟,為這些有編號的 Pod,在 DNS 服務(wù)器中生成帶有同樣編號的 DNS 記錄赏酥,生成唯一的網(wǎng)絡(luò)標(biāo)識喳整。
- StatefulSet 為每一個 Pod 分配并創(chuàng)建一個同樣編號的 PVC。保證了每一個 Pod 都擁有一個獨立的 Volume裸扶,保證數(shù)據(jù)不會丟失框都。
下篇文章:k8s五 | Pod的作業(yè)副本與滾動更新
系列文章:深入理解Kuerneters
參考資料:深入剖析Kubernetes-張磊
關(guān)注公眾號回復(fù)【k8s】關(guān)鍵詞獲取視頻教程及更多資料: