# StatefulSet StatefulSet 作为 Controller 为 Pod 提供唯一的标识。它可以保证部署和 scale 的顺序。 使用案例参考:[kubernetes contrib - statefulsets](https://github.com/kubernetes/contrib/tree/master/statefulsets),其中包含zookeeper和kakfa的statefulset设置和使用说明。 StatefulSet是为了解决有状态服务的问题(对应Deployments和ReplicaSets是为无状态服务而设计),其应用场景包括: - 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现 - 稳定的网络标志,即Pod重新调度后其PodName和HostName不变,基于Headless Service(即没有Cluster IP的Service)来实现 - 有序部署,有序扩展,即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0到N-1,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态),基于init containers来实现 - 有序收缩,有序删除(即从N-1到0) 从上面的应用场景可以发现,StatefulSet由以下几个部分组成: - 用于定义网络标志(DNS domain)的Headless Service - 用于创建PersistentVolumes的volumeClaimTemplates - 定义具体应用的StatefulSet StatefulSet中每个Pod的DNS格式为`statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local`,其中 - `serviceName`为Headless Service的名字 - `0..N-1`为Pod所在的序号,从0开始到N-1 - `statefulSetName`为StatefulSet的名字 - `namespace`为服务所在的namespace,Headless Servic和StatefulSet必须在相同的namespace - `.cluster.local`为Cluster Domain ## 使用 StatefulSet StatefulSet 适用于有以下某个或多个需求的应用: - 稳定,唯一的网络标志。 - 稳定,持久化存储。 - 有序,优雅地部署和 scale。 - 有序,优雅地删除和终止。 - 有序,自动的滚动升级。 在上文中,稳定是 Pod (重新)调度中持久性的代名词。 如果应用程序不需要任何稳定的标识符、有序部署、删除和 scale,则应该使用提供一组无状态副本的 controller 来部署应用程序,例如 [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment) 或 [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset) 可能更适合您的无状态需求。 ## 限制 - StatefulSet 是 beta 资源,Kubernetes 1.5 以前版本不支持。 - 对于所有的 alpha/beta 的资源,您都可以通过在 apiserver 中设置 `--runtime-config` 选项来禁用。 - 给定 Pod 的存储必须由 PersistentVolume Provisioner 根据请求的 `storage class` 进行配置,或由管理员预先配置。 - 删除或 scale StatefulSet 将_不会_删除与 StatefulSet 相关联的 volume。 这样做是为了确保数据安全性,这通常比自动清除所有相关 StatefulSet 资源更有价值。 - StatefulSets 目前要求 [Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) 负责 Pod 的网络身份。 您有责任创建此服务。 ## 组件 下面的示例中描述了 StatefulSet 中的组件。 - 一个名为 nginx 的 headless service,用于控制网络域。 - 一个名为 web 的 StatefulSet,它的 Spec 中指定在有 3 个运行 nginx 容器的 Pod。 - volumeClaimTemplates 使用 PersistentVolume Provisioner 提供的 [PersistentVolumes](https://kubernetes.io/docs/concepts/storage/volumes) 作为稳定存储。 ```yaml apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx --- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: web spec: serviceName: "nginx" replicas: 3 template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: gcr.io/google_containers/nginx-slim:0.8 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www annotations: volume.beta.kubernetes.io/storage-class: anything spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi ``` ## Pod 身份 StatefulSet Pod 具有唯一的身份,包括序数,稳定的网络身份和稳定的存储。 身份绑定到 Pod 上,不管它(重新)调度到哪个节点上。 ### 序数 对于一个有 N 个副本的 StatefulSet,每个副本都会被指定一个整数序数,在 [0,N)之间,且唯一。 ## 稳定的网络 ID StatefulSet 中的每个 Pod 从 StatefulSet 的名称和 Pod 的序数派生其主机名。构造的主机名的模式是`$(statefulset名称)-$(序数)`。 上面的例子将创建三个名为`web-0,web-1,web-2`的 Pod。 StatefulSet 可以使用 [Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) 来控制其 Pod 的域。此服务管理的域的格式为:`$(服务名称).$(namespace).svc.cluster.local`,其中 “cluster.local” 是集群域。 在创建每个Pod时,它将获取一个匹配的 DNS 子域,采用以下形式:`$(pod 名称).$(管理服务域)`,其中管理服务由 StatefulSet 上的 `serviceName` 字段定义。 以下是 Cluster Domain,服务名称,StatefulSet 名称以及如何影响 StatefulSet 的 Pod 的 DNS 名称的一些示例。 | Cluster Domain | Service (ns/name) | StatefulSet (ns/name) | StatefulSet Domain | Pod DNS | Pod Hostname | | -------------- | ----------------- | --------------------- | ------------------------------- | ---------------------------------------- | ------------ | | cluster.local | default/nginx | default/web | nginx.default.svc.cluster.local | web-{0..N-1}.nginx.default.svc.cluster.local | web-{0..N-1} | | cluster.local | foo/nginx | foo/web | nginx.foo.svc.cluster.local | web-{0..N-1}.nginx.foo.svc.cluster.local | web-{0..N-1} | | kube.local | foo/nginx | foo/web | nginx.foo.svc.kube.local | web-{0..N-1}.nginx.foo.svc.kube.local | web-{0..N-1} | 注意 Cluster Domain 将被设置成 `cluster.local` 除非进行了其他配置。 ### 稳定存储 Kubernetes 为每个 VolumeClaimTemplate 创建一个 [PersistentVolume](https://kubernetes.io/docs/concepts/storage/volumes)。上面的 nginx 的例子中,每个 Pod 将具有一个由 `anything` 存储类创建的 1 GB 存储的 PersistentVolume。当该 Pod (重新)调度到节点上,`volumeMounts` 将挂载与 PersistentVolume Claim 相关联的 PersistentVolume。请注意,与 PersistentVolume Claim 相关联的 PersistentVolume 在 产出 Pod 或 StatefulSet 的时候不会被删除。这必须手动完成。 ## 部署和 Scale 保证 - 对于有 N 个副本的 StatefulSet,Pod 将按照 {0..N-1} 的顺序被创建和部署。 - 当 删除 Pod 的时候,将按照逆序来终结,从{N-1..0} - 对 Pod 执行 scale 操作之前,它所有的前任必须处于 Running 和 Ready 状态。 - 在终止 Pod 前,它所有的继任者必须处于完全关闭状态。 不应该将 StatefulSet 的 `pod.Spec.TerminationGracePeriodSeconds` 设置为 0。这样是不安全的且强烈不建议您这样做。进一步解释,请参阅 [强制删除 StatefulSet Pod](https://kubernetes.io/docs/tasks/run-application/force-delete-stateful-set-pod)。 上面的 nginx 示例创建后,3 个 Pod 将按照如下顺序创建 web-0,web-1,web-2。在 web-0 处于 [运行并就绪](https://kubernetes.io/docs/user-guide/pod-states) 状态之前,web-1 将不会被部署,同样当 web-1 处于运行并就绪状态之前 web-2也不会被部署。如果在 web-1 运行并就绪后,web-2 启动之前, web-0 失败了,web-2 将不会启动,直到 web-0 成功重启并处于运行并就绪状态。 如果用户通过修补 StatefulSet 来 scale 部署的示例,以使 `replicas=1`,则 web-2 将首先被终止。 在 web-2 完全关闭和删除之前,web-1 不会被终止。 如果 web-0 在 web-2 终止并且完全关闭之后,但是在 web-1 终止之前失败,则 web-1 将不会终止,除非 web-0 正在运行并准备就绪。 ### Pod 管理策略 在 Kubernetes 1.7 和之后版本,StatefulSet 允许您放开顺序保证,同时通过 `.spec.podManagementPolicy` 字段保证身份的唯一性。 #### OrderedReady Pod 管理 StatefulSet 中默认使用的是 `OrderedReady` pod 管理。它实现了 [如上](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset.md#deployment-and-scaling-guarantees) 所述的行为。 #### 并行 Pod 管理 `Parallel` pod 管理告诉 StatefulSet controller 并行的启动和终止 Pod,在启动和终止其他 Pod 之前不会等待 Pod 变成 运行并就绪或完全终止状态。 ## 更新策略 在 kubernetes 1.7 和以上版本中,StatefulSet 的 `.spec.updateStrategy` 字段允许您配置和禁用 StatefulSet 中的容器、label、resource request/limit、annotation 的滚动更新。 ### 删除 `OnDelete` 更新策略实现了遗留(1.6和以前)的行为。 当 `spec.updateStrategy` 未指定时,这是默认策略。 当StatefulSet 的 `.spec.updateStrategy.type` 设置为 `OnDelete` 时,StatefulSet 控制器将不会自动更新 `StatefulSet` 中的 Pod。 用户必须手动删除 Pod 以使控制器创建新的 Pod,以反映对StatefulSet的 `.spec.template` 进行的修改。 ### 滚动更新 `RollingUpdate` 更新策略在 StatefulSet 中实现 Pod 的自动滚动更新。 当StatefulSet的 `.spec.updateStrategy.type` 设置为 `RollingUpdate` 时,StatefulSet 控制器将在 StatefulSet 中删除并重新创建每个 Pod。 它将以与 Pod 终止相同的顺序进行(从最大的序数到最小的序数),每次更新一个 Pod。 在更新其前身之前,它将等待正在更新的 Pod 状态变成正在运行并就绪。 #### 分区 可以通过指定 `.spec.updateStrategy.rollingUpdate.partition` 来对 `RollingUpdate` 更新策略进行分区。如果指定了分区,则当 StatefulSet 的 `.spec.template` 更新时,具有大于或等于分区序数的所有 Pod 将被更新。具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 `.spec.updateStrategy.rollingUpdate.partition` 大于其 `.spec.replicas`,则其 `.spec.template` 的更新将不会传播到 Pod。 在大多数情况下,您不需要使用分区,但如果您想要进行分阶段更新,使用金丝雀发布或执行分阶段发布,它们将非常有用。 ## 简单示例 以一个简单的nginx服务[web.yaml](https://github.com/rootsongjc/kubernetes-handbook/blob/master/manifests/test/web.yaml)为例: ```yaml --- apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx --- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: web spec: serviceName: "nginx" replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: gcr.io/google_containers/nginx-slim:0.8 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www annotations: volume.alpha.kubernetes.io/storage-class: anything spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi ``` ```sh $ kubectl create -f web.yaml service "nginx" created statefulset "web" created # 查看创建的headless service和statefulset $ kubectl get service nginx NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx None 80/TCP 1m $ kubectl get statefulset web NAME DESIRED CURRENT AGE web 2 2 2m # 根据volumeClaimTemplates自动创建PVC(在GCE中会自动创建kubernetes.io/gce-pd类型的volume) $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESSMODES AGE www-web-0 Bound pvc-d064a004-d8d4-11e6-b521-42010a800002 1Gi RWO 16s www-web-1 Bound pvc-d06a3946-d8d4-11e6-b521-42010a800002 1Gi RWO 16s # 查看创建的Pod,他们都是有序的 $ kubectl get pods -l app=nginx NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 5m web-1 1/1 Running 0 4m # 使用nslookup查看这些Pod的DNS $ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh / # nslookup web-0.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx Address 1: 10.244.2.10 / # nslookup web-1.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-1.nginx Address 1: 10.244.3.12 / # nslookup web-0.nginx.default.svc.cluster.local Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx.default.svc.cluster.local Address 1: 10.244.2.10 ``` 还可以进行其他的操作 ```sh # 扩容 $ kubectl scale statefulset web --replicas=5 # 缩容 $ kubectl patch statefulset web -p '{"spec":{"replicas":3}}' # 镜像更新(目前还不支持直接更新image,需要patch来间接实现) $ kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"gcr.io/google_containers/nginx-slim:0.7"}]' # 删除StatefulSet和Headless Service $ kubectl delete statefulset web $ kubectl delete service nginx # StatefulSet删除后PVC还会保留着,数据不再使用的话也需要删除 $ kubectl delete pvc www-web-0 www-web-1 ``` ## zookeeper 另外一个更能说明StatefulSet强大功能的示例为[zookeeper.yaml](https://github.com/rootsongjc/kubernetes-handbook/blob/master/manifests/test/zookeeper.yaml),这个例子仅为讲解,实际可用的配置请使用 https://github.com/kubernetes/contrib/tree/master/statefulsets 中的配置。 ```yaml --- apiVersion: v1 kind: Service metadata: name: zk-headless labels: app: zk-headless spec: ports: - port: 2888 name: server - port: 3888 name: leader-election clusterIP: None selector: app: zk --- apiVersion: v1 kind: ConfigMap metadata: name: zk-config data: ensemble: "zk-0;zk-1;zk-2" jvm.heap: "2G" tick: "2000" init: "10" sync: "5" client.cnxns: "60" snap.retain: "3" purge.interval: "1" --- apiVersion: policy/v1beta1 kind: PodDisruptionBudget metadata: name: zk-budget spec: selector: matchLabels: app: zk minAvailable: 2 --- apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: zk spec: serviceName: zk-headless replicas: 3 template: metadata: labels: app: zk annotations: pod.alpha.kubernetes.io/initialized: "true" scheduler.alpha.kubernetes.io/affinity: > { "podAntiAffinity": { "requiredDuringSchedulingRequiredDuringExecution": [{ "labelSelector": { "matchExpressions": [{ "key": "app", "operator": "In", "values": ["zk-headless"] }] }, "topologyKey": "kubernetes.io/hostname" }] } } spec: containers: - name: k8szk imagePullPolicy: Always image: gcr.io/google_samples/k8szk:v1 resources: requests: memory: "4Gi" cpu: "1" ports: - containerPort: 2181 name: client - containerPort: 2888 name: server - containerPort: 3888 name: leader-election env: - name : ZK_ENSEMBLE valueFrom: configMapKeyRef: name: zk-config key: ensemble - name : ZK_HEAP_SIZE valueFrom: configMapKeyRef: name: zk-config key: jvm.heap - name : ZK_TICK_TIME valueFrom: configMapKeyRef: name: zk-config key: tick - name : ZK_INIT_LIMIT valueFrom: configMapKeyRef: name: zk-config key: init - name : ZK_SYNC_LIMIT valueFrom: configMapKeyRef: name: zk-config key: tick - name : ZK_MAX_CLIENT_CNXNS valueFrom: configMapKeyRef: name: zk-config key: client.cnxns - name: ZK_SNAP_RETAIN_COUNT valueFrom: configMapKeyRef: name: zk-config key: snap.retain - name: ZK_PURGE_INTERVAL valueFrom: configMapKeyRef: name: zk-config key: purge.interval - name: ZK_CLIENT_PORT value: "2181" - name: ZK_SERVER_PORT value: "2888" - name: ZK_ELECTION_PORT value: "3888" command: - sh - -c - zkGenConfig.sh && zkServer.sh start-foreground readinessProbe: exec: command: - "zkOk.sh" initialDelaySeconds: 15 timeoutSeconds: 5 livenessProbe: exec: command: - "zkOk.sh" initialDelaySeconds: 15 timeoutSeconds: 5 volumeMounts: - name: datadir mountPath: /var/lib/zookeeper securityContext: runAsUser: 1000 fsGroup: 1000 volumeClaimTemplates: - metadata: name: datadir annotations: volume.alpha.kubernetes.io/storage-class: anything spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 20Gi ``` ```sh kubectl create -f zookeeper.yaml ``` 详细的使用说明见[zookeeper stateful application](https://kubernetes.io/docs/tutorials/stateful-application/zookeeper/)。 关于StatefulSet的更多示例请参阅 [github.com/kubernetes/contrib - statefulsets](https://github.com/kubernetes/contrib/tree/master/statefulsets),其中包括了zookeeper和kafka。 ## 集群外部访问StatefulSet的Pod 我们设想一下这样的场景:在kubernetes集群外部调试StatefulSet中有序的Pod,那么如何访问这些的pod呢? 方法是为pod设置label,然后用`kubectl expose`将其以NodePort的方式暴露到集群外部,以上面的zookeeper的例子来说明,下面使用命令的方式来暴露其中的两个zookeeper节点,也可以写一个serivce配置yaml文件。 ```bash kubectl label pod zk-0 zkInst=0 kubectl label pod zk-1 zkInst=1 kubectl expose po zk-0 --port=2181 --target-port=2181 --name=zk-0 --selector=zkInst=0 --type=NodePort kubectl expose po zk-1 --port=2181 --target-port=2181 --name=zk-1 --selector=zkInst=1 --type=NodePort ``` 这样在kubernetes集群外部就可以根据pod所在的主机所映射的端口来访问了。 查看`zk-0`这个service可以看到如下结果: ```bash NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE zk-0 10.254.98.14 2181:31693/TCP 5m ``` 集群外部就可以使用所有的node中的任何一个IP:31693来访问这个zookeeper实例。 ## 参考 - https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ - [kubernetes contrib - statefulsets](https://github.com/kubernetes/contrib/tree/master/statefulsets)