本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面上的信息自发布以来是否已变得不正确。

Kubernetes 1.24:StatefulSet 的最大不可用副本数

Kubernetes StatefulSets 自 1.5 版本引入并在 1.9 版本稳定以来,已被广泛用于运行有状态应用程序。它们提供稳定的 Pod 身份、每个 Pod 持久存储以及有序的优雅部署、扩展和滚动更新。您可以将 StatefulSet 视为运行复杂有状态应用程序的原子构建块。随着 Kubernetes 的使用增长,需要 StatefulSets 的场景也越来越多。在许多这些场景中,如果为 StatefulSet 使用 OrderedReady Pod 管理策略,则需要比当前支持的每次更新一个 Pod 更快的滚动更新。

以下是一些示例:

  • 我正在使用 StatefulSet 来编排一个多实例、基于缓存的应用程序,其中缓存的大小很大。缓存启动时是冷的,并且需要相当长的时间才能启动容器。可能还需要更多的初始启动任务。在此 StatefulSet 上进行滚动更新将需要很长时间才能完全更新应用程序。如果 StatefulSet 支持一次更新多个 Pod,则更新速度会快得多。

  • 我的有状态应用程序由领导者和追随者或一个写入器和多个读取器组成。我有多个读取器或追随者,并且我的应用程序可以容忍多个 Pod 同时关闭。我希望一次更新此应用程序的多个 Pod,以便快速推出新更新,尤其是在我的应用程序实例数量很大时。请注意,我的应用程序仍然需要每个 Pod 唯一的身份。

为了支持此类场景,Kubernetes 1.24 包含一项新的 alpha 功能来提供帮助。在使用新功能之前,您必须启用 MaxUnavailableStatefulSet 功能标志。启用该功能后,您可以在 StatefulSet 的 spec 中指定一个名为 maxUnavailable 的新字段。例如:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
  namespace: default
spec:
  podManagementPolicy: OrderedReady  # you must set OrderedReady
  replicas: 5
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      # image changed since publication (previously used registry "k8s.gcr.io")
      - image: registry.k8s.io/nginx-slim:0.8
        imagePullPolicy: IfNotPresent
        name: nginx
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 2 # this is the new alpha field, whose default value is 1
      partition: 0
    type: RollingUpdate

如果您启用了新功能并且没有在 StatefulSet 中为 maxUnavailable 指定值,则 Kubernetes 将应用默认值 maxUnavailable: 1。这与您不启用新功能时看到的行为一致。

我将基于该示例清单运行一个场景,以演示此功能的工作原理。我将部署一个具有 5 个副本的 StatefulSet,并将 maxUnavailable 设置为 2,将 partition 设置为 0。

我可以通过将镜像更改为 registry.k8s.io/nginx-slim:0.9 来触发滚动更新。启动滚动更新后,我可以观察到 Pod 每次更新 2 个,因为 maxUnavailable 的当前值为 2。下面的输出显示了一段时间,但并非完整。maxUnavailable 可以是绝对数字(例如,2)或所需 Pod 的百分比(例如,10%)。绝对数字是通过将百分比向上舍入到最接近的整数来计算的。

kubectl get pods --watch 
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          85s
web-1   1/1     Running   0          2m6s
web-2   1/1     Running   0          106s
web-3   1/1     Running   0          2m47s
web-4   1/1     Running   0          2m27s
web-4   1/1     Terminating   0          5m43s ----> start terminating 4
web-3   1/1     Terminating   0          6m3s  ----> start terminating 3
web-3   0/1     Terminating   0          6m7s
web-3   0/1     Pending       0          0s
web-3   0/1     Pending       0          0s
web-4   0/1     Terminating   0          5m48s
web-4   0/1     Terminating   0          5m48s
web-3   0/1     ContainerCreating   0          2s
web-3   1/1     Running             0          2s
web-4   0/1     Pending             0          0s
web-4   0/1     Pending             0          0s
web-4   0/1     ContainerCreating   0          0s
web-4   1/1     Running             0          1s
web-2   1/1     Terminating         0          5m46s ----> start terminating 2 (only after both 4 and 3 are running)
web-1   1/1     Terminating         0          6m6s  ----> start terminating 1
web-2   0/1     Terminating         0          5m47s
web-1   0/1     Terminating         0          6m7s
web-1   0/1     Pending             0          0s
web-1   0/1     Pending             0          0s
web-1   0/1     ContainerCreating   0          1s
web-1   1/1     Running             0          2s
web-2   0/1     Pending             0          0s
web-2   0/1     Pending             0          0s
web-2   0/1     ContainerCreating   0          0s
web-2   1/1     Running             0          1s
web-0   1/1     Terminating         0          6m6s ----> start terminating 0 (only after 2 and 1 are running)
web-0   0/1     Terminating         0          6m7s
web-0   0/1     Pending             0          0s
web-0   0/1     Pending             0          0s
web-0   0/1     ContainerCreating   0          0s
web-0   1/1     Running             0          1s

请注意,滚动更新一旦开始,4 和 3(两个序数最高的 Pod)就同时开始终止。序号为 4 和 3 的 Pod 可能以它们自己的速度进入就绪状态。一旦 Pod 4 和 3 都就绪,Pod 2 和 1 就同时开始终止。当 Pod 2 和 1 都运行并就绪时,Pod 0 开始终止。

在 Kubernetes 中,对 StatefulSet 的更新在更新 Pod 时遵循严格的顺序。在此示例中,更新从副本 4 开始,然后是副本 3,然后是副本 2,依此类推,一次更新一个 Pod。当一次更新一个 Pod 时,3 不可能在 4 之前运行并就绪。当 maxUnavailable 大于 1 时(在示例场景中,我将 maxUnavailable 设置为 2),副本 3 可能在副本 4 就绪之前就运行并就绪了——这没问题。如果您是开发人员并且将 maxUnavailable 设置为大于 1,则您应该知道这种结果是可能发生的,并且您必须确保您的应用程序能够处理可能发生的任何此类排序问题。当您将 maxUnavailable 设置为大于 1 时,在每批更新的 Pod 之间保证排序。这种保证意味着,更新批次 2(副本 2 和 1)中的 Pod 在批次 0(副本 4 和 3)中的 Pod 就绪之前不能开始更新。

尽管 Kubernetes 将它们称为副本,但您的有状态应用程序可能有不同的视图,并且 StatefulSet 的每个 Pod 可能持有与其他 Pod 完全不同的数据。这里重要的是,对 StatefulSet 的更新是以批次进行的,而且您现在可以拥有大于 1 的批次大小(作为 alpha 功能)。

另请注意,上述行为是在 podManagementPolicy: OrderedReady 的情况下发生的。如果您将 StatefulSet 定义为 podManagementPolicy: Parallel,则不仅会同时终止 maxUnavailable 个副本,而且 maxUnavailable 个副本也会同时在 ContainerCreating 阶段启动。这称为突发。

因此,现在您可能对以下问题有很多疑问:

  • 当您设置 podManagementPolicy: Parallel 时会发生什么行为?
  • partition 的值不是 0 时会发生什么行为?

最好尝试自己看看。这是一个 alpha 功能,Kubernetes 贡献者正在寻求对此功能的反馈。这是否帮助您实现了您的有状态场景?您是否发现了错误,或者您是否认为已实现的这种行为不直观或者可能会破坏应用程序或让应用程序措手不及?请打开一个 issue,让我们知道。

进一步阅读和后续步骤