【Kubernetes】StatefulSet 简介

641 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第N天,点击查看活动详情

一、StatefulSet 简介

有状态应用在 Kubernetes 里对应的对象就是 StatefulSet

Deployment 一样,StatefulSet 也是一种控制器。 区别是一个控制无状态应用,一个控制有状态应用。

当一个有状态的 Pod 挂掉之后(或者是所在节点出现故障),这个 Pod 实例必须在其它的节点上重建,但是新的实例必须和被替换的实例拥有相同的名称、网络标识和状态,StatefulSet 可以保证 Pod 在重新调度以后保留它们之前的标识和状态,这样就可以方便的扩缩容。

StatefulSet 适合具有以下需求的场景:

  • 稳定的持久化存储 Pod 在重新调度之后还能继续访问之前的存储数据
  • 稳定的网络标志 Pod 在重新调度后其名字和主机名保持不变
  • 有序部署:构成应用的多个 Pod 启动顺序是固定的,适合 Pod 之间有依赖关系的情况
  • 有序删除:删除应用时 Pod 销毁顺序跟启动顺序正好相反

StatefulSet 中每个 Pod 具有固定的名字,PodDNS 域名格式为 statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local。跟普通域名一样,子域在前,主域在后,只不过节数比较多而已。

各部分含义如下:

  • statefulSetNameStatefulSet 名字
  • 0..N-1 Pod 的序号:从 0 到 N-1
  • serviceNameHeadless Service(没有 ClusterIPService) 名字,StatefulSet 无法使用普通的 Service(有一个 ClusterIP 作为服务入口) 来提供应用服务(包括负载均衡)。这是因为 StatefulSet 里的各个 Pod 不是对等的,只能由用户自己来实现服务内部的网络路由
  • namespace:服务所在的命名空间,Headless ServiceStatefulSet 必须在相同的命名空间下
  • svc.cluster.local:集群域名

StatefulSet 的设置由以下 3 部分组成:

  • 用于定义网络标志的 Headless Service
  • 用于创建 PV(持久存储卷)的 volumeClaimTemplates
  • 定义具体应用的 StatefulSet

(1)Headless Service(无头服务)

Headless Service 是指不为 Service 设置 ClusterIP(入口 IP 地址),只通过标签选择器将后端的 Pod 列表返回给调用的客户端,客户端自行决定如何处理这个 Pod 列表。

Deployment ,每个创建的 Pod 的名称都是没有顺序的,名称最后有一截 Hash 值,因此这样创建的 Pod 是无序的。但是 StatefulSet 中要求 Pod 名称必须有序,每个 Pod 名称具有唯一性,并且当 Pod 重建后名称依然要保持一致,Pod 的名称就是唯一性的标志,必须持久有效,因此会使用 Headless Service 服务。

(2)volumeClaimTemplates(卷申请模板)

有状态自然是需要持久化的,所以 StatefulSet 引入了 PV(持久存储卷) 和 PVC(持久存储卷声明) 对象来持久存储服务产生的状态,这样即便服务可能被 kill 掉或是重启,但是由于数据保存在 PV 中,所以这些状态也不会丢失。

Deployment 时,直接在 Pod template 中定义存储卷,所有副本集使用同一个存储卷,数据都是相同的。但是 StatefulSet 中要求有状态存储,也就意味着数据不一样,每个节点必须有专有存储卷,不能使用相同存储卷,因此需要使用 volumeClaimTemplates,为每个 Pod 生成不同的 PVC,并绑定 PV,这样就实现了 Pod 分别有专属的存储卷。



二、运行 nginx 实例

整个流程:

  • 创建本地持久存储卷
  • 将本地持久存储卷绑定到 Node
  • 创建 StatefulSet
  1. 创建 local PV,新建 local-storage.yaml文件
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
# provisioner 的值为 kubernetes.io/no-provisioner,因为 local pv 不支持 Dynamic Provisioning,不能在创建 pvc 的时候自动创建 pv
provisioner: kubernetes.io/no-provisioner
# 使用延迟卷绑定功能,将 volume binding 延迟至 pod scheduling 阶段执行
volumeBindingMode: WaitForFirstConsumer
  1. 执行创建
$ kubectl create -f local-storage.yaml
storageclass.storage.k8s.io/local-storage created
  1. 在每个 Node 上创建一个 PV,新建 local-pv.yaml文件
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv1
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  # local.path 指明对应的磁盘路径
  local:
    path: /tmp
  # spec.nodeAffinity 指定对应的 node
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - kube-node-1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv2
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: /tmp
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - kube-node-2

上面的配置会在 kube-node-1kube-node-2 两个 Node 上分别创建一个 PV。其中,存储类 spec.storageClassName 用的是前面启用的 local-storagespec.capacity.storage 指定了容量为 1G,挂载路径 spec.local.path/tmp(该路径需要在 Node 上存在)。

  1. 执行创建
$ kubectl create -f local-pv.yaml
persistentvolume/local-pv1 created
persistentvolume/local-pv2 created

$ kubectl get pv
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    REASON   AGE
local-pv1   1Gi        RWO            Retain           Available           local-storage            2m47s
local-pv2   1Gi        RWO            Retain           Available           local-storage            2m47s
  1. 创建有状态集合 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/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: registry-vpc.cn-hangzhou.aliyuncs.com/chenshi-kubernetes/nginx-slim:0.8
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: local-storage
        resources:
          requests:
            storage: 10Mi

其中 ServiceclusterIP 指定为 None,表示它是一个 Headless Service,集群不用为它分配 IPStatefulSet 包含 2 个 Pod 副本,它们的名字分别为 web-0web-1,并且都挂载了一个 PV。不管 Pod 被重新调度到哪个节点,Pod 的名字和挂载的 PV 都将跟之前保持一致。

  1. 创建之前可以监听 Pod 的创建过程,在一个新的终端执行:
kubectl get pods -w -l app=nginx
  1. 执行创建
$ kubectl create -f web.yaml
service/nginx created
statefulset.apps/web created
  1. 查看创建的 Headless ServiceStatefulSet
$ kubectl get service nginx
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    22s
$ kubectl get statefulset web
NAME   READY   AGE
web    2/2     37s
  1. 查看根据 volumeClaimTemplates 自动创建 PVC
# PVC 名称的格式为:$(volumeClainTemplates.name)-(pod_name),这里 volumeClainTemplates.name 为 www,pod_name 为 web-[0-1]
$ kubectl get pvc
NAME        STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS    AGE
www-web-0   Bound    local-pv1   1Gi        RWO            local-storage   2m53s
www-web-1   Bound    local-pv2   1Gi        RWO            local-storage   2m44s
  1. 查看创建的 Pod,都是有序的
# Pod 名称的格式为:$(StatefulSet 名称)-$(序号),这里为:web-0、web-1
$ kubectl get pods -l app=nginx
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          5m33s
web-1   1/1     Running   0          5m24s