将有状态的应用程序部署到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
,与使用注释相比,一切都变得更好了。