將有狀態的應用程序部署到Kubernetes是棘手的。 StatefulSet使它變得容易得多,可是它們仍然不能解決全部問題。最大的挑戰之一是如何縮小StatefulSet而不將數據留在斷開鏈接的PersistentVolume成爲孤立對象上。在這篇博客中,我將描述該問題和兩種可能的解決方案。git
經過StatefulSet建立的每一個Pod都有本身的PersistentVolumeClaim(PVC)和PersistentVolume(PV)。當按一個副本按比例縮小StatefulSet的大小時,其Pod之一將終止,但關聯的PersistentVolumeClaim和綁定到其的PersistentVolume保持不變。在隨後擴大規模時,它們會從新鏈接到Pod。github
Scaling a StatefulSetapi
如今,想象一下使用StatefulSet部署一個有狀態的應用程序,其數據在其pod中進行分區。每一個實例僅保存和處理一部分數據。當您縮小有狀態應用的規模時,其中一個實例將終止,其數據應從新分配到其他的Pod。若是您不從新分配數據,則在再次進行擴展以前,它仍然不可訪問。app
Redistributing data on scale-downide
您可能會想:「既然Kubernetes支持Pod正常關閉的機制,那麼Pod是否能夠在關閉過程當中簡單地將其數據從新分配給其餘實例呢?」事實上,它不能。爲何不這樣作有兩個緣由:post
所以,相信在正常關閉期間Pod可以從新分發(或以其餘方式處理其全部數據)並非一個好主意,而且會致使系統很是脆弱。測試
若是您不是Kubernetes的新手,那麼你極可能知道什麼是初始化容器。它們在容器的主要容器以前運行,而且必須在主要容器啓動以前所有完成。code
若是咱們有tear-down容器(相似於init容器),可是在Pod的主容器終止後又會運行,該怎麼辦?他們能夠在咱們的有狀態Pod中執行數據從新分發嗎?對象
假設tear-down容器可以肯定Pod是否因爲縮容而終止。並假設Kubernetes(更具體地說是Kubelet)將確保tear-down容器成功完成(經過在每次返回非零退出代碼時從新啓動它)。若是這兩個假設都成立,咱們將擁有一種機制,可確保有狀態的容器始終可以按比例縮小規模從新分配其數據。blog
可是?
可悲的是,當tear-down容器自己發生瞬態錯誤,而且一次或屢次從新啓動容器最終使它成功完成時,像上述的tear-down容器機制將只處理那些狀況。可是,在tear-down過程當中託管Pod的集羣節點死掉的那些不幸時刻又如何呢?顯然,該過程沒法完成,所以沒法訪問數據。
如今很明顯,咱們不該該在Pod關閉時執行數據從新分配。相反,咱們應該建立一個新的Pod(可能安排在一個徹底不一樣的集羣節點上)以執行從新分發過程。
這爲咱們帶來了如下解決方案:
縮小StatefulSet時,必須建立一個新的容器並將其綁定到孤立的PersistentVolumeClaim。咱們稱其爲「drain pod」,由於它的工做是將數據從新分發到其餘地方(或以其餘方式處理)。Pod必須有權訪問孤立的數據,而且可使用它作任何想作的事情。因爲每一個應用程序的從新分發程序差別很大,所以新的容器應該是徹底可配置的-用戶應該可以在drain Pod內運行他們想要的任何容器。
因爲StatefulSet控制器當前尚不提供此功能,所以咱們能夠實現一個額外的控制器,其惟一目的是處理StatefulSet縮容。我最近實現了這種控制器的概念驗證。您能夠在GitHub上找到源代碼:
luksa/statefulset-scaledown-controllergithub.com
下面咱們解釋一下它是如何工做的。
在將控制器部署到Kubernetes集羣后,您只需在StatefulSet清單中添加註釋,便可將drain容器模板添加到任何StatefulSet中。這是一個例子:
apiVersion: apps/v1 kind: StatefulSet metadata: name: datastore annotations: statefulsets.kubernetes.io/drainer-pod-template: | { "metadata": { "labels": { "app": "datastore-drainer" } }, "spec": { "containers": [ { "name": "drainer", "image": "my-drain-container", "volumeMounts": [ { "name": "data", "mountPath": "/var/data" } ] } ] } } spec: ...
該模板與StatefulSet中的主要Pod模板沒有太大區別,只不過它是經過註釋定義的。您能夠像日常同樣部署和擴展StatefulSet。
當控制器檢測到按比例縮小了StatefulSet時,它將根據指定的模板建立新的drain容器,並確保將其綁定到PersistentVolumeClaim,該PersistentVolumeClaim先前已綁定至因按比例縮小而刪除的有狀態容器。
Drain容器得到與已刪除的有狀態容器相同的身份(即名稱和主機名)。這樣作有兩個緣由:
若是drain pod或其主機節點崩潰,則drain pod將從新安排到另外一個節點上,在該節點上能夠重試/恢復其操做。Drain pod完成後, Pod和PVC將被刪除。備份StatefulSet時,將建立一個新的PVC。
首先部署drain控制器:
$ kubectl apply -f https://raw.githubusercontent.com/luksa/statefulset-drain-controller/master/artifacts/cluster-scoped.yaml
接着部署示例StatefulSet:
$ kubectl apply -f https://raw.githubusercontent.com/luksa/statefulset-drain-controller/master/example/statefulset.yaml
這將運行三個有狀態的Pod。將StatefulSet縮小爲兩個時,您會看到其中一個Pod開始終止。而後,刪除Pod後,drain控制器將當即建立一個具備相同名稱的新drain Pod:
$ kubectl scale statefulset datastore --replicas 2 statefulset.apps/datastore scaled $ kubectl get po NAME READY STATUS RESTARTS AGE datastore-0 1/1 Running 0 3m datastore-1 1/1 Running 0 2m datastore-2 1/1 Terminating 0 49s $ kubectl get po NAME READY STATUS RESTARTS AGE datastore-0 1/1 Running 0 3m datastore-1 1/1 Running 0 3m datastore-2 1/1 Running 0 5s <-- the drain pod
當drain pod 完成其工做時,控制器將其刪除並刪除PVC:
$ kubectl get po NAME READY STATUS RESTARTS AGE datastore-0 1/1 Running 0 3m datastore-1 1/1 Running 0 3m $ kubectl get pvc NAME STATUS VOLUME CAPACITY ... data-datastore-0 Bound pvc-57224b8f-... 1Mi ... data-datastore-1 Bound pvc-5acaf078-... 1Mi ...
控制器的另外一個好處是它能夠釋放PersistentVolume,由於它再也不受PersistentVolumeClaim約束。若是您的集羣在雲環境中運行,則能夠下降存儲成本。
請記住,這僅是概念驗證。要成爲StatefulSet縮容問題的正確解決方案,須要進行大量工做和測試。理想狀況下,Kubernetes StatefulSet控制器自己將支持這樣的運行drain容器,而不是須要一個與原始控制器競爭的附加控制器(當您縮容並當即再次擴容時)。
經過將此功能直接集成到Kubernetes中,能夠在StatefulSet規範中用常規字段替換註釋,所以它將具備模板,volumeClaimTemplates
和rainePodTemplate
,與使用註釋相比,一切都變得更好了。