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

Kubernetes StatefulSets 和 DaemonSets 更新

本文讨论了 Kubernetes 中 DaemonSetStatefulSet API 对象的最新更新。我们使用 Apache ZooKeeperApache Kafka StatefulSets 以及 Prometheus 节点导出器 DaemonSet 来探索这些功能。

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

在 Kubernetes 1.7 中,我们增强了 DaemonSet 控制器,使其可以跟踪 DaemonSet 的 PodTemplateSpecs 的修订历史。这允许 DaemonSet 控制器回滚更新。我们还向 StatefulSet API 对象添加了 RollingUpdate 策略,并为 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 的终止和创建是有序和顺序的。

您可以修补 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 运行并就绪,然后再更新后续的 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。如果 Pod 尚未更新,则将根据 StatefulSet 的 spec.template 的先前版本创建该 Pod。我们将在以下部分中进一步探讨这一点。

暂存更新

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

您可以修补 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

如果您修补 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

推出 Canary

通常,我们希望在全局推出之前在应用程序的单个实例上验证镜像更新或配置更改。如果将上面创建的分区修改为 2,则 StatefulSet 控制器将推出一个 canary,可用于验证更新是否按预期工作。

$ 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

分阶段推出

与推出 Canary 类似,您可以根据分阶段的进展(例如,线性、几何或指数推出)来推出更新。

如果你修改 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

请注意,你不必每次将分区减一。对于较大的 StatefulSet(例如,具有 100 个副本的 StatefulSet),你可能会使用更像 100、99、90、50、0 的渐进方式。在这种情况下,你可以分阶段进行更新,部署一个 Canary 版本,推出到 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 节点导出器。这些节点导出器将节点指标导出到 Prometheus 监控系统。为了简单起见,我们省略了 Prometheus 服务器的安装以及用于 与 DaemonSet Pod 通信的服务。

先决条件

要跟进本节内容,你需要一个可用的 Kubernetes 1.7 集群和 kubectl 1.7 或更高版本。如果你已经完成了第一节的内容,你可以使用同一个集群。

DaemonSet 滚动更新。首先,准备节点导出器 DaemonSet 清单,在集群中的每个节点上运行 v0.13 Prometheus 节点导出器

$ 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 滚动更新功能。

应用清单来创建节点导出器 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

你应该看到每个节点都运行一个节点导出器 Pod 的副本

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

要在节点导出器 DaemonSet 上执行滚动更新,请准备一个包含 v0.14 Prometheus 节点导出器的清单

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

然后应用 v0.14 节点导出器 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 历史记录、回滚和向前滚动

接下来,执行回滚。查看节点导出器 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

节点导出器 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` 也支持 StatefulSet!

清理

$ kubectl delete ds node-exporter

DaemonSet 和 StatefulSet 的下一步计划

滚动更新和回滚弥补了 DaemonSet 和 StatefulSet 的一个重要功能差距。在我们规划 Kubernetes 1.8 时,我们希望继续专注于将核心控制器推进到 GA。这可能意味着一些高级功能请求(例如,自动回滚、早期失效检测)将被推迟,以便确保核心控制器的一致性、可用性和稳定性。我们欢迎反馈和贡献,因此请随时在 Slack 上联系,在 Stack Overflow 上提问,或在 GitHub 上打开问题或拉取请求。