乐趣区

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

将有状态的应用程序部署到 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: 本文属于翻译,原文

退出移动版