復制有狀態的Pod
replicaSet通過一個pod模版創建多個pod副本。這些副本除了它們的名字和IP地址不同外,沒有別的差異。如果pod模版里描述了一個關聯到特定持久卷聲明的數據卷,那么ReplicaSet的所有副本都將共享這個持久卷聲明,也就是綁定到同一個持久卷聲明。
因為是在pod模版里關聯持久卷聲明的,又會依據pod模版創建多個副本,則不能對每個副本都指定獨立的持久卷聲明。所以也不能通過一個ReplicaSet來運行一個每個實例都需要獨立存儲的分布式數據存儲服務,至少通過單個ReplicaSet是做不到的。老實說,之前你學習到的所有API對象都不能提供這樣的數據存儲服務,還需要一個其他的對象--StatefulSet
我們先看不使用StatefulSet的情況下有沒有方法實現多個副本有自己的持久卷聲明。
三種取巧的方法。
第一種方法,不使用ReplicaSet,使用Pod創建多個pod,每個pod都有獨立的持久卷聲明。需要手動創建它們,當有的pod消失后(節點故障),需要手動創建它們。因此不是一個好方法。
第二種方法,多個replicaSet ,每個rs只有一個pod副本。但這看起來很笨重,而且沒辦法擴縮容。
第三種方法,使用同一個ReplicaSet,大家也都掛載同一個持久卷聲明,應用內部做好互斥,創建多個data數據目錄,每一個pod用一個標記為在用,后面應用不能選被標記為在用的目錄。這樣做很難保證協調的一點沒問題,同時大家用同一個持久卷,讀寫io將成為整個應用的瓶頸。
除了上面的存儲需求,集群應用也會要求每一個實例擁有生命周期內唯一標識。pod可以隨時被刪掉,然后被新的pod替代。當一個ReplicaSet中的pod被替換時,盡管新的pod也可能使用被刪除pod數據卷中的數據,但它卻是擁有全新主機名和IP的嶄新pod.在一些應用中,當啟動的實例擁有完全新的網絡標識,但還使用舊實例的數據時,很可能引起問題,比如etcd存儲服務。
當然也可以創建多個service ,每一個replicaset對應一個service,那么一樣很笨重,且顯得很低級。辛運的是,Kubernetes為我們提供了這類需求的完美解決方案--StatefulSet.
了解StatefulSet
可以創建一個StatefulSet資源代替ReplicaSet來運行這類pod.它們是專門定制的一類應用,這類應用中每一個實例都是不可替代的個體,都擁有穩定的名字和狀態。
對比StatefulSet 與 ReplicaSet 或 ReplicationController
RS或RC管理的pod副本比較像牛,它們都是無狀態的,任何時候它們都可以被一個全新的pod替換。然后有狀態的pod需要不同的方法,當一個有狀態的pod掛掉后,這個pod實例需要在別的節點上重建,但是新的實例必須與被替換的實例擁有相同的名稱、網絡標識和狀態。這就是StatefulSet如何管理pod的。
StatefulSet 保證了pod在重新調度后保留它們的標識和狀態。它讓你方便地擴容、縮容。與RS類似,StatefulSet也會指定期望的副本數,它決定了在同一時間內運行的寵物數。也是依據pod模版創建的,與RS不同的是,StatefulSet 創建的pod副本並不是完全一樣的。每個pod都可以擁有一組獨立的數據卷(持久化狀態)。另外pod的名字都是規律的(固定的),而不是每個新pod都隨機獲取一個名字。
提供穩定的網絡標識
StatefulSet 創建的pod的名稱,按照從零開始的順序索引,這個會體現在pod的名稱和主機名稱上,同樣還會體現在pod對應的固定存儲上。
有狀態的pod與普通的pod不一樣的是,有狀態的pod有時候需要通過其主機名來定位,而無狀態的不需要,因為無狀態的都一樣,隨機選一個就行,但對於有狀態的來說,每一個pod都不一樣,通常希望操作的是特定的一個。基於這個原因,一個StatefulSet要求你創建一個用來記錄每個pod網絡標記的headless Service。通過這個Service,每個pod將擁有獨立的DNS記錄,這樣集群里它的伙伴或者客戶端就可以通過主機名找到它。比如說一個屬於default命名空間,名為foo的控制服務,它的一個pod名稱為A-0,那么完整域名為:a-0.foo.default.svc.cluster.local。而在ReplicaSet是行不通的。
此外我們可以在容器中通過dig foo.default.svc.cluster.local對應的SRV記錄,獲取一個StatefulSet中所有pod的名稱.
StatefulSet擴縮容的特點
擴容,會按照索引進行
縮容,也會按照索引,刪除索引值最大的 pod
縮容StatefulSet任何時候只會操作一個pod實例,所以會很慢,不是因為索引要順序進行,而是為了避免數據丟失。舉例來說,一個分布式存儲應用副本數為2,如果同時下線兩個,一份數據記錄就會丟失。
基於以上原因,StatefulSet在有實例不健康的情況下是不允許進行縮容操作的。一個不健康,你又縮容一個這樣相當於兩個同時下線。
持久卷的創建和刪除
擴容Statefulset增加一個副本,會創建兩個或更多的API對象(一個pod和一個與之關聯的持久卷聲明)。但對於縮容來將,只會刪除一個pod,而遺留下之前創建的聲明。因為當一個聲明被刪除后,與之綁定的持久卷就會被回收或刪除,其上面的數據就會丟失。基於這個原因,你需要釋放特定的持久卷時,需要手動刪除對應的持久卷聲明。
StatefulSet的保障機制。
一個有狀態的pod總會被一個完全一致的pod替換(兩者相同的名稱,主機名和存儲等)。這個替換發生在kubernetes發現舊pod不存在時(例如手動刪除這個pod).
那么當Kubernetes不能確定一個pod的狀態呢?如果它創建一個完全一致的pod,那系統中就會有兩個完全一致的pod在同時運行。這兩個pod會綁定到相同的存儲,所以這兩個相同標記的進程會同時寫相同的文件。
為了保證兩個擁有相同標記和綁定相同持久卷聲明的有狀態的pod實例不會同時運行,statefulset遵循at-most-one語義。也就是說一個StatefulSet必須在准確確認一個pod不再運行后,才會去創建它的替換pod。這對如何處理節點故障有很大幫助。具體實現,內部的,暫不深入。
講了那么多StatefulSet實現有狀態pod的好處,下面看看如何創建。
我們假設使用gec創建三個pv
kind: list
apiVersion: v1
item:
- apiVersion: v1
kind: PersistenVolume
metadata:
name: pv-a
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistenVolumeReclaimPolicy: Recycle 卷被聲明釋放后,空間會被回收再利用
gcePersistentDisk:
poName: pv-a
fsType: nfs4
- apiVersion: v1
kind: PersistenVolume
metadata:
name: pv-b
...
准備好pv后,我們接下來創建statefulset
如我們之前將到的,在部署一個StatefulSet之前,需要創建一個用於在有狀態的pod之間提供網絡標識的headless Service
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None (StatefulSet的控制Service必須時None即headless模式)
selector:
app: kubia
ports:
- name: http
port: 80
創建StatefulSet詳單
apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: kubia spec: serviceName: kubia replicas: 2 template: metadta: labels: app: kubia spec: containers: - name: kubia image: luksa/kubia-pet ports: - name: kubia containerPort: 8080
volumeMounts:
- name: data
mountPath: /var/data volumeClaimTemplates: - metadata: name: data spec: resources: requests: storage: 1Mi accessModes: - ReadWriteOnce
創建:
kubectl create -f kubia-statefulset.yaml
列出pod:
kubectl get pod
Name READY
kubia-0 0/1 ...
看到會一個個進行
kubectl get pod
Name READY
kubia-0 1/1 ...
kubia-1 0/1 ...
查看pvc
kubectl get pvc
Name STATUS VOlUME
data-kubia-0 Bound pv - c ...
data-kubia-1 Bound pv - a ...
可以看到生成的持久卷聲明的名稱由 volumeClaimTeplate 字段中定義的名稱和每個pod的名稱組成。
現在你的數據存儲集群節點都已經運行,可以開始使用它們了。因為之前創建的Service處於headless模式,所以不能通過service來訪問你的pod。需要直接連接每個單獨的pod來訪問(或者創建一個普通的Service,但是這樣還是不允許你訪問指定的pod)
我們來創建一個普通的service如下:
apiVersion: v1
kind: service
metadata:
name: kubia-public
spec:
selector:
app: kubia
ports:
- port: 80
targetPort: 8080
StatefulSet 已經運行起來了,那么我們看下如何更新它的pod模版,讓它使用新的鏡像。同時你也會修改副本數為3.通常會使用kubectl edit命令來更新StatefulSet
kubectl edit statefulset kubia
你會看到新的pod實例會使用新的鏡像運行,那已經存在的兩個副本呢?通過他們的壽命可以看出它們沒有更新。這是符合預期的。因為,首先StatefulSet更像ReplicaSet,而不是Deployment,所以在模版被修改后,它們不會重啟更新,需要手動刪除這些副本,然后StatefulSet會根據新的模版重新調度啟動它們。
kubectl delete po kubia-0 kubia-1
注意: 從Kubernetes1.7版本開始,statefulSet支持與Deployment和DaemonSet一樣的滾動升級。通過kubectl explain 獲取StatefulSet的spec.updateStrategy 相關文檔來獲取更多信息。
前面我們提到StatefulSet的保障機制,那么當一個節點故障了,會出現什么情況。
statefulset在明確知道一個pod不再運行之前,它不能或者不應當創建一個替換pod。只有當集群的管理者告訴它這些信息時候,它才能明確知道。為了做到這一點,管理者需要刪除這個pod,或者刪除整個節點。
當手動停止一個node的網卡,使用kubectl get node,會顯示Status notReady
因為控制台不會再收到該節點發送的狀態更新,該節點上嗎的所有pod狀態都會變為Unknown。
當一個pod狀態為Unknown時會發生什么
若該節點過段時間正常連接,並且重新匯報它上面的pod狀態,那這個pod就會重新被標記為Runing。但如果這個pod的未知狀態持續幾分鍾后(這個時間是可以配置的),這個pod就會自動從節點上驅逐。這是由主節點(kubernetes的控制組件)處理的。它通過刪除pod的資源來把它從節點上驅逐。
當kubelet發現這個pod標記為刪除狀態后,它開始終止運行該pod。在上面的示例中,kubelet已不能與主節點通信(因為網卡斷了),這意味着這個pod會一直運行着。查看
kubectl describe po kubia-0
發現status一直為Terminating,原因是NodeLost,在信息中說明的是節點不回應導致不可達。
這時候你想要手動刪除pod
kubectl delete po kubia-0
執行完成后,你的想法是會再次運行一個kubia-0
但是kubectl get po會發現kubia-0 狀態為 Unknown 並且還是之前那個舊pod ,因為啟動時長沒變。
為什么會這樣?因為在刪除pod之前,這個pod已經被標記為刪除。這是因為控制組件已經刪除了它(把它從節點驅逐)。這時你用kubectl describe po kubia-0 查看狀態依然是Terminating。
這時候只能進行強制刪除
kubectl delete po kubia-0 --force --grace-period 0
你需要同時使用--force和 --grace-period 0兩個選項。然后kubectl 會對你做的事發出
警告信息。如果你再次列舉pod,就可以看到一個新的kubia-0 pod被創建出來。
警告: 除非你確認節點不再運行或者不會再可以訪問(永遠不會再可以訪問),否則不要強制刪除有狀態的pod.