这篇文章已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已不再准确。

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

Kubernetes StatefulSets 自 1.5 版本引入并在 1.9 版本中达到稳定以来,已被广泛用于运行有状态应用。它们提供稳定的 Pod 身份、每个 Pod 持久存储以及有序的平滑部署、伸缩和滚动更新。你可以将 StatefulSet 视为运行复杂有状态应用的基本构建块。随着 Kubernetes 的使用日益增长,需要 StatefulSets 的场景也越来越多。许多这些场景要求比当前支持的“一次一个 Pod”更新模式(在使用 StatefulSet 的 OrderedReady Pod 管理策略时)更快的滚动更新。

以下是一些示例:

  • 我正在使用 StatefulSet 来编排一个基于缓存的多实例应用,该应用的缓存很大。缓存开始时是冷的,需要相当长的时间才能使容器启动。可能还需要其他一些初始启动任务。对这个 StatefulSet 进行滚动更新会花费大量时间才能完全更新应用。如果 StatefulSet 支持一次更新多个 Pod,将显著加快更新速度。

  • 我的有状态应用由主副本和从副本(或一个写入者和多个读取者)组成。我有多个读取者或从副本,并且我的应用可以容忍多个 Pod 同时宕机。我希望一次更新该应用的多个 Pod,以便快速推出新更新,尤其是在我的应用实例数量很多的情况下。请注意,我的应用仍然需要每个 Pod 具有唯一身份。

为了支持这些场景,Kubernetes 1.24 包含了一个新的 Alpha 特性。在使用新特性之前,必须启用 MaxUnavailableStatefulSet feature flag。启用后,你可以指定一个新字段 maxUnavailable,它是 StatefulSet 的 spec 的一部分。例如:

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 来触发滚动更新。启动滚动更新后,我将观察 Pods 每次更新 2 个,因为当前的 maxUnavailable 值为 2。以下输出显示了一段时间内的信息,不完整。maxUnavailable 可以是绝对数字(例如,2)或所需 Pods 的百分比(例如,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 的 Pods 可能以自己的速度准备就绪。一旦 Pods 4 和 3 都准备就绪,Pods 2 和 1 同时开始终止。当 Pods 2 和 1 都运行并准备就绪时,Pod 0 开始终止。

在 Kubernetes 中,更新 StatefulSets 时遵循严格的 Pod 更新顺序。在这个示例中,更新从副本 4 开始,然后副本 3,然后副本 2,依此类推,一次一个 Pod。一次一个 Pod 时,3 不可能在 4 之前就已运行并准备就绪。当 maxUnavailable 大于 1(在示例场景中我将 maxUnavailable 设置为 2)时,副本 3 可能在副本 4 准备就绪之前就已准备并运行——这是允许的。如果你是开发者,并将 maxUnavailable 设置为大于 1,你应该知道这种情况是可能发生的,并且必须确保你的应用能够处理由此可能引起的排序问题(如果存在)。当你将 maxUnavailable 设置为大于 1 时,在每个批次更新的 Pod 之间保证顺序。这个保证意味着第二批次更新的 Pods(副本 2 和 1)不能开始更新,直到第一批次(副本 4 和 3)的 Pods 准备就绪。

尽管 Kubernetes 将这些称为*副本*,但你的有状态应用可能有不同的视图,并且 StatefulSet 的每个 Pod 可能持有完全不同的数据。这里的重点是,StatefulSet 的更新是分批进行的,你现在可以拥有大于 1 的批次大小(作为 Alpha 特性)。

另请注意,上述行为是在 podManagementPolicy: OrderedReady 下的。如果你将 StatefulSet 定义为 podManagementPolicy: Parallel,不仅 maxUnavailable 数量的副本会同时终止;maxUnavailable 数量的副本也会同时进入 ContainerCreating 阶段。这称为突发 (bursting)。

那么,你现在可能有很多疑问,例如:-

  • podManagementPolicy 设置为 Parallel 时的行为是什么?
  • partition 设置为非 0 值时的行为是什么?

最好亲手尝试一下。这是一个 Alpha 特性,Kubernetes 贡献者正在寻求关于此特性的反馈。这是否帮助你实现了你的有状态应用场景?你是否发现了 Bug?你是否认为已实现的行为不直观,可能会破坏应用或让你感到意外?请提交 issue 让我们知道。

延伸阅读和后续步骤