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

StatefulSet:在 Kubernetes 中轻松运行和扩展有状态应用程序

编者按: 此文章是关于 Kubernetes 1.5 新特性的 系列深度文章 的一部分。

在最新版本 Kubernetes 1.5 中,我们将之前称为 PetSet 的功能移到了 beta 版,并命名为 StatefulSet。除了社区选择的名称外,API 对象没有重大更改,但我们为集合中 Pod 的部署添加了“每个索引最多一个 Pod”的语义。凭借有序部署、有序终止、唯一的网络名称和持久稳定的存储,我们认为我们拥有支持许多容器化有状态工作负载的正确原语。我们不声称该功能是 100% 完整的(毕竟它是软件),但我们相信它目前的形式是有用的,并且我们可以在向最终 GA 版本推进时以向后兼容的方式扩展 API。

StatefulSet 何时是我的存储应用程序的正确选择?

DeploymentsReplicaSets 是在 Kubernetes 上运行应用程序的无状态副本的好方法,但它们的语义对于部署有状态应用程序来说并不完全正确。StatefulSet 的目的是为控制器提供正确的语义,以部署各种有状态工作负载。但是,将您的存储应用程序迁移到 Kubernetes 并不总是正确的选择。在您全力以赴地融合您的存储层和编排框架之前,您应该问自己几个问题。

您的应用程序可以使用远程存储运行,还是需要本地存储介质?

目前,我们建议将 StatefulSets 与远程存储一起使用。因此,您必须准备好容忍网络附加存储的性能影响。即使使用优化的存储实例,您也可能无法实现与本地附加固态存储介质相同的性能。您云上的网络附加存储的性能是否允许您的存储应用程序满足其 SLA?如果是这样,从自动化的角度来看,在 StatefulSet 中运行您的应用程序提供了引人注目的好处。如果运行您的存储应用程序的节点发生故障,则包含该应用程序的 Pod 可以重新调度到另一个节点,并且由于它使用网络附加存储介质,因此在重新调度后其数据仍然可用。

您是否需要扩展您的存储应用程序?

您希望通过在 StatefulSet 中运行应用程序获得什么好处?您是否为整个组织拥有一个存储应用程序实例?扩展您的存储应用程序是您实际遇到的问题吗?如果您有几个存储应用程序实例,并且它们成功地满足了您组织的需求,并且这些需求没有快速增长,那么您已经处于局部最优状态。

但是,如果您有一个微服务生态系统,或者如果您经常推出包含存储应用程序的新服务足迹,那么您可能会从自动化和整合中受益。如果您已经使用 Kubernetes 来管理生态系统的无状态层,则应考虑使用相同的基础架构来管理存储应用程序。

可预测的性能有多重要?

Kubernetes 尚不支持跨容器的网络或存储 I/O 隔离。将您的存储应用程序与吵闹的邻居放在一起可能会降低应用程序可以处理的 QPS。您可以通过将包含存储应用程序的 Pod 调度为节点上的唯一租户(从而为其提供专用计算机)或使用 Pod 反亲和性规则来隔离争用网络或磁盘的 Pod 来缓解此问题,但这意味着您必须主动识别并缓解热点。

如果从您的存储应用程序中榨取绝对最大的 QPS 不是您的主要关注点,如果您愿意并且能够缓解热点以确保您的存储应用程序满足其 SLA,并且启动新的“足迹”(服务或服务集合)、扩展它们以及灵活地重新分配资源是您的主要关注点,那么 Kubernetes 和 StatefulSet 可能是解决此问题的正确解决方案。

您的应用程序是否需要专用硬件或实例类型?

如果您在高配置硬件或超大型实例大小上运行存储应用程序,而在商品硬件或更小、更便宜的映像上运行其他工作负载,则可能不想部署异构集群。如果可以为所有类型的应用程序标准化为单一实例大小,那么您可能会受益于 Kubernetes 提供的灵活资源重新分配和整合。

一个实际示例 - ZooKeeper

ZooKeeper 是 StatefulSet 的一个有趣用例,原因有二。首先,它证明了 StatefulSet 可用于在 Kubernetes 上运行分布式、强一致的存储应用程序。其次,它是运行 Apache HadoopApache Kakfa 等工作负载在 Kubernetes 上的先决条件。Kubernetes 文档中提供了有关在 Kubernetes 上部署 ZooKeeper 集群的 深入教程,我们将在下面概述一些关键功能。

创建 ZooKeeper 集群
创建集群就像使用 kubectl create 生成清单中存储的对象一样简单。

$ kubectl create -f [http://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml](https://raw.githubusercontent.com/kubernetes/kubernetes.github.io/master/docs/tutorials/stateful-application/zookeeper.yaml)

service "zk-headless" created

configmap "zk-config" created

poddisruptionbudget "zk-budget" created

statefulset "zk" created

当您创建清单时,StatefulSet 控制器会根据其序号创建每个 Pod,并在每个 Pod 都处于 Running 和 Ready 状态后,再创建其后继者。

$ kubectl get -w -l app=zk

NAME      READY     STATUS    RESTARTS   AGE

zk-0      0/1       Pending   0          0s

zk-0      0/1       Pending   0         0s

zk-0      0/1       Pending   0         7s

zk-0      0/1       ContainerCreating   0         7s

zk-0      0/1       Running   0         38s

zk-0      1/1       Running   0         58s

zk-1      0/1       Pending   0         1s

zk-1      0/1       Pending   0         1s

zk-1      0/1       ContainerCreating   0         1s

zk-1      0/1       Running   0         33s

zk-1      1/1       Running   0         51s

zk-2      0/1       Pending   0         0s

zk-2      0/1       Pending   0         0s

zk-2      0/1       ContainerCreating   0         0s

zk-2      0/1       Running   0         25s

zk-2      1/1       Running   0         40s

检查 StatefulSet 中每个 Pod 的主机名,您可以看到 Pod 的主机名也包含 Pod 的序号。

$ for i in 0 1 2; do kubectl exec zk-$i -- hostname; done

zk-0

zk-1

zk-2

ZooKeeper 将每个服务器的唯一标识符存储在一个名为“myid”的文件中。用于 ZooKeeper 服务器的标识符只是自然数。对于集群中的服务器,通过将 Pod 主机名中提取的序号加一来填充“myid”文件。

$ for i in 0 1 2; do echo "myid zk-$i";kubectl exec zk-$i -- cat /var/lib/zookeeper/data/myid; done

myid zk-0

1

myid zk-1

2

myid zk-2

3

每个 Pod 都有一个基于其主机名和 zk-headless Headless Service 控制的网络域的唯一网络地址。

$  for i in 0 1 2; do kubectl exec zk-$i -- hostname -f; done

zk-0.zk-headless.default.svc.cluster.local

zk-1.zk-headless.default.svc.cluster.local

zk-2.zk-headless.default.svc.cluster.local

唯一的 Pod 序号和唯一的网络地址相结合,使您可以使用一致的集群成员资格来填充 ZooKeeper 服务器的配置文件。

$  kubectl exec zk-0 -- cat /opt/zookeeper/conf/zoo.cfg

clientPort=2181

dataDir=/var/lib/zookeeper/data

dataLogDir=/var/lib/zookeeper/log

tickTime=2000

initLimit=10

syncLimit=2000

maxClientCnxns=60

minSessionTimeout= 4000

maxSessionTimeout= 40000

autopurge.snapRetainCount=3

autopurge.purgeInteval=1

server.1=zk-0.zk-headless.default.svc.cluster.local:2888:3888

server.2=zk-1.zk-headless.default.svc.cluster.local:2888:3888

server.3=zk-2.zk-headless.default.svc.cluster.local:2888:3888

StatefulSet 使您可以以一致且可重现的方式部署 ZooKeeper。您不会创建多个具有相同 id 的服务器,服务器可以通过稳定的网络地址找到彼此,并且由于集群具有一致的成员资格,因此它们可以执行领导者选举并复制写入。

验证集群是否正常工作的最简单方法是将值写入一个服务器并从另一个服务器读取它。您可以使用 ZooKeeper 发行版附带的“zkCli.sh”脚本来创建一个包含一些数据的 ZNode。

$  kubectl exec zk-0 zkCli.sh create /hello world

...


WATCHER::


WatchedEvent state:SyncConnected type:None path:null

Created /hello

您可以使用相同的脚本从集群中的另一个服务器读取数据。

$  kubectl exec zk-1 zkCli.sh get /hello

...


WATCHER::


WatchedEvent state:SyncConnected type:None path:null

world

...

您可以通过删除 zk StatefulSet 来关闭集群。

$  kubectl delete statefulset zk

statefulset "zk" deleted

级联删除会销毁 StatefulSet 中的每个 Pod,并按照 Pod 序号的相反顺序进行,并且它会等待每个 Pod 完全终止后再终止其前任。

$  kubectl get pods -w -l app=zk

NAME      READY     STATUS    RESTARTS   AGE

zk-0      1/1       Running   0          14m

zk-1      1/1       Running   0          13m

zk-2      1/1       Running   0          12m

NAME      READY     STATUS        RESTARTS   AGE

zk-2      1/1       Terminating   0          12m

zk-1      1/1       Terminating   0         13m

zk-0      1/1       Terminating   0         14m

zk-2      0/1       Terminating   0         13m

zk-2      0/1       Terminating   0         13m

zk-2      0/1       Terminating   0         13m

zk-1      0/1       Terminating   0         14m

zk-1      0/1       Terminating   0         14m

zk-1      0/1       Terminating   0         14m

zk-0      0/1       Terminating   0         15m

zk-0      0/1       Terminating   0         15m

zk-0      0/1       Terminating   0         15m

您可以使用 kubectl apply 来重新创建 zk StatefulSet 并重新部署集群。

$  kubectl apply -f [http://k8s.io/docs/tutorials/stateful-application/zookeeper.yaml](https://raw.githubusercontent.com/kubernetes/kubernetes.github.io/master/docs/tutorials/stateful-application/zookeeper.yaml)

service "zk-headless" configured

configmap "zk-config" configured

statefulset "zk" created

如果您使用“zkCli.sh”脚本获取在删除 StatefulSet 之前输入的值,您会发现该集群仍然提供数据。

$  kubectl exec zk-2 zkCli.sh get /hello

...


WATCHER::


WatchedEvent state:SyncConnected type:None path:null

world

...

StatefulSet 确保即使 StatefulSet 中的所有 Pod 都被销毁,当它们被重新调度时,ZooKeeper 集群也可以选举新的领导者并继续服务请求。

容忍节点故障

ZooKeeper 将其状态机复制到集群中的不同服务器,其明确目的是容忍节点故障。默认情况下,Kubernetes 调度程序可以将 zk StatefulSet 中的多个 Pod 部署到同一节点。如果 zk-0 和 zk-1 Pod 部署在同一节点上,并且该节点发生故障,则 ZooKeeper 集群无法形成仲裁来提交写入,并且 ZooKeeper 服务将遇到中断,直到其中一个 Pod 可以重新调度。

您应该始终为集群中的关键进程预留余量容量,如果您这样做,在这种情况下,Kubernetes 调度程序会将 Pod 重新调度到另一个节点,并且中断将是短暂的。

如果您的服务的 SLA 甚至排除了由于单个节点故障而导致的短暂中断,则应使用 PodAntiAffinity 注释。用于创建集群的清单包含这样的注释,它告诉 Kubernetes 调度程序不要将 zk StatefulSet 中的多个 Pod 放置在同一节点上。

容忍计划维护

用于创建 ZooKeeper 集群的清单还创建了一个 PodDisruptionBudget,zk-budget。zk-budget 会告知 Kubernetes 该服务可以容忍的最大中断(不健康的 Pod)上限。

 {

              "podAntiAffinity": {

                "requiredDuringSchedulingRequiredDuringExecution": [{

                  "labelSelector": {

                    "matchExpressions": [{

                      "key": "app",

                      "operator": "In",

                      "values": ["zk-headless"]

                    }]

                  },

                  "topologyKey": "kubernetes.io/hostname"

                }]

              }

            }

}
$ kubectl get poddisruptionbudget zk-budget

NAME        MIN-AVAILABLE   ALLOWED-DISRUPTIONS   AGE

zk-budget   2               1                     2h

zk-budget 指示集群中必须始终至少有两个成员可用,集群才能正常运行。如果您尝试在使节点脱机之前耗尽节点,并且如果耗尽节点会终止违反预算的 Pod,则耗尽操作将失败。如果您将 kubectl drain 与 PodDisruptionBudgets 结合使用,以隔离节点并在维护或退役之前逐出所有 Pod,则可以确保该过程不会对您的有状态应用程序造成破坏。

展望未来

随着 Kubernetes 开发迈向 GA(正式发布),我们正在审视用户提出的长长一列建议。如果您想深入了解我们的待办事项,请查看 GitHub 上带有 stateful 标签的问题。然而,由于最终的 API 可能难以理解,我们不期望实现所有这些功能请求。一些功能请求,例如对滚动更新的支持、与节点升级的更好集成以及使用快速本地存储,将使大多数有状态应用程序受益,我们预计将优先考虑这些。StatefulSet 的目标是能够良好地运行大量应用程序,而不是完美地运行所有应用程序。考虑到这一点,我们避免以依赖隐藏机制或无法访问的功能的方式实现 StatefulSet。任何人都可以编写一个与 StatefulSet 类似工作的控制器。我们称之为“使其可分叉”。

在接下来的一年中,我们预计许多流行的存储应用程序都将拥有各自的社区支持的、专门的控制器或 “操作符”。我们已经听说了针对 etcd、Redis 和 ZooKeeper 的自定义控制器的工作。我们预计自己会编写更多,并支持社区开发其他控制器。

CoreOS 的 etcdPrometheus 操作符,展示了一种在 Kubernetes 上运行有状态应用程序的方法,该方法提供的自动化和集成程度超出了仅使用 StatefulSet 所能实现的范围。另一方面,使用像 StatefulSet 或 Deployment 这样的通用控制器意味着可以通过理解单个配置对象来管理各种各样的应用程序。我们认为 Kubernetes 用户会喜欢这两种方法的选择。