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

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

编者按:这篇博文是关于 Kubernetes 1.5 新功能系列深度文章的一部分。

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

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

部署(Deployments)副本集(ReplicaSets)是在 Kubernetes 上运行应用程序无状态副本的绝佳方式,但它们的语义并不完全适用于部署有状态应用程序。StatefulSet 的目的是提供一个具有正确语义的控制器,用于部署各种有状态工作负载。然而,将存储应用程序迁移到 Kubernetes 并不总是正确的选择。在您全力以赴地整合存储层和编排框架之前,您应该问自己几个问题。

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

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

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

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

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

可预测的性能有多重要?

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

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

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

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

一个实际的例子 - ZooKeeper

ZooKeeper 是 StatefulSet 的一个有趣的用例,原因有二。首先,它证明 StatefulSet 可以用于在 Kubernetes 上运行分布式、强一致性存储应用程序。其次,它是在 Kubernetes 上运行像 Apache HadoopApache Kafka 这样的工作负载的先决条件。在 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 状态,然后才创建其后续 Pod。

$ 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 服务器的标识符只是自然数。对于集群中的服务器,“myid”文件通过将从 Pod 主机名中提取的序号加一来填充。

$ 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 无头服务控制的网络域的唯一网络地址。

$  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

级联删除会根据 Pod 序号的相反顺序销毁 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 集群的清单还会创建 PodDistruptionBudget (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 与 PodDisruptionBudget 结合使用,在维护或退役之前隔离节点并驱逐所有 Pod,您可以确保该过程不会对您的有状态应用程序造成干扰。

展望未来

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

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

来自 CoreOS 的 etcdPrometheus 的操作员展示了一种在 Kubernetes 上运行有状态应用程序的方法,该方法提供了比单独使用 StatefulSet 更高的自动化和集成水平。另一方面,使用像 StatefulSet 或 Deployment 这样的通用控制器意味着可以通过理解单个配置对象来管理各种应用程序。我们认为 Kubernetes 用户会欣赏这两种方法的选择。