Kubernetes中有状态应用的优雅缩容

11次阅读

共计 3867 个字符,预计需要花费 10 分钟才能阅读完成。

将有状态的应用程序部署到 Kubernetes 是棘手的。StatefulSet 使它变得容易得多,但是它们仍然不能解决所有问题。最大的挑战之一是如何缩小 StatefulSet 而不将数据留在断开连接的 PersistentVolume 成为孤立对象上。在这篇博客中,我将描述该问题和两种可能的解决方案。

通过 StatefulSet 创建的每个 Pod 都有自己的 PersistentVolumeClaim(PVC)和 PersistentVolume(PV)。当按一个副本按比例缩小 StatefulSet 的大小时,其 Pod 之一将终止,但关联的 PersistentVolumeClaim 和绑定到其的 PersistentVolume 保持不变。在随后扩大规模时,它们会重新连接到 Pod。

Scaling a StatefulSet

现在,想象一下使用 StatefulSet 部署一个有状态的应用程序,其数据在其 pod 中进行分区。每个实例仅保存和处理一部分数据。当您缩小有状态应用的规模时,其中一个实例将终止,其数据应重新分配到其余的 Pod。如果您不重新分配数据,则在再次进行扩展之前,它仍然不可访问。

Redistributing data on scale-down

在正常关机期间重新分发数据

您可能会想:“既然 Kubernetes 支持 Pod 正常关闭的机制,那么 Pod 是否可以在关闭过程中简单地将其数据重新分配给其他实例呢?”事实上,它不能。为什么不这样做有两个原因:

  • Pod(或更确切地说,其容器)可能会收到除缩容以外的其他原因的终止信号。容器中运行的应用程序不知道为什么终止该程序,因此不知道是否要清空数据。
  • 即使该应用程序可以区分是缩容还是由于其他原因而终止,它也需要保证即使经过数小时或数天也可以完成关闭程序。Kubernetes 不提供该保证。如果应用程序进程在关闭过程中死掉,它将不会重新启动,因此也就没有机会完全分发数据。

因此,相信在正常关闭期间 Pod 能够重新分发(或以其他方式处理其所有数据)并不是一个好主意,并且会导致系统非常脆弱。

使用 tear-down 容器?

如果您不是 Kubernetes 的新手,那么你很可能知道什么是初始化容器。它们在容器的主要容器之前运行,并且必须在主要容器启动之前全部完成。

如果我们有 tear-down 容器(类似于 init 容器),但是在 Pod 的主容器终止后又会运行,该怎么办?他们可以在我们的有状态 Pod 中执行数据重新分发吗?

假设 tear-down 容器能够确定 Pod 是否由于缩容而终止。并假设 Kubernetes(更具体地说是 Kubelet)将确保 tear-down 容器成功完成(通过在每次返回非零退出代码时重新启动它)。如果这两个假设都成立,我们将拥有一种机制,可确保有状态的容器始终能够按比例缩小规模重新分配其数据。

但是?

可悲的是,当 tear-down 容器本身发生瞬态错误,并且一次或多次重新启动容器最终使它成功完成时,像上述的 tear-down 容器机制将只处理那些情况。但是,在 tear-down 过程中托管 Pod 的集群节点死掉的那些不幸时刻又如何呢?显然,该过程无法完成,因此无法访问数据。

现在很明显,我们不应该在 Pod 关闭时执行数据重新分配。相反,我们应该创建一个新的 Pod(可能安排在一个完全不同的集群节点上)以执行重新分发过程。

这为我们带来了以下解决方案:

缩小 StatefulSet 时,必须创建一个新的容器并将其绑定到孤立的 PersistentVolumeClaim。我们称其为“_drain pod_”,因为它的工作是将数据重新分发到其他地方(或以其他方式处理)。Pod 必须有权访问孤立的数据,并且可以使用它做任何想做的事情。由于每个应用程序的重新分发程序差异很大,因此新的容器应该是完全可配置的 - 用户应该能够在 drain Pod 内运行他们想要的任何容器。

StatefulSet Drain Controller

由于 StatefulSet 控制器当前尚不提供此功能,因此我们可以实现一个额外的控制器,其唯一目的是处理 StatefulSet 缩容。我最近实现了这种控制器的概念验证。您可以在 GitHub 上找到源代码:

luksa/statefulset-scaledown-controller​github.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 过程时再次扩容 StatefulSet,则这将阻止 StatefulSet 控制器创建重复的容器并将其附加到同一 PVC。

如果 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 规范中用常规字段替换注释,因此它将具有模板,volumeClaimTemplatesrainePodTemplate,与使用注释相比,一切都变得更好了。

PS: 本文属于翻译,原文

正文完
 0