StatefulSet

StatefulSet中的Stateful是有状态的意思。其中的状态即指:StatefulSet接管的每个Pod有稳定的网络标识(包括Pod的名称和主机名,是顺序索引值)、稳定的专属存储。进一步解释:

  1. StatefulSet替换旧Pod时,新Pod与其有相同的名称、网络标识和状态;

  2. StatefulSet管理的Pod可以有各自独立的存储卷;

  3. StatefulSet创建的Pod的名字是有规律的,不是随机生成的;

StatefulSet接管下的Pod的架构可以用下图表示:

StatefulSet接管的Pod架构图

Tips: 从图中可以看出,StatefulSet创建的Pod的名称(和主机名)以及PVC的名称都是按顺序索引的,这是它的特征之一。

StatefulSet与ReplicaSet对比

把StatefulSet比作宠物,ReplicaSet比作

对于有状态应用,即StatefulSet接管的Pod,就像宠物一样,若一只宠物死掉,若要找一个替换它,除非找到完全一模一样的(现实中当然是不可能的),否则用户不可能感知不到。

对于无状态应用,即ReplicaSet接管的Pod,就像农场中的牛一样,若一只牛死掉,农场主完全可以找另一个牛来替代,不用一模一样,对于用户(食客)没有什么差别。

稳定的网络标识

在前面提到过,StatefulSet创建的Pod的名称和主机名都是固定的(因此称之为稳定的网络标识),因此在使用StatefulSet时通常都会创建一个用来记录每个Pod网络标识headless Service(即Service资源的spec.clusterIP=None。通过这个Service资源对象,每个Pod都有自己专属的DNS记录,这样分布式应用中的其他Pod可以方便的通过主机名找到该Pod。例如,一个Pod的名称(主机名)为A-0,Service的名字为foo,属于default这个命名空间,那么a-0.foo.default.svc.cluster.local就是这个Pod的专属域名。

紧跟着上面这个例子,我们通过访问foo.default.svc.cluster.local也可以获得其对应的所有SRV记录,从而获得了StatefulSet管理的所有Pod的名称和主机名。

Tips: headless Service通过将spec.clusterIP设置为None,这样访问Service名.命名空间.svc.cluster.local获得的就不会是Service的clusterIP了,而是被该Service监听的所有Pod的IP。

headlessService的目的是为了为分布式应用中的各Pod提供通过主机名就可以访问其他Pod的能力。

若要将应用的API服务请求地址映射出去,通常会创建另外一个Service来达到该目的。

稳定的专属存储

StatefulSet创建的Pod都有自己的专属存储,这是依赖PVC(持久卷声明)实现的,PVC与PV是一一对应的关系,而Pod与PVC是一一对应的关系,即PVC会在Pod创建之前创建出来,并绑定至Pod上。

当StatefulSet删除Pod时,会保留PVC和PV,当创建新的同规格的Pod时,会将该保留的PVC再绑定至该Pod上。

扩缩容

扩缩容的方式可见修改资源方式中的方式。

扩容

扩容一个StatefulSet会使用下一个还没用到的顺序索引值创建一个新的Pod。比如,现有2个Pod,它们的索引值为0和1,则新创建的Pod的索引值为2。

在扩容时,会根据Pod的PVC模板创建它专属的PV。

缩容

缩容一个StatefulSet会最先删除最高索引值,因此StatefulSet的缩容结果是可预见的。比如,现有3个Pod,则最先被删除的Pod的索引值为2。

StatefulSet的缩容是线性的(即删除完一个,再删下一个),所以在有Pod不健康的情况下,是不允许做缩容操作的。

在缩容时,Pod会被直接删除,但是PV和PVC不会被删除。

Tips: 这里解释一下线性缩容的原因。假设一个StatefulSet中有2个副本Pod,它们均为应用提供数据存储功能,若两个Pod缩容时同时被删除(一个是在缩容时删除,另一个则因Node故障而删除),那么应用的存储功能相当于完全崩溃了(虽然它们的PV还在,但是由于没有Pod通过该PV提供服务,所以应用此刻是无法存储数据的)。因此设计成线性缩容的方式,这样在其中一个Pod处于删除过程中时,另一个Pod不能被删除,从而正常为应用提供存储功能。

创建StatefulSet

通过StatefulSet部署应用时,总共需要部署两个(或三个)不同的资源类型对象:

  1. 持久卷(PersistentVolume):存储应用的数据文件(若集群不支持持久卷动态供应时,才需要手动创建)

  2. headless Service:为Pod提供依靠主机名访问应用中其他Pod的能力(通常访问应用不会通过该Service,而是另外再建一个Service将应用的API接口地址暴露出去);

  3. StatefulSet本身;

部署持久卷

kind: List    # List类型,其实和使用---将资源分割效果一样,这里是在items中指定其他资源类型
apiVersion: v1
items:
- apiVersion: v1    # 第一个PV
  kind: PersistentVolume    # 类型
  metadata:
    name: pv-a    # PV名
  spec:
    capacity:    # PV容量
      storage: 1Mi
    accessModes:    # PV的访问模式
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle    # 回收策略,当PVC释放后,空间会被回收并清空,从而可以被其他PVC继续访问再利用(记住在删除Pod时是不会删除它的PVC的,因为如果删除了PVC,则空间会被回收,则该Pod的状态就没了,即使创建新Pod,也不会有原数据了,所以缩容是不会删除PVC的)
    hostPath:    # 存储策略,使用hostpath
      path: /tmp/pv-a    # (必须)宿主机路径
- apiVersion: v1    # 第二个PV
  kind: PersistentVolume
  metadata:
    name: pv-b
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    hostPath:
      path: /tmp/pv-b
- apiVersion: v1    # 第三个PV
  kind: PersistentVolume
  metadata:
    name: pv-c
  spec:
    capacity:
      storage: 1Mi
    accessModes:
      - ReadWriteOnce
    persistentVolumeReclaimPolicy: Recycle
    hostPath:
      path: /tmp/pv-c

部署headless Service

apiVersion: v1
kind: Service
metadata:
  name: kubia
spec:
  clusterIP: None    # headless Service需要指定ClusterIP为None
  selector:    # 所有标签为app=kubia的Pod都属于这个Service
    app: kubia
  ports:
  - name: http
    port: 80    # (必须)该Service向外暴露的端口

部署StatefulSet

apiVersion: apps/v1
kind: StatefulSet    # 资源类型为StatefulSet
metadata:
  name: kubia
spec:
  replicas: 2    # 副本数
  serviceName: kubia    # (必须)指定控制Service的名字,该Service必须在该资源创建之前就已经存在
  selector:    # (必须)标签选择器,匹配需要接管的Pod的标签。可以使用matchExpressions或matchLabels
    matchLabels:
      app: kubia
  template:    # (必须)Pod模板
    metadata:
      labels:
        app: kubia
    spec:
      containers:
      - name: kubia
        image: luksa/kubia-pet
        ports:
        - name: http
          containerPort: 8080
        volumeMounts:    # Pod中的容器会把PVC数据集嵌入指定目录
        - name: data    # (必须)需要挂载的卷的名字,这里是声明的PVC模板的名字
          mountPath: /var/data    # (必须)Pod内部的挂载点
  volumeClaimTemplates:    # PVC模板,这里没有指定StorageClass
  - metadata:
      name: data
    spec:
      resources:
        requests:
          storage: 1Mi
      accessModes:
      - ReadWriteOnce
      storageClassName: ""    # 为了避免使用Minikube默认的SC,动态创建PV,这里我们要使用的是我们事先创建好的三个PV

部署结果

部署结果如下:

# 查看Pods
$ get pod --show-labels
NAME          READY   STATUS    RESTARTS   AGE   LABELS
kubia-0       1/1     Running   0          18m   app=kubia,controller-revision-hash=kubia-c94bcb69b,statefulset.kubernetes.io/pod-name=kubia-0
kubia-1       1/1     Running   0          18m   app=kubia,controller-revision-hash=kubia-c94bcb69b,statefulset.kubernetes.io/pod-name=kubia-1

# 查看PV
$ get pv -o wide
>>> get pv -o wide
NAME   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM                  STORAGECLASS   REASON   AGE   VOLUMEMODE
pv-a   1Mi        RWO            Recycle          Bound       default/data-kubia-0                           40m   Filesystem
pv-b   1Mi        RWO            Recycle          Bound       default/data-kubia-1                           40m   Filesystem
pv-c   1Mi        RWO            Recycle          Available                                                  40m   Filesystem

# 查看PVC
$ get pvc --show-labels
NAME           STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE   LABELS
data-kubia-0   Bound    pv-a     1Mi        RWO                           22m   app=kubia
data-kubia-1   Bound    pv-b     1Mi        RWO                           22m   app=kubia

# 查看Service
$ get service -o wide
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE     SELECTOR
kubia        ClusterIP   None         <none>        80/TCP    34m     app=kubia

# 查看StatefulSet
$ get statefulset -o wide
NAME    READY   AGE   CONTAINERS   IMAGES
kubia   2/2     21m   kubia        luksa/kubia-pet

Tips:

  1. StatefulSet创建Pod同样是线性创建的,会令第一个Pod就绪之后,才会创建第二个Pod;

  2. 创建出的PVC的名字结构为pvc模板中指定的名字-Pod的名称

  3. 创建出的PVC、Pod的名字都是有序的;

发现应用中其他Pod节点

在之前提到过,在部署StatefuSet资源对象时,需要创建一个headless Service资源对象,该对象可以为应用中的Pod提供发现其他Pod的能力。

假设在default命名空间中,有一个名为kubia的headless Service,那么应用中的其他Pod可以使用dig命令,访问kubia.default.svc.cluster.local就可以通过SRV记录,获得该Service的所有后端的Pod:

$ dig SRV kubia.default.svc.cluster.local
...
;; ANSWER SECTION:
kubia.default.svc.cluster.local. 30 IN    SRV    0 50 80 kubia-0.kubia.default.svc.cluster.local.
kubia.default.svc.cluster.local. 30 IN    SRV    0 50 80 kubia-1.kubia.default.svc.cluster.local.

;; ADDITIONAL SECTION:
kubia-0.kubia.default.svc.cluster.local. 30 IN A 172.17.0.8
kubia-1.kubia.default.svc.cluster.local. 30 IN A 172.17.0.9
...

kubia-0.kubia.default.svc.cluster.local是一个Pod的DNS域名,其地址为172.17.0.8

kubia-1.kubia.default.svc.cluster.local是另一个Pod的DNS域名,其地址为172.17.0.9

通过该机制,可以得到一个简易的分布式数据存储服务的操作流程,如下图所示,图中的每一个Pod都有自己的存储空间,用户的请求会随机打到任意一个Pod上,该Pod会从其他Pod上获取数据,并与自己本地数据汇总,然后再返回给用户:

简易的分布式数据存储服务的操作流程

处理Node失效

由于StatefulSet需要保证不会拥有两个相同标记和存储的Pod同时运行,因此在明确知道一个Pod不再运行之前,它不会去创建一个替换Pod。

当Node失效时,需要管理员手动通知Kubernetes删除失效的Pod,StatefulSet才会去创建替换Pod。

Last updated

Was this helpful?