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

Kubernetes StatefulSets 和 DaemonSets 更新

本文讨论了 Kubernetes 中 DaemonSetStatefulSet API 对象的最新更新。我们将使用 Apache ZooKeeperApache Kafka StatefulSet 以及 Prometheus node exporter DaemonSet 来探讨这些功能。

在 Kubernetes 1.6 中,我们在 DaemonSet API 对象中添加了 RollingUpdate 更新策略。使用 RollingUpdate 策略配置 DaemonSet 后,当其 spec.template 更新时,DaemonSet 控制器会自动对 DaemonSet 中的 Pod 执行滚动更新。

在 Kubernetes 1.7 中,我们增强了 DaemonSet 控制器,使其能够跟踪 DaemonSet 的 PodTemplateSpecs 的修订历史。这使得 DaemonSet 控制器可以回滚更新。我们还将 RollingUpdate 策略添加到了 StatefulSet API 对象中,并为 StatefulSet 控制器实现了修订历史跟踪。此外,我们还添加了 Parallel Pod 管理策略,以支持需要具有唯一身份但不要求有序 Pod 创建和终止的有状态应用。

StatefulSet 滚动更新和 Pod 管理策略

首先,我们将通过部署一个 ZooKeeper 集群和一个 Kafka 集群来演示如何使用 StatefulSet 滚动更新和 Pod 管理策略。

先决条件

要跟着操作,你需要设置一个拥有至少 3 个可调度节点的 Kubernetes 1.7 集群。每个节点需要有 1 个 CPU 和 2 GiB 可用内存。你还需要一个动态供应器,以便 StatefulSet 控制器能够供应 6 个每个 10 GiB 的持久卷 (PV),或者你需要在部署 ZooKeeper 集群或 Kafka 集群之前手动供应 PV。

部署 ZooKeeper 集群

Apache ZooKeeper 是一个强一致性的分布式系统,被其他分布式系统用于集群协调和配置管理。

注意:你可以使用此 zookeeper_mini.yaml 清单文件创建一个 ZooKeeper 集群。你可以在这里了解更多关于在 Kubernetes 上运行 ZooKeeper 集群的信息,并在此获得对清单文件及其内容的更深入解释。

应用此清单文件后,你将看到类似以下的输出。

$ kubectl apply -f zookeeper\_mini.yaml

service "zk-hs" created

service "zk-cs" created

poddisruptionbudget "zk-pdb" created

statefulset "zk" created

此清单文件使用 StatefulSet zk 创建一个由三个 ZooKeeper 服务器组成的集群;一个 Headless Service zk-hs,用于控制集群的域名;一个 Service zk-cs,客户端可以使用它连接到就绪的 ZooKeeper 实例;以及一个 PodDisruptionBugdet zk-pdb,允许一次计划中断。(请注意,虽然此集群适用于演示目的,但其规模不足以用于生产环境。)

如果你在另一个终端中使用 kubectl get 命令来观察 Pod 的创建过程,你会发现,与 OrderedReady 策略(实现完整 StatefulSet 保证的默认策略)相反,zk StatefulSet 中的所有 Pod 都是并行创建的。

$ kubectl get po -lapp=zk -w

NAME           READY         STATUS        RESTARTS     AGE


zk-0           0/1             Pending      0                   0s


zk-0           0/1             Pending     0                  0s


zk-1           0/1             Pending     0                  0s


zk-1           0/1             Pending     0                  0s


zk-0           0/1             ContainerCreating      0                  0s


zk-2           0/1             Pending      0                  0s


zk-1           0/1             ContainerCreating     0                  0s


zk-2           0/1             Pending      0                  0s


zk-2           0/1             ContainerCreating      0                  0s


zk-0           0/1             Running     0                  10s


zk-2           0/1             Running     0                  11s


zk-1           0/1             Running      0                  19s


zk-0           1/1             Running      0                  20s


zk-1           1/1             Running      0                  30s


zk-2           1/1             Running      0                  30s

这是因为 zookeeper_mini.yaml 清单文件将 StatefulSet 的 podManagementPolicy 设置为 Parallel。

apiVersion: apps/v1beta1  
kind: StatefulSet  
metadata:  
   name: zk  

spec:  
   serviceName: zk-hs  

   replicas: 3  

   updateStrategy:  

       type: RollingUpdate  

   podManagementPolicy: Parallel  

 ...

许多分布式系统(如 ZooKeeper)不要求其进程有序创建和终止。你可以使用 Parallel Pod 管理策略来加快管理这些系统的 StatefulSet 的创建和删除速度。请注意,使用 Parallel Pod 管理时,如果 StatefulSet 控制器创建 Pod 失败,它不会阻塞。当 StatefulSet 的 podManagementPolicy 设置为 OrderedReady 时,将执行有序的、顺序的 Pod 创建和终止。

部署 Kafka 集群

Apache Kafka 是一个流行的分布式流处理平台。Kafka 生产者将数据写入分区主题,这些主题以可配置的复制因子存储在 broker 集群上。消费者从存储在 broker 上的分区消费生成的数据。

注意:可以在此处找到清单文件内容的详细信息。你可以在此处了解更多关于在 Kubernetes 上运行 Kafka 集群的信息。

要创建集群,你只需下载并应用 kafka_mini.yaml 清单文件。应用此清单文件后,你将看到类似以下的输出

$ kubectl apply -f kafka\_mini.yaml

service "kafka-hs" created

poddisruptionbudget "kafka-pdb" created

statefulset "kafka" created

此清单文件使用 kafka StatefulSet 创建一个由三个 broker 组成的集群,一个 Headless Service kafka-hs 用于控制 broker 的域名;以及一个 PodDisruptionBudget kafka-pdb,允许一次计划中断。这些 broker 配置为通过 zk-cs Service 连接到我们上面创建的 ZooKeeper 集群。与上面部署的 ZooKeeper 集群一样,此 Kafka 集群适用于演示目的,但其规模可能不足以用于生产环境。

如果你观察 Pod 创建过程,你会注意到,与上面创建的 ZooKeeper 集群一样,Kafka 集群使用了 Parallel podManagementPolicy。

$ kubectl get po -lapp=kafka -w

NAME           READY         STATUS        RESTARTS     AGE


kafka-0     0/1             Pending      0                   0s


kafka-0     0/1             Pending      0                  0s


kafka-1     0/1             Pending      0                  0s


kafka-1     0/1             Pending      0                  0s


kafka-2     0/1             Pending      0                  0s


kafka-0     0/1             ContainerCreating     0                  0s


kafka-2     0/1             Pending      0                  0s


kafka-1     0/1             ContainerCreating     0                  0s


kafka-1     0/1             Running     0                  11s


kafka-0     0/1             Running     0                  19s


kafka-1     1/1             Running     0                  23s


kafka-0     1/1             Running     0                  32s

生产和消费数据

你可以使用 kubectl run 命令执行 kafka-topics.sh 脚本来创建一个名为 test 的主题。

$ kubectl run -ti --image=gcr.io/google\_containers/kubernetes-kafka:1.0-10.2.1 createtopic --restart=Never --rm -- kafka-topics.sh --create \

\> --topic test \

\> --zookeeper zk-cs.default.svc.cluster.local:2181 \

\> --partitions 1 \

\> --replication-factor 3

现在你可以使用 kubectl run 命令执行 kafka-console-consumer.sh 命令来监听消息。

$ kubectl run -ti --image=gcr.io/google\_containers/kubnetes-kafka:1.0-10.2.1 consume --restart=Never --rm -- kafka-console-consumer.sh --topic test --bootstrap-server kafka-0.kafka-hs.default.svc.cluster.local:9093

在另一个终端中,你可以运行 kafka-console-producer.sh 命令。

$kubectl run -ti --image=gcr.io/google\_containers/kubernetes-kafka:1.0-10.2.1 produce --restart=Never --rm \

\>   -- kafka-console-producer.sh --topic test --broker-list kafka-0.kafka-hs.default.svc.cluster.local:9093,kafka-1.kafka-hs.default.svc.cluster.local:9093,kafka-2.kafka-hs.default.svc.cluster.local:9093

第二个终端的输出会出现在第一个终端中。如果你在更新集群时继续生产和消费消息,你会注意到没有消息丢失。当单个 broker 更新时,你可能会看到由于分区领导者变化而产生的错误消息,但客户端会重试直到消息被提交。这是由于 StatefulSet 滚动更新的有序、顺序特性所致,我们将在下一节中进一步探讨。

更新 Kafka 集群

StatefulSet 更新与 DaemonSet 更新类似,它们都通过设置相应 API 对象的 spec.updateStrategy 进行配置。当更新策略设置为 OnDelete 时,相应的控制器仅在 StatefulSet 或 DaemonSet 中的 Pod 被删除时才会创建新的 Pod。当更新策略设置为 RollingUpdate 时,当 DaemonSet 或 StatefulSet 的 spec.template 字段发生修改时,控制器会删除并重新创建 Pod。你可以使用滚动更新来更改 StatefulSet 或 DaemonSet 中 Pod 的配置(通过环境变量或命令行参数)、资源请求、资源限制、容器镜像、标签和/或注解。请注意,所有更新都是破坏性的,总是需要销毁并重新创建 DaemonSet 或 StatefulSet 中的每个 Pod。StatefulSet 滚动更新与 DaemonSet 滚动更新的区别在于,Pod 的终止和创建是有序且顺序执行的。

你可以通过 patching kafka StatefulSet 来将 CPU 资源请求减少到 250m。

$ kubectl patch sts kafka --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"250m"}]'

statefulset "kafka" patched

如果你观察 StatefulSet 中 Pod 的状态,你会看到每个 Pod 都按照倒序(从序号最大的 Pod 开始,依次递减到最小)删除并重新创建。控制器会等待每个更新后的 Pod 运行并就绪后,再更新下一个 Pod。

$kubectl get po -lapp=kafka -w

NAME           READY         STATUS       RESTARTS     AGE


kafka-0     1/1             Running     0                   13m


kafka-1     1/1             Running     0                   13m


kafka-2     1/1             Running     0                   13m


kafka-2     1/1             Terminating     0                 14m


kafka-2     0/1             Terminating     0                 14m


kafka-2     0/1             Terminating     0                 14m


kafka-2     0/1             Terminating     0                 14m


kafka-2     0/1             Pending     0                 0s


kafka-2     0/1             Pending     0                 0s


kafka-2     0/1             ContainerCreating     0                 0s


kafka-2     0/1             Running     0                 10s


kafka-2     1/1             Running     0                 21s


kafka-1     1/1             Terminating     0                 14m


kafka-1     0/1             Terminating     0                 14m


kafka-1     0/1             Terminating     0                 14m


kafka-1     0/1             Terminating     0                 14m


kafka-1     0/1             Pending     0                 0s


kafka-1     0/1             Pending     0                 0s


kafka-1     0/1             ContainerCreating     0                 0s


kafka-1     0/1             Running     0                 11s


kafka-1     1/1             Running     0                 21s


kafka-0     1/1             Terminating     0                 14m


kafka-0     0/1             Terminating     0                 14m


kafka-0     0/1             Terminating     0                 14m


kafka-0     0/1             Terminating     0                 14m


kafka-0     0/1             Pending     0                 0s


kafka-0     0/1             Pending     0                 0s


kafka-0     0/1             ContainerCreating     0                 0s


kafka-0     0/1             Running     0                 10s


kafka-0     1/1             Running     0                 22s

请注意,非计划的中断不会在更新过程中导致意外更新。也就是说,StatefulSet 控制器将始终以正确的版本重新创建 Pod,以确保更新的顺序得以保留。如果一个 Pod 被删除,并且它已经被更新过,它将根据 StatefulSet 的 spec.template 的更新版本进行创建。如果该 Pod 尚未更新,它将根据 StatefulSet 的 spec.template 的先前版本进行创建。我们将在以下部分进一步探讨这一点。

暂存更新

根据你的组织处理部署和配置修改的方式,你可能希望或需要先暂存对 StatefulSet 的更新,然后再允许其推广。你可以通过为 RollingUpdate 设置一个分区来实现这一点。当 StatefulSet 控制器检测到 StatefulSet 的 updateStrategy 中存在分区时,它只会将 StatefulSet 的 spec.template 的更新版本应用于序号大于或等于该分区值的 Pod。

你可以通过 patching kafka StatefulSet 来为 RollingUpdate 更新策略添加一个分区。如果你将分区设置为大于或等于 StatefulSet 的 spec.replicas(如下所示)的数字,你随后对 StatefulSet 的 spec.template 所做的任何更新都将暂存以待推广,但 StatefulSet 控制器不会启动滚动更新。

$ kubectl patch sts kafka -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":3}}}}'

statefulset "kafka" patched

如果你通过 patching StatefulSet 将请求的 CPU 设置为 0.3,你会注意到没有任何 Pod 被更新。

$ kubectl patch sts kafka --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/cpu", "value":"0.3"}]'

statefulset "kafka" patched

即使你删除一个 Pod 并等待 StatefulSet 控制器重新创建它,你会注意到该 Pod 是使用当前的 CPU 请求重新创建的。

$   kubectl delete po kafka-1


pod "kafka-1" deleted


$ kubectl get po kafka-1 -w

NAME           READY         STATUS                           RESTARTS     AGE


kafka-1     0/1             ContainerCreating     0                   10s


kafka-1     0/1             Running     0                 19s


kafka-1     1/1             Running     0                 21s



$ kubectl get po kafka-1 -o yaml

apiVersion: v1

kind: Pod

metadata:

   ...


       resources:


           requests:


               cpu: 250m


               memory: 1Gi

金丝雀发布

通常,我们希望在将镜像更新或配置更改推广到全局之前,先在应用的单个实例上进行验证。如果你将上面创建的分区修改为 2,StatefulSet 控制器将推广一个金丝雀版本,用于验证更新是否按预期工作。

$ kubectl patch sts kafka -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'

statefulset "kafka" patched

你可以观察 StatefulSet 控制器更新 kafka-2 Pod 并在更新完成后暂停。

$   kubectl get po -lapp=kafka -w


NAME           READY         STATUS       RESTARTS     AGE


kafka-0     1/1             Running     0                   50m


kafka-1     1/1             Running     0                   10m


kafka-2     1/1             Running     0                   29s


kafka-2     1/1             Terminating     0                 34s


kafka-2     0/1             Terminating     0                 38s


kafka-2     0/1             Terminating     0                 39s


kafka-2     0/1             Terminating     0                 39s


kafka-2     0/1             Pending     0                 0s


kafka-2     0/1             Pending     0                 0s


kafka-2     0/1             Terminating     0                 20s


kafka-2     0/1             Terminating     0                 20s


kafka-2     0/1             Pending     0                 0s


kafka-2     0/1             Pending     0                 0s


kafka-2     0/1             ContainerCreating     0                 0s


kafka-2     0/1             Running     0                 19s


kafka-2     1/1             Running     0                 22s

分阶段发布

与金丝雀发布类似,你可以根据分阶段的进展(例如,线性、几何或指数级发布)来推广更新。

如果你通过 patching kafka StatefulSet 将分区设置为 1,StatefulSet 控制器会再更新一个 broker。

$ kubectl patch sts kafka -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":1}}}}'

statefulset "kafka" patched

如果你将其设置为 0,StatefulSet 控制器将更新最后一个 broker 并完成更新。

$ kubectl patch sts kafka -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'

statefulset "kafka" patched

请注意,你不必将分区递减 1。对于更大的 StatefulSet——例如,拥有 100 个副本的 StatefulSet——你可能会使用更像是 100、99、90、50、0 的进展。在这种情况下,你将暂存更新,部署金丝雀版本,推广到 10 个实例,更新 50% 的 Pod,然后完成更新。

清理

要删除上面创建的 API 对象,你可以使用 kubectl delete 命令删除你用于创建 ZooKeeper 集群和 Kafka 集群的两个清单文件。

$ kubectl delete -f kafka\_mini.yaml

service "kafka-hs" deleted

poddisruptionbudget "kafka-pdb" deleted

Statefulset “kafka” deleted


$ kubectl delete -f zookeeper\_mini.yaml

service "zk-hs" deleted

service "zk-cs" deleted

poddisruptionbudget "zk-pdb" deleted

statefulset "zk" deleted

按设计,StatefulSet 控制器不会删除任何持久卷声明 (PVC):为 ZooKeeper 集群和 Kafka 集群创建的 PVC 必须手动删除。根据你集群的存储回收策略,你也可能需要手动删除支持的 PV。

DaemonSet 滚动更新、历史记录和回滚

在本节中,我们将向您展示如何对 DaemonSet 执行滚动更新、查看其历史记录,以及在出现不良部署后执行回滚。我们将使用 DaemonSet 在集群中的每个 Kubernetes 节点上部署一个 Prometheus node exporter。这些 node exporter 将节点指标导出到 Prometheus 监控系统。为简单起见,本文省略了 Prometheus server 的安装以及与 DaemonSet Pod 通信所需的服务。

先决条件

要继续阅读本文这一部分,您需要一个可用的 Kubernetes 1.7 集群和 kubectl 1.7 或更高版本。如果您已经按照第一部分操作,可以使用同一个集群。

DaemonSet 滚动更新。首先,准备 node exporter DaemonSet 清单文件,以在集群的每个节点上运行 v0.13 版本的 Prometheus node exporter。

$ cat \>\> node-exporter-v0.13.yaml \<\<EOF

apiVersion: extensions/v1beta1  
kind: DaemonSet  
metadata:  
   name: node-exporter  

spec:  
   updateStrategy:  

       type: RollingUpdate  

   template:  

       metadata:  

           labels:  

               app: node-exporter  

           name: node-exporter  

       spec:  

           containers:  

           - image: prom/node-exporter:v0.13.0  

               name: node-exporter  

               ports:  

               - containerPort: 9100  

                   hostPort: 9100  

                   name: scrape  

           hostNetwork: true  

           hostPID: true


EOF

请注意,您需要通过显式设置 DaemonSet .spec.updateStrategy.type 为 RollingUpdate 来启用 DaemonSet 滚动更新功能。

应用清单文件以创建 node exporter DaemonSet

$ kubectl apply -f node-exporter-v0.13.yaml --record

daemonset "node-exporter" created

等待第一次 DaemonSet 部署完成

$ kubectl rollout status ds node-exporter  
daemon set "node-exporter" successfully rolled out

您应该看到每个节点都运行着一个 node exporter Pod 的副本

$ kubectl get pods -l app=node-exporter -o wide

要对 node exporter DaemonSet 执行滚动更新,请准备一个包含 v0.14 版本 Prometheus node exporter 的清单文件

$ cat node-exporter-v0.13.yaml ```  sed "s/v0.13.0/v0.14.0/g" \> node-exporter-v0.14.yaml

然后应用 v0.14 版本的 node exporter DaemonSet

$ kubectl apply -f node-exporter-v0.14.yaml --record

daemonset "node-exporter" configured

等待 DaemonSet 滚动更新完成

$ kubectl rollout status ds node-exporter

...

Waiting for rollout to finish: 3 out of 4 new pods have been updated...  
Waiting for rollout to finish: 3 of 4 updated pods are available...  
daemon set "node-exporter" successfully rolled out

我们通过更新 DaemonSet 模板触发了 DaemonSet 滚动更新。默认情况下,一次只终止一个旧的 DaemonSet Pod 并创建一个新的 DaemonSet Pod。

现在,我们将通过将镜像更新为无效值来导致部署失败

$ cat node-exporter-v0.13.yaml | sed "s/v0.13.0/bad/g" \> node-exporter-bad.yaml


$ kubectl apply -f node-exporter-bad.yaml --record

daemonset "node-exporter" configured

请注意,部署永远不会完成

$ kubectl rollout status ds node-exporter   
Waiting for rollout to finish: 0 out of 4 new pods have been updated...  
Waiting for rollout to finish: 1 out of 4 new pods have been updated…

# Use ^C to exit

这是预期的行为。我们之前提到,DaemonSet 滚动更新一次只终止并创建一个 Pod。由于新 Pod 永远无法变为可用状态,部署就会停止,从而防止无效的规范传播到多个节点。StatefulSet 滚动更新在处理失败部署时实现了相同的行为。不成功的更新会被阻塞,直到通过回滚或使用有效的规范向前滚动来纠正。

$ kubectl get pods -l app=node-exporter

NAME                                   READY         STATUS                 RESTARTS     AGE


node-exporter-f2n14     0/1             ErrImagePull     0                   3m


...


# N = number of nodes

$ kubectl get ds node-exporter  
NAME                       DESIRED     CURRENT     READY         UP-TO-DATE     AVAILABLE     NODE SELECTOR     AGE  

node-exporter     N                 N                 N-1             1                       N                     \<none\>                   46m

DaemonSet 历史记录、回滚和向前滚动

接下来,执行回滚。查看 node exporter DaemonSet 的部署历史记录

$ kubectl rollout history ds node-exporter   
daemonsets "node-exporter"  
REVISION               CHANGE-CAUSE  

1                             kubectl apply --filename=node-exporter-v0.13.yaml --record=true  

2                             kubectl apply --filename=node-exporter-v0.14.yaml --record=true


3                             kubectl apply --filename=node-exporter-bad.yaml --record=true

检查您想要回滚到的修订版本的详细信息

$ kubectl rollout history ds node-exporter --revision=2  
daemonsets "node-exporter" with revision #2  
Pod Template:  
   Labels:             app=node-exporter  

   Containers:  

     node-exporter:  

       Image:           prom/node-exporter:v0.14.0  

       Port:             9100/TCP  

       Environment:               \<none\>  

       Mounts:         \<none\>  

   Volumes:           \<none\>

您可以通过 kubectl rollout history 快速回滚到找到的任何 DaemonSet 修订版本

# Roll back to the last revision

$ kubectl rollout undo ds node-exporter   
daemonset "node-exporter" rolled back


# Or use --to-revision to roll back to a specific revision

$ kubectl rollout undo ds node-exporter --to-revision=2  
daemonset "node-exporter" rolled back

DaemonSet 回滚是通过向前滚动完成的。因此,回滚后,DaemonSet 修订版本 2 变为修订版本 4(当前修订版本)

$ kubectl rollout history ds node-exporter   
daemonsets "node-exporter"  
REVISION               CHANGE-CAUSE  

1                             kubectl apply --filename=node-exporter-v0.13.yaml --record=true  

3                             kubectl apply --filename=node-exporter-bad.yaml --record=true  

4                             kubectl apply --filename=node-exporter-v0.14.yaml --record=true

node exporter DaemonSet 现在又恢复健康了

$ kubectl rollout status ds node-exporter  
daemon set "node-exporter" successfully rolled out


# N = number of nodes

$ kubectl get ds node-exporter

NAME                       DESIRED     CURRENT     READY         UP-TO-DATE     AVAILABLE     NODE SELECTOR     AGE  

node-exporter     N                 N                 N                 N                       N                     \<none\>                   46m

如果在执行回滚时指定了当前的 DaemonSet 修订版本,则回滚会被跳过

$ kubectl rollout undo ds node-exporter --to-revision=4  
daemonset "node-exporter" skipped rollback (current template already matches revision 4)

如果未找到 DaemonSet 修订版本,kubectl 会显示此警告

$ kubectl rollout undo ds node-exporter --to-revision=10  
error: unable to find specified revision 10 in history

请注意,kubectl rollout history 和 kubectl rollout status 也支持 StatefulSets!

清理

$ kubectl delete ds node-exporter

DaemonSet 和 StatefulSet 的下一步计划

滚动更新和回滚弥补了 DaemonSet 和 StatefulSet 的一个重要功能空白。在我们规划 Kubernetes 1.8 时,我们希望继续专注于推动核心控制器达到 GA(普遍可用)状态。这可能意味着一些高级功能请求(例如自动回滚、早期死亡检测)将被推迟,以便确保核心控制器的一致性、可用性和稳定性。我们欢迎反馈和贡献,因此请随时在 Slack 上联系我们,在 Stack Overflow 上提问,或者在 GitHub 上提出 issue 或提交 pull request。