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

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

Kubernetes StatefulSet 自 1.5 版本引入并在 1.9 版本稳定以来,已被广泛用于运行有状态应用。它们提供稳定的 Pod 标识、每个 Pod 的持久化存储以及有序的优雅部署、扩缩和滚动更新。你可以将 StatefulSet 视为运行复杂有状态应用的原子构建块。随着 Kubernetes 的使用越来越广泛,需要 StatefulSet 的场景也越来越多。其中许多场景需要比当前支持的“一次一个 Pod”更新更快的滚动更新,特别是在你为 StatefulSet 使用 OrderedReady Pod 管理策略时。

以下是一些例子

  • 我正在使用 StatefulSet 来编排一个多实例、基于缓存的应用,其中缓存的规模很大。缓存从冷启动开始,需要相当长的时间才能启动容器。可能还需要执行更多初始启动任务。对此 StatefulSet 进行滚动更新(RollingUpdate)会花费大量时间才能完全更新应用。如果 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。这与未启用新功能时的行为一致。

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

我可以通过将镜像更改为 registry.k8s.io/nginx-slim:0.9 来触发滚动更新。一旦我启动滚动更新,我可以观察到 Pod 每次更新 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

请注意,滚动更新一开始,序号最高的两个 Pod 4 和 3 会同时开始终止。序号为 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)的 Pods 就绪之前不能开始更新。

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

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

那么,现在你可能会有很多关于以下方面的问题:

  • 当你设置 podManagementPolicy: Parallel 时,行为是怎样的?
  • partition 设置为非 0 的值时,行为又是怎样的?

最好还是亲自尝试一下。这是一个 Alpha 功能,Kubernetes 的贡献者们正在寻求对此功能的反馈。这个功能是否帮助你实现了你的有状态场景?你是否发现了 bug,或者你认为当前的实现行为不直观,可能会破坏应用或让它们措手不及?请提交一个 issue 让我们知道。

进一步阅读和后续步骤