UP | HOME

StatefulSet

Table of Contents

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

使用案例参考:https://github.com/kubernetes-retired/contrib/tree/master/statefulsets

其中包含zookeeper和kakfa的statefulset设置和使用说明

StatefulSet是为了解决 有状态服务 的问题(对应Deployments和ReplicaSets是为无状态服务而设计),其应用场景包括:

从上面的应用场景可以发现,StatefulSet由以下几个部分组成:

StatefulSet中每个Pod的 DNS格式 为 statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local ,其中

使用

StatefulSet 适用于有以下某个或多个需求的应用:

  • 稳定,唯一的网络标志
  • 稳定,持久化存储
  • 有序,优雅地部署和 scale
  • 有序,优雅地删除和终止
  • 有序,自动的滚动升级
稳定是 Pod (重新)调度中持久性的代名词,如果应用程序不需要任何稳定的标识符、有序部署、删除和 scale,则应该使用提供一组无状态副本的 controller 来部署应用程序

例如 Deployment 或 ReplicaSet 可能更适合您的无状态需求

限制

  • 给定 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 作为 稳定存储
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 来控制其 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  

稳定存储

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,这样是不安全的且强烈不建议这样做

上面的 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

以一个简单的nginx服务web.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
$ 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         <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

还可以进行其他的操作:

# 扩容
$ 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

---
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
这个例子仅为讲解,实际可用的配置请使用 https://github.com/kubernetes/contrib/tree/master/statefulsets 中的配置

集群外部访问StatefulSet的Pod

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

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

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可以看到如下结果:

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

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

Next:DaemonSet

Previous: Deployment

Home:Controller