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

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 上运行分布式、强一致性的存储应用。其次,它是在 Kubernetes 上运行 Apache HadoopApache Kakfa 等工作负载的先决条件。在 Kubernetes 文档中提供了关于在 Kubernetes 上部署 ZooKeeper 集群的深度教程,我们将在下面概述一些关键特性。

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

$ 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

当您创建 manifest 时,StatefulSet 控制器会根据 Pod 的序号创建每个 Pod,并在创建下一个 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 服务器的标识符只是自然数。对于集群中的服务器,“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 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

级联删除会按照 Pod 序号的逆序销毁 StatefulSet 中的每个 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 Scheduler 可能会将 zk StatefulSet 中的多个 Pod 部署到同一个节点上。如果 zk-0 和 zk-1 Pod 部署在同一个节点上并且该节点发生故障,ZooKeeper 集群将无法形成多数派来提交写入,并且 ZooKeeper 服务会经历中断,直到其中一个 Pod 可以被重新调度。

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

如果您的服务的 SLA 不允许因单个节点故障而导致短暂中断,则应使用 PodAntiAffinity 注解。用于创建集群的 manifest 中包含此注解,它会告诉 Kubernetes Scheduler 不要将 zk StatefulSet 中的多个 Pod 放在同一个节点上。

容忍计划维护

用于创建 ZooKeeper 集群的 manifest 还创建了一个 PodDistruptionBudget,即 zk-budget。zk-budget 告知 Kubernetes 该服务可以容忍的(不健康 Pods)中断的上限。

 {

              "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 表示,集群至少需要有两个成员始终可用才能保持健康。如果您尝试在节点下线之前 drain 它,并且 drain 操作会导致终止违反预算的 Pod,则 drain 操作将失败。如果您结合 PodDisruptionBudgets 使用 kubectl drain,在维护或退役之前 cordon 您的节点并驱逐所有 Pod,您可以确保该过程不会对您的有状态应用造成干扰。

展望未来

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

在未来一年,我们预计许多流行的存储应用将拥有自己的社区支持的专用控制器或“operators”。我们已经听说有人正在开发 etcd、Redis 和 ZooKeeper 的自定义控制器。我们期望自己编写一些,并支持社区开发其他控制器。

CoreOS 的 etcdPrometheus Operator 演示了一种在 Kubernetes 上运行有状态应用的方法,该方法提供的自动化和集成水平超出了 StatefulSet 单独所能实现的。另一方面,使用像 StatefulSet 或 Deployment 这样的通用控制器意味着可以通过理解单个配置对象来管理各种应用。我们认为 Kubernetes 用户会乐于拥有这两种方法的选择。