# StatefulSet: Kubernetes 中对有状态应用的运行和伸缩

原文：[StatefulSet: Run and Scale Stateful Applications Easily in Kubernetes](https://kubernetes.io/blog/2016/12/statefulset-run-scale-stateful-applications-in-kubernetes)

在最新发布的 [Kubernetes 1.5](http://blog.kubernetes.io/2016/12/kubernetes-1.5-supporting-production-workloads.html) 我们将过去的 PetSet 功能升级到了 Beta 版本，并重新命名为 [StatefulSet](http://kubernetes.io/docs/concepts/abstractions/controllers/statefulsets/)。除了依照社区民意改了名字之外，这一 API 对象并没有太大变化，不过我们在向集合里部署 Pod 的过程中加入了“每索引最多一个”的语义。有了顺序部署、顺序终结、唯一网络名称以及持久稳定的存储，我们认为，对于大量的有状态容器化负载，我们已经具备了一定的支持能力。我们并不是宣称这一功能已经完全完成，但是我们相信他已经处于一个可用状态，并且我们会在推动其正式发布的过程中保持其兼容性。

## StatefulSet 的采用时机

在 Kubernetes 中，[Deployment](http://kubernetes.io/docs/user-guide/deployments/) 和 [ReplicaSets](http://kubernetes.io/docs/user-guide/replicasets/) 都是运行无状态应用的有效手段。但这两种方式对于有状态应用来说就不太合适了。StatefulSet 的目的就是给为数众多的有状态负载提供正确的控制器支持。然而需要注意的是，不一定所有的有存储应用都是适合移植到 Kubernetes 上的，在移植存储层和编排框架之前，需要回答以下几个问题。

### 应用是否可以使用远程存储？

目前，我们推荐用远程存储来使用 StatefulSets，就要对因为网络造成的存储性能损失有一个准备：即使是专门优化的实例，也无法同本地加载的 SSD 相提并论。你的云中的网络存储，能够满足 SLA 要求么？如果答案是肯定的，那么利用 StatefulSet 运行这些应用，就能够获得自动化的优势。如果应用所在的 Node 发生故障，包含应用的 Pod 会调度到其他 Node 上，在这之后会重新加载他的网络存储以及其中的数据。

### 这些应用是否有伸缩需求？

用 StatefulSet 运行应用会带来什么好处呢？你的整个组织是否只需要一个应用实例？对该应用的伸缩是否会引起问题？如果你只需要较少的应用实例数量，这些实例能够满足组织现有的需要，而且可以预见的是，应用的负载不会很快增长，那么你的本地应用可能无需移植。

然而，如果你的系统是微服务所构成的生态系统，就会比较频繁的交付新服务，如果更近一步，服务是有状态的，那么 Kubernetes 的自动化和健壮性特性会对你的系统有很大帮助。如果你已经在使用 Kubernetes 来管理你的无状态服务，你可能会想要在同一个体系中管理你的有状态应用。

### 预期性能增长的重要性？

Kubernetes 还不支持网络或存储在 Pod 之间的隔离。如果你的应用不巧和嘈杂的邻居共享同一个节点，会导致你的 QPS 下降。解决方式是把 Pod 调度为该 Node 的唯一租户（独占服务器），或者使用互斥规则来隔离会争用网络和磁盘的 Pod，但是这就意味着用户必须鉴别和处置（竞争）热点。

如果榨干有状态应用的最大 QPS 不是你的首要目标，而且你愿意也有能力处理竞争问题，似的有状态应用能够达到 SLA 需要，又如果对服务的移植、伸缩和重新调度是你的主要需求，Kubernetes 和 StatefulSet 可能就是解决问题的好方案了。

### 你的应用是否需要特定的硬件或者实例类型

如果你的有状态应用在高端硬件或高规格实例上运行，而其他应用在通用硬件或者低规格实例上运行，你可能不想部署一个异构的集群。如果可以把所有应用都部署到统一实例规格的实例上，那么你就能够从 Kubernetes 获得动态资源调度和健壮性的好处。

## 实践环节 - ZooKeeper

有两个原因让 [ZooKeeper] 成为 StatefulSet 的好例子。首先，StatefulSet 在其中演示了运行分布式、强一致性存储的应用的能力；其次，ZooKeeper 也是 [Apache Hadoop](http://hadoop.apache.org/) 和 [Apache Kafka](https://kafka.apache.org/) 在 Kubernetes 上运行的前置条件。在 Kubernetes 文档中有一个 [深度教程](http://kubernetes.io/docs/tutorials/stateful-application/zookeeper/) 说明了在 Kubernetes 集群上部署 ZooKeeper Ensemble 的过程，这里会简要描述一下其中的关键特性。

### 创建 ZooKeeper 的 Ensemble

创建 Ensemble 很容易，只要用 [kubectl create](http://kubernetes.io/docs/user-guide/kubectl/kubectl_create/) 来根据定义来创建对象就可以了。

    $ kubectl create -f 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 之前，首先要等前面的 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" 的文件中保存了每个服务器的唯一标识符。这个标识符只是自然数。在 Ensemble 的服务器中，"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 服务所控制。

    $  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 的配置文件中设置 Ensemble 成员了。

    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 的服务器，服务器之间可以通过稳定的网络地址互相通信，因为 Ensemble 具有稳定的成员关系，因此 Leader 选拔和写入复制能力也得到了保障。

检查 Ensemble 工作状况的最简单方式就是向一台服务器写入一个值，然后从另一台服务器中读取。可以利用 ZooKeeper 自带的 "zkCli.sh" 脚本来创建包含数据的 ZNode。

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

    WATCHER::

    WatchedEvent state:SyncConnected type:None path:null
    Created /hello

使用同一脚本，可以从 Ensemble 另外一台服务器中读取数据。

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

    WATCHER::

    WatchedEvent state:SyncConnected type:None path:null
    world
    ...

可以用删除 zk StatefulSet 的方式停掉 Ensemble。

    $  kubectl delete statefulset zk
    statefulset "zk" deleted

级联删除会销毁 StatefulSet 中的每个 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](http://kubernetes.io/docs/user-guide/kubectl/kubectl_apply/) 命令来重建 zk StatefulSet，并重新部署 Ensemble。

    $  kubectl apply -f http://k8s.io/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
    ...

所有的 Pod 都被销毁，他们一旦被重新调度，StatefulSet 也能保证 Ensemble 能够选拔新的 Leader 并继续提供服务。

### Node 故障的容错

ZooKeeper 会在 Ensmble 的服务器中复制他的状态机，用于应对 Node 故障。缺省情况下 Kubernetes 调度器可以在同一个 Node 上部署属于 zk StatefulSet 的多个 Pod，假设 zk-0 和 zk-1 两个 Pod 被部署在同一个 Node 上，如果这一 Node 出现故障，ZooKeepers Ensemble 会因为数量不足造成无法提交写入，ZooKeeper 会出现服务中断，直到 Pod 被重新调度。

在集群中，建议为关键进程预留更多资源，这样就能保证故障情况发生的时候能够迅速重新调度 Pod，缩短故障时间。

如果这样无法 SLA 规定的停机时间，那么就应该使用 [PodAntiAffinity( Pod 互斥性)](http://kubernetes.io/docs/user-guide/node-selection/)注解。用来创建 Ensemble 的定义文件中就包含了这样的注解，他会要求 Kubernetes 调度器不要把 zk StatefulSet 中的多个 Pod 部署在同一 Node 上。

### 计划内维护的容错

用于创建 ZooKeeper Ensemble 的描述文件还创建了一个 [PodDistruptionBudget( Pod 中断预算 )](http://kubernetes.io/docs/admin/disruptions/) 对象：zk-budget。zk-budget 用于指示 Kubernetes， 这一服务能够容忍的中断 Pod （不健康 Pod）的上限。

~~~json
{
  "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 定义，至少要有两个处于可用状态的成员才能保障 Ensemble 的健康。如果在离线之前对 Node 进行 Drain 操作，如果这一操作过程中终止的 Pod 会违反预算，Drain 操作就会失败。如果使用 [kubectl drain](http://kubernetes.io/docs/user-guide/kubectl/kubectl_drain/)，来对 Node 进行 cordon 操作并驱逐所有其中运行的 Node，PodDistruption 让你可以确认这一操作不会中断有状态应用的服务。

## 更进一步

因为 Kubernetes 的开发工作目标是可用，我们希望获得更多来自用户的设想。如果你想要帮我们处理问题，可以看看 [GitHub 上关于 statful 的 Issues](https://github.com/kubernetes/kubernetes/labels/area%2Fstateful-apps)。然而为了 API 的易于理解，我们并不准备实现所有的功能请求。我们会优先实现一些能够对所有有状态应用产生改善的功能，例如滚动更新支持、Node 升级的集成、使用高速的本地存储等。StatefulSet 的目的在于支持尽可能多而不是全部的有状态应用。基于这些考虑，我们会避免依赖隐藏特性或者技术的方式来充实 StatefulSet。每个人都可以开发一个想 StatefulSets 的控制器。我们称之为 “making it forkable”。明年，我们希望更多的流行的有状态应用能够有自己的社区支持的独立控制器或 ["操作器"](https://coreos.com/blog/introducing-operators.html)。我们已经听说了 etcd、Redis 和 ZooKeeper 的自定义控制器的开发工作。我们期待更多类似案例不断涌现。

ETCD 和 Prometheus 的 Operator 来自 CoreOS，演示了一种超出 StatefulSet 能力的自动化和集成能力。另一方面，使用 Deployment 和 StatefulSet 这样的通用控制器能够用同一种对象管理大量有状态应用。Kubernetes 用户有了运行有状态应用的能力，而且可以自行在两种方式之中进行选择。
