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

Kubernetes StatefulSets 和 DaemonSets 更新

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

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

在 Kubernetes 1.7 中,我们增强了 DaemonSet 控制器,以跟踪 DaemonSets 的 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 的持久卷 (PVs),或者您需要在部署 ZooKeeper 集群或 Kafka 集群之前手动供应 PVs。

部署 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 服务器组成的集群;一个无头服务 zk-hs,用于控制集群的域;一个服务 zk-cs,客户端可以使用它连接到就绪的 ZooKeeper 实例;以及一个 PodDisruptionBudget 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 管理策略来加速管理这些系统的 StatefulSets 的创建和删除。请注意,当使用 Parallel Pod 管理时,如果 StatefulSet 控制器未能创建 Pod,它不会阻塞。当 StatefulSet 的 podManagementPolicy 设置为 OrderedReady 时,将执行有序的顺序 Pod 创建和终止。

部署 Kafka 集群

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

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

要创建集群,您只需下载并应用 kafka_mini.yaml 清单。应用清单后,您将看到如下输出:

$ kubectl apply -f kafka\_mini.yaml

service "kafka-hs" created

poddisruptionbudget "kafka-pdb" created

statefulset "kafka" created

该清单使用 kafka StatefulSet 创建了一个由三个代理组成的集群;一个无头服务 kafka-hs,用于控制代理的域;以及一个 PodDisruptionBudget kafka-pdb,允许进行一次计划中断。这些代理配置为通过 zk-cs 服务连接到我们上面创建的 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

第二个终端的输出出现在第一个终端中。如果您在更新集群时继续生产和消费消息,您会注意到没有消息丢失。当分区的主导者在单个代理更新时发生变化时,您可能会看到错误消息,但客户端会重试直到消息被提交。这是由于 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 尚未更新,它将从 StatefulSet 的 spec.template 的先前版本创建。我们将在以下章节中进一步探讨这一点。

暂存更新

根据您的组织处理部署和配置修改的方式,您可能希望或需要在允许推出进程之前暂存 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

金丝雀发布

通常,我们希望在全局推出镜像更新或配置更改之前,在应用程序的单个实例上验证其是否按预期工作。如果您将上面创建的分区修改为 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

分阶段推出

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

如果您修补 kafka StatefulSet 将分区设置为 1,StatefulSet 控制器将再更新一个代理。

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

statefulset "kafka" patched

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

$ 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 个实例,更新百分之五十的 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 上打开问题或拉取请求。