30.StatefulSet

StatefulSet

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

使用案例参考:kubernetes contrib - statefulsets,其中包含zookeeperkakfastatefulset设置和使用说明。

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

  • 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现
  • 稳定的网络标志,即Pod重新调度后其PodNameHostName不变,基于Headless Service(即没有Cluster IPService)来实现
  • 有序部署,有序扩展,即Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次依次进行(即从0N-1,在下一个Pod运行之前所有之前的Pod必须都是RunningReady状态,基于init containers来实现
  • 有序收缩,有序删除(即从N-10

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

  • 用于定义网络标志(DNS domain)的Headless Service
  • 用于创建PersistentVolumesvolumeClaimTemplates
  • 定义具体应用的StatefulSet

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

  • serviceNameHeadless Service的名字
  • 0..N-1Pod所在的序号,从0开始到N-1
  • statefulSetNameStatefulSet的名字
  • namespace为服务所在的namespaceHeadless ServicStatefulSet必须在相同的namespace
  • .cluster.localCluster Domain

使用StatefulSet

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

  • 稳定,唯一的网络标志。
  • 稳定,持久化存储。
  • 有序,优雅地部署和scale
  • 有序,优雅地删除和终止。
  • 有序,自动的滚动升级。

在上文中,稳定是Pod (重新)调度中持久性的代名词。如果应用程序不需要任何稳定的标识符、有序部署、删除和scale,则应该使用提供一组无状态副本的controller来部署应用程序,例如 DeploymentReplicaSet 可能更适合您的无状态需求。

限制

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

组件

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

  • 一个名为nginxheadless service,用于控制网络域。
  • 一个名为webStatefulSet,它的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中的每个PodStatefulSet的名称和Pod的序数派生其主机名。构造的主机名的模式是$(statefulset名称)-$(序数)。上面的例子将创建三个名为web-0,web-1,web-2Pod

StatefulSet可以使用 Headless Service 来控制其Pod的域。此服务管理的域的格式为:$(服务名称).$(namespace).svc.cluster.local,其中 “cluster.local” 是集群域。

在创建每个Pod时,它将获取一个匹配的DNS子域,采用以下形式:$(pod 名称).$(管理服务域),其中管理服务由StatefulSet上的 serviceName 字段定义。

以下是Cluster Domain,服务名称,StatefulSet名称以及如何影响StatefulSetPodDNS名称的一些示例。

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在 产出PodStatefulSet的时候不会被删除。这必须手动完成。

部署和Scale保证

  • 对于有N个副本的StatefulSetPod将按照{0..N-1}的顺序被创建和部署。
  • 当 删除Pod的时候,将按照逆序来终结,从{N-1..0}
  • Pod执行scale操作之前,它所有的前任必须处于RunningReady状态。
  • 在终止Pod前,它所有的继任者必须处于完全关闭状态。

不应该将StatefulSetpod.Spec.TerminationGracePeriodSeconds 设置为0。这样是不安全的且强烈不建议您这样做。进一步解释,请参阅 强制删除StatefulSet Pod

上面的nginx示例创建后,3Pod将按照如下顺序创建web-0,web-1,web-2。在web-0处于 运行并就绪 状态之前,web-1将不会被部署,同样当web-1处于运行并就绪状态之前web-2也不会被部署。如果在web-1运行并就绪后,web-2启动之前, web-0失败了,web-2将不会启动,直到web-0成功重启并处于运行并就绪状态。

如果用户通过修补StatefulSetscale部署的示例,以使 replicas=1,则web-2将首先被终止。在web-2完全关闭和删除之前,web-1不会被终止。如果web-0web-2终止并且完全关闭之后,但是在web-1终止之前失败,则web-1将不会终止,除非web-0正在运行并准备就绪。

Pod管理策略

Kubernetes 1.7和之后版本,StatefulSet允许您放开顺序保证,同时通过 .spec.podManagementPolicy 字段保证身份的唯一性。

OrderedReady Pod管理

StatefulSet中默认使用的是OrderedReadypod管理。它实现了 如上 所述的行为。

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

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

---
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
kubectl create -f zookeeper.yaml

详细的使用说明见zookeeper stateful application

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

集群外部访问StatefulSetPod

我们设想一下这样的场景:在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实例。

参考

上一页
下一页