StatefulSet

StatefulSet 作为 Controller 为 Pod 提供唯一的标识。它可以保证部署和 scale 的顺序。

使用案例参考:kubernetes contrib - 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 来部署应用程序,例如 DeploymentReplicaSet 可能更适合您的无状态需求。

限制

  • StatefulSet 是 beta 资源,Kubernetes 1.5 以前版本不支持。
  • 对于所有的 alpha/beta 的资源,您都可以通过在 apiserver 中设置 --runtime-config 选项来禁用。
  • 给定 Pod 的存储必须由 PersistentVolume Provisioner 根据请求的 storage class 进行配置,或由管理员预先配置。
  • 删除或 scale StatefulSet 将不会删除与 StatefulSet 相关联的 volume。 这样做是为了确保数据安全性,这通常比自动清除所有相关 StatefulSet 资源更有价值。
  • StatefulSets 目前要求 Headless Service 负责 Pod 的网络身份。 您有责任创建此服务。

组件

下面的示例中描述了 StatefulSet 中的组件。

  • 一个名为 nginx 的 headless service,用于控制网络域。
  • 一个名为 web 的 StatefulSet,它的 Spec 中指定在有 3 个运行 nginx 容器的 Pod。
  • volumeClaimTemplates 使用 PersistentVolume Provisioner 提供的 PersistentVolumes 作为稳定存储。
  1. apiVersion: v1
  2. kind: Service
  3. metadata:
  4. name: nginx
  5. labels:
  6. app: nginx
  7. spec:
  8. ports:
  9. - port: 80
  10. name: web
  11. clusterIP: None
  12. selector:
  13. app: nginx
  14. ---
  15. apiVersion: apps/v1beta1
  16. kind: StatefulSet
  17. metadata:
  18. name: web
  19. spec:
  20. serviceName: "nginx"
  21. replicas: 3
  22. template:
  23. metadata:
  24. labels:
  25. app: nginx
  26. spec:
  27. terminationGracePeriodSeconds: 10
  28. containers:
  29. - name: nginx
  30. image: gcr.io/google_containers/nginx-slim:0.8
  31. ports:
  32. - containerPort: 80
  33. name: web
  34. volumeMounts:
  35. - name: www
  36. mountPath: /usr/share/nginx/html
  37. volumeClaimTemplates:
  38. - metadata:
  39. name: www
  40. annotations:
  41. volume.beta.kubernetes.io/storage-class: anything
  42. spec:
  43. accessModes: [ "ReadWriteOnce" ]
  44. resources:
  45. requests:
  46. 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 来控制其 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。上面的 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

上面的 nginx 示例创建后,3 个 Pod 将按照如下顺序创建 web-0,web-1,web-2。在 web-0 处于 运行并就绪 状态之前,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 管理。它实现了 如上 所述的行为。

并行 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为例:

  1. ---
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: nginx
  6. labels:
  7. app: nginx
  8. spec:
  9. ports:
  10. - port: 80
  11. name: web
  12. clusterIP: None
  13. selector:
  14. app: nginx
  15. ---
  16. apiVersion: apps/v1beta1
  17. kind: StatefulSet
  18. metadata:
  19. name: web
  20. spec:
  21. serviceName: "nginx"
  22. replicas: 2
  23. template:
  24. metadata:
  25. labels:
  26. app: nginx
  27. spec:
  28. containers:
  29. - name: nginx
  30. image: gcr.io/google_containers/nginx-slim:0.8
  31. ports:
  32. - containerPort: 80
  33. name: web
  34. volumeMounts:
  35. - name: www
  36. mountPath: /usr/share/nginx/html
  37. volumeClaimTemplates:
  38. - metadata:
  39. name: www
  40. annotations:
  41. volume.alpha.kubernetes.io/storage-class: anything
  42. spec:
  43. accessModes: [ "ReadWriteOnce" ]
  44. resources:
  45. requests:
  46. storage: 1Gi
  1. $ kubectl create -f web.yaml
  2. service "nginx" created
  3. statefulset "web" created
  4. # 查看创建的headless service和statefulset
  5. $ kubectl get service nginx
  6. NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  7. nginx None <none> 80/TCP 1m
  8. $ kubectl get statefulset web
  9. NAME DESIRED CURRENT AGE
  10. web 2 2 2m
  11. # 根据volumeClaimTemplates自动创建PVC(在GCE中会自动创建kubernetes.io/gce-pd类型的volume)
  12. $ kubectl get pvc
  13. NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
  14. www-web-0 Bound pvc-d064a004-d8d4-11e6-b521-42010a800002 1Gi RWO 16s
  15. www-web-1 Bound pvc-d06a3946-d8d4-11e6-b521-42010a800002 1Gi RWO 16s
  16. # 查看创建的Pod,他们都是有序的
  17. $ kubectl get pods -l app=nginx
  18. NAME READY STATUS RESTARTS AGE
  19. web-0 1/1 Running 0 5m
  20. web-1 1/1 Running 0 4m
  21. # 使用nslookup查看这些Pod的DNS
  22. $ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
  23. / # nslookup web-0.nginx
  24. Server: 10.0.0.10
  25. Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
  26. Name: web-0.nginx
  27. Address 1: 10.244.2.10
  28. / # nslookup web-1.nginx
  29. Server: 10.0.0.10
  30. Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
  31. Name: web-1.nginx
  32. Address 1: 10.244.3.12
  33. / # nslookup web-0.nginx.default.svc.cluster.local
  34. Server: 10.0.0.10
  35. Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
  36. Name: web-0.nginx.default.svc.cluster.local
  37. Address 1: 10.244.2.10

还可以进行其他的操作

  1. # 扩容
  2. $ kubectl scale statefulset web --replicas=5
  3. # 缩容
  4. $ kubectl patch statefulset web -p '{"spec":{"replicas":3}}'
  5. # 镜像更新(目前还不支持直接更新image,需要patch来间接实现)
  6. $ 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"}]'
  7. # 删除StatefulSet和Headless Service
  8. $ kubectl delete statefulset web
  9. $ kubectl delete service nginx
  10. # StatefulSet删除后PVC还会保留着,数据不再使用的话也需要删除
  11. $ kubectl delete pvc www-web-0 www-web-1

zookeeper

另外一个更能说明StatefulSet强大功能的示例为zookeeper.yaml,这个例子仅为讲解,实际可用的配置请使用 https://github.com/kubernetes/contrib/tree/master/statefulsets 中的配置。

  1. ---
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: zk-headless
  6. labels:
  7. app: zk-headless
  8. spec:
  9. ports:
  10. - port: 2888
  11. name: server
  12. - port: 3888
  13. name: leader-election
  14. clusterIP: None
  15. selector:
  16. app: zk
  17. ---
  18. apiVersion: v1
  19. kind: ConfigMap
  20. metadata:
  21. name: zk-config
  22. data:
  23. ensemble: "zk-0;zk-1;zk-2"
  24. jvm.heap: "2G"
  25. tick: "2000"
  26. init: "10"
  27. sync: "5"
  28. client.cnxns: "60"
  29. snap.retain: "3"
  30. purge.interval: "1"
  31. ---
  32. apiVersion: policy/v1beta1
  33. kind: PodDisruptionBudget
  34. metadata:
  35. name: zk-budget
  36. spec:
  37. selector:
  38. matchLabels:
  39. app: zk
  40. minAvailable: 2
  41. ---
  42. apiVersion: apps/v1beta1
  43. kind: StatefulSet
  44. metadata:
  45. name: zk
  46. spec:
  47. serviceName: zk-headless
  48. replicas: 3
  49. template:
  50. metadata:
  51. labels:
  52. app: zk
  53. annotations:
  54. pod.alpha.kubernetes.io/initialized: "true"
  55. scheduler.alpha.kubernetes.io/affinity: >
  56. {
  57. "podAntiAffinity": {
  58. "requiredDuringSchedulingRequiredDuringExecution": [{
  59. "labelSelector": {
  60. "matchExpressions": [{
  61. "key": "app",
  62. "operator": "In",
  63. "values": ["zk-headless"]
  64. }]
  65. },
  66. "topologyKey": "kubernetes.io/hostname"
  67. }]
  68. }
  69. }
  70. spec:
  71. containers:
  72. - name: k8szk
  73. imagePullPolicy: Always
  74. image: gcr.io/google_samples/k8szk:v1
  75. resources:
  76. requests:
  77. memory: "4Gi"
  78. cpu: "1"
  79. ports:
  80. - containerPort: 2181
  81. name: client
  82. - containerPort: 2888
  83. name: server
  84. - containerPort: 3888
  85. name: leader-election
  86. env:
  87. - name : ZK_ENSEMBLE
  88. valueFrom:
  89. configMapKeyRef:
  90. name: zk-config
  91. key: ensemble
  92. - name : ZK_HEAP_SIZE
  93. valueFrom:
  94. configMapKeyRef:
  95. name: zk-config
  96. key: jvm.heap
  97. - name : ZK_TICK_TIME
  98. valueFrom:
  99. configMapKeyRef:
  100. name: zk-config
  101. key: tick
  102. - name : ZK_INIT_LIMIT
  103. valueFrom:
  104. configMapKeyRef:
  105. name: zk-config
  106. key: init
  107. - name : ZK_SYNC_LIMIT
  108. valueFrom:
  109. configMapKeyRef:
  110. name: zk-config
  111. key: tick
  112. - name : ZK_MAX_CLIENT_CNXNS
  113. valueFrom:
  114. configMapKeyRef:
  115. name: zk-config
  116. key: client.cnxns
  117. - name: ZK_SNAP_RETAIN_COUNT
  118. valueFrom:
  119. configMapKeyRef:
  120. name: zk-config
  121. key: snap.retain
  122. - name: ZK_PURGE_INTERVAL
  123. valueFrom:
  124. configMapKeyRef:
  125. name: zk-config
  126. key: purge.interval
  127. - name: ZK_CLIENT_PORT
  128. value: "2181"
  129. - name: ZK_SERVER_PORT
  130. value: "2888"
  131. - name: ZK_ELECTION_PORT
  132. value: "3888"
  133. command:
  134. - sh
  135. - -c
  136. - zkGenConfig.sh && zkServer.sh start-foreground
  137. readinessProbe:
  138. exec:
  139. command:
  140. - "zkOk.sh"
  141. initialDelaySeconds: 15
  142. timeoutSeconds: 5
  143. livenessProbe:
  144. exec:
  145. command:
  146. - "zkOk.sh"
  147. initialDelaySeconds: 15
  148. timeoutSeconds: 5
  149. volumeMounts:
  150. - name: datadir
  151. mountPath: /var/lib/zookeeper
  152. securityContext:
  153. runAsUser: 1000
  154. fsGroup: 1000
  155. volumeClaimTemplates:
  156. - metadata:
  157. name: datadir
  158. annotations:
  159. volume.alpha.kubernetes.io/storage-class: anything
  160. spec:
  161. accessModes: [ "ReadWriteOnce" ]
  162. resources:
  163. requests:
  164. storage: 20Gi
  1. kubectl create -f zookeeper.yaml

详细的使用说明见zookeeper stateful application

关于StatefulSet的更多示例请参阅 github.com/kubernetes/contrib - statefulsets,其中包括了zookeeper和kafka。

集群外部访问StatefulSet的Pod

我们设想一下这样的场景:在kubernetes集群外部调试StatefulSet中有序的Pod,那么如何访问这些的pod呢?

方法是为pod设置label,然后用kubectl expose将其以NodePort的方式暴露到集群外部,以上面的zookeeper的例子来说明,下面使用命令的方式来暴露其中的两个zookeeper节点,也可以写一个serivce配置yaml文件。

  1. kubectl label pod zk-0 zkInst=0
  2. kubectl label pod zk-1 zkInst=1
  3. kubectl expose po zk-0 --port=2181 --target-port=2181 --name=zk-0 --selector=zkInst=0 --type=NodePort
  4. kubectl expose po zk-1 --port=2181 --target-port=2181 --name=zk-1 --selector=zkInst=1 --type=NodePort

这样在kubernetes集群外部就可以根据pod所在的主机所映射的端口来访问了。

查看zk-0这个service可以看到如下结果:

  1. NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
  2. zk-0 10.254.98.14 <nodes> 2181:31693/TCP 5m

集群外部就可以使用所有的node中的任何一个IP:31693来访问这个zookeeper实例。

参考

https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/

kubernetes contrib - statefulsets