• K8S之StatefulSet


    Deployment控制器已经非常优秀了,那为什么还需要StatefulSet呢?

    Deployment控制器所应用的场景只限于一个应用的所有 Pod都是一样的,Pod的IP、名字和启停顺序等都可以是随机的,无所谓运行在哪台宿主机上。但实际应用中,很多应用的实例直接往往都会有依赖关系,例如最常见的“主从关系”,这种情况,肯定是要先启动”主“才行。另外,像需要使用到数据存储类的应用,不同的实例可能存储的数据还略有差别。

    这些这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,被称为“有状态应用”(Stateful Application)。

    StatefulSet 把真实世界里的应用状态,抽象为了两种情况:

    • 拓扑状态:这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
    • 存储状态:这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。

    所以,StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。

    Headless Service

    在K8S中,一般使用Service来为Pod提供访问入口,用户访问Service提供的VIP,然后再由Service调度到后端的随机的Pod上。但对于StatefulSet 来说就行不通了,因为有状态应用通常后面的多个Pod可能提供的服务都不太一样,并不能简单的通过VIP随机调度,而是要精确的知道什么需求下该访问哪个Pod。

    因此,K8S中定义了一种叫做Headless的特殊Service对象,来解决这个问题。

    我们先来看一个简单的Headless Service的 YAML 文件:

    apiVersion: v1
    kind: Service
    metadata:
      name: nginx
      namespace: wuvikr
      labels:
        app: nginx
    spec:
      ports:
      - port: 80
        name: web
      clusterIP: None
      selector:
        app: nginx
    

    可以看出它和一般的Service基本一致,唯一的区别在于ClusterIP字段为None。这下你就能知道为什么它要叫做Headless,即这个 Service,没有一个 VIP 作为“头”。

    一个 Headless Service被创建后,它所匹配到的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录:

    <pod-name>.<svc-name>.<namespace>.svc.cluster.local
    

    有了这样的DNS 记录规则,只要我们知道了Pod 的名字,以及它对应的Service的名字,就可以精确的通过DNS 记录访问到Pod的IP地址了。

    PVC

    存储卷官方文档

    K8S的存储实现有非常多种不同的方案,这些方案和K8S集群对接的时候就会产生各种各样的配置参数,使用起来非常麻烦,这就导致一个开发人员在使用K8S的时候还需要额外的去学习部署存储方面的知识,所谓“术业有专攻”,这些关于 Volume 的管理和远程持久化存储的知识,不仅超越了开发者的知识储备,还会有暴露公司基础设施秘密的风险。为了解决这个问题,K8S引入了PV,PVC的概念。

    PV(PersistentVolume): 不同的存储方案例如 CEPH ,NFS,FS,Longhorn等通过CSI接口以插件的方式与k8s集群进行对接。然后由专门的运维管理人员定义和创建PV存储,它把存储资源抽象成k8s能够处理的一种资源对象。

    PVC(PersistentVolumeClaim):PVC是用户要使用存储资源的一种声明,用户可以在PVC中定义自己的存储消费需求,然后controller会自动将PVC与现有的PV资源进行匹配检测,找出一个最佳的PV进行匹配绑定。需要使用存储的用户(一般是开发)不需要了解底层存储实现方案和配置细节,只需要直接声明定义PVC即可使用存储资源。

    但仅仅如此,并不能完全解决存储相关的问题,因为PV大都是存储管理员事先定义好的,可能并不能满足应用对于存储的各种需求(读写速度,并发性能),也可能造成存储空间的浪费(明明不需要很多的空间,但恰好只有一个很大空间的PV能匹配上某个PVC)。所以,可能某些公司内部用户在使用存储的时候需要提前写PVC工单需求,然后再由管理员去实时创建PV。这样一来效率就很低了,所以就又有了SC这一概念。

    SC(StorageClass):通过SC定义,管理员可以将不同的SC关联到不同的存储实现方案上,这些SC有着各自不同的特性(例如高速存储,分布式存储之类),存储服务会将管理接口提供给SC,从而实现SC自动控制PV的创建,删除,修改,省去了管理员每次都要手动管理PV的麻烦,而用户在定义PVC的时候直接声明使用具有某些存储特性的SC,这样就能自动的创建合适的PV与其绑定,大大减轻了管理人员的负担。当然这个SC的概念并不是单纯为此所提出的,PV和PVC在绑定匹配的时候也要依赖这一字段。PV和PVC在做匹配的时候默认只在具有相同StorageClassName的对象中进行匹配,例如某个PVC中指定了StorageClassName为sc-nfs,那么它只能与同样具有StorageClassName为sc-nfs的PV所绑定,如果某个PVC没有指定StorageClassName,那么这个PVC同样也只能与StorageClassName为空的PV进行绑定。

    说了这么多,我们举几个例子看一下吧,下面是一个PVC的声明yaml文件:

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: pv-claim
      namespace: wuvikr
    spec:
      accessModes:			# 访问模式
      - ReadWriteOnce
      resources:			# 资源需求
        requests:
          storage: 1Gi
    

    可以看到,不需要任何关于 Volume 细节的字段,只有描述性的属性和定义,如:storage: 1Gi,表示想要的 Volume 大小至少是 1 GiB;accessModes: ReadWriteOnce,表示这个 Volume 的挂载方式是单路读写,只能被一个节点使用。

    然后我们需要创建一个PV与其进行绑定,这里我们以NFS为例:

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv-test
    spec:
      accessModes:					# 访问模式需要和pvc一致
        - ReadWriteOnce
      capacity:					# 空间大小
        storage: 1Gi
      persistentVolumeReclaimPolicy: Retain		# 回收策略
      nfs:
        path: /data/volumes/v1
        server: 10.0.0.88
    

    PV和PVC的绑定操作是由Volume Controller中的PersistentVolumeController控制循环决定的,它会不断检查每一个PVC是否已经处于Bound状态了,如果不是,它就会遍历所有可用的 PV,并尝试将其与这个为绑定的PVC进行绑定。另外,PV在创建的时候需要依据PVC的声明内容,否则可能无法进行绑定。

    接下来我们就可以使用这个PVC了:

    apiVersion: v1
    kind: Pod
    metadata:
      name: pv-pod
    spec:
      containers:
        - name: pv-container
          image: nginx
          ports:
            - containerPort: 80
              name: "http-server"
          volumeMounts:
            - mountPath: "/usr/share/nginx/html"
              name: pv-storage
      volumes:
        - name: pv-storage
          persistentVolumeClaim:
            claimName: pv-claim
    

    在这个Pod的定义中,只需要指定我们刚刚声明的PVC即可,至于这个存储到底从哪里来,就并不需要用户去关心了。有点类似于编程中接口和实现的关系。

    下面我们再来看一个官方文档中的StorageClass的yaml文件:

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: standard
    provisioner: kubernetes.io/aws-ebs
    parameters:
      type: gp2
    reclaimPolicy: Retain
    allowVolumeExpansion: true
    mountOptions:
      - debug
    volumeBindingMode: Immediate
    

    可以看到StorageClass没有spec字段,其中比较重要的几个字段有:

    • provisioner:必选字段,用于指定存储服务方,存储类要依赖该字段值来判定要使用什么存储插件来创建PV。Kubernetes内建支持许多的Provisioner,它们的名字都以kubernetes.io/为前缀,例如上面yaml文件中的kubernetes.io/aws-ebs这种,具体哪些是K8S内建支持的,可以参考官方文档来查询。当然也可以自定义符合K8S规范的存储类插件,例如NFS
    • parameters:定义连接至指定的Provisioner类别下的某特定存储时需要使用的各相关参数,不同存储服务的Provisioner的可用的参数各不相同,需要针对性的去了解学习;
    • reclaimPolicy:存储类创建的PV资源回收策略,可用值为Delete(默认)和Retain两个;

    当我们定义好了StorageClass后,一旦出现符合SC定义的PVC后(一般由storageClassName来判定),就会自动为其创建一个合适的PV与其进行绑定。

    volumeClaimTemplates

    在前面例子中我们已经知道Pod可以使用PVC来持久化书存储,而StatefulSet会有多个不同的Pod,这时候是使用的是Pod模板来创建Pod,那么如何来保证每个Pod中的数据都单独持久化数据呢,总不可能在Pod模板中手动声明一堆的PVC吧。而解决方式就是volumeClaimTemplates。

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: sts-nginx
      namespace: wuvikr
    spec:
      serviceName: nginx
      replicas: 2
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx:1.18.0-alpine
            ports:
            - name: web
              containerPort: 80
            volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
      volumeClaimTemplates:
      - metadata:
          name: www
        spec:
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 10Gi
          storageClassName: managed-nfs-storage
    

    在上面的StatefulSet的yaml文件定义中有个spec.volumeClaimTemplates<[]Object>字段,其字段中的定义和PVC的定义几乎一致,而且从名字上看和Deployment中的PodTemplate有点相似,所以也就不难猜到它的用处了。每个被这个StatefulSet创建出来的Pod,都会声明一个对应的PVC,这个PVC的定义信息就来自于volumeClaimTemplates 这个字段。最重要的是,PVC的名字会被分配一个与Pod完全一样的编号,这样,持久化的数据就可以与Pod一一对应上了。

    在上面这个例子中我们使用了NFS的外部存储,并创建了SC,因此StatefulSet每创建一个Pod,volumeClaimTemplates字段都将创建一个同名的PVC,而SC又将为这个PVC创建一个对应的PV。一切都连接起来了。

    应用

    我们应用一下上面的Headless Service和StatefulSet :

    [root@center-188 statefulset]# kubectl apply -f headless-nginx.yaml
    [root@center-188 statefulset]# kubectl apply -f sts-nginx.yaml
    
    # 可以看到出现了两个PVC,两个Pod,并且名称一一对应
    [root@center-188 statefulset]# kubectl get pvc -l app=nginx -n wuvikr
    NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
    www-sts-nginx-0   Bound    pvc-af8b73c0-81f4-4027-adfe-ba58e43ca7ed   10Gi       RWO            managed-nfs-storage   128m
    www-sts-nginx-1   Bound    pvc-3772c488-4b3c-4f17-be3c-3f8b51b9d0ae   10Gi       RWO            managed-nfs-storage   128m
    [root@center-188 statefulset]# kubectl get pod -n wuvikr
    NAME                            READY   STATUS    RESTARTS   AGE
    sts-nginx-0                     1/1     Running   0          47s
    sts-nginx-1                     1/1     Running   0          45s
    
    
    # 使用DNS记录解析Pod IP
    [root@center-188 statefulset]# kubectl get svc -n wuvikr
    NAME             			TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
    headless-nginx            ClusterIP   	None         <none>        80/TCP              55s
    
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-0 -n wuvikr -- sh
    / # cat /etc/hostname
    sts-nginx-0
    / # cat /etc/resolv.conf
    nameserver 10.68.0.2
    search wuvikr.svc.cluster.local. svc.cluster.local. cluster.local.
    options ndots:5
    / # nslookup wuvikr.svc.cluster.local.
    Server:		10.68.0.2
    Address:	10.68.0.2:53
    
    
    
    / # nslookup sts-nginx-0.headless-nginx.wuvikr.svc.cluster.local.
    Server:		10.68.0.2
    Address:	10.68.0.2:53
    
    Name:	sts-nginx-0.headless-nginx.wuvikr.svc.cluster.local
    Address: 172.20.157.210
    
    
    # 创建index.html文件
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-0 -n wuvikr -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-1 -n wuvikr -- sh -c 'echo $(hostname) > /usr/share/nginx/html/index.html'
    
    # 访问Pod的nginx主页
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-0 -n wuvikr -- curl localhost
    sts-nginx-0
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-1 -n wuvikr -- curl localhost
    sts-nginx-1
    
    # 删除Pod
    [root@center-188 statefulset]# kubectl delete pod -l app=nginx -n wuvikr
    pod "sts-nginx-0" deleted
    pod "sts-nginx-1" deleted
    
    # 查看Pod
    [root@center-188 statefulset]# kubectl get pod -n wuvikr
    NAME                            READY   STATUS    RESTARTS   AGE
    sts-nginx-0                     1/1     Running   0          12s
    sts-nginx-1                     1/1     Running   0          10s
    
    # 查看新Pod的内容
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-0 -n wuvikr -- hostname
    sts-nginx-0
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-1 -n wuvikr -- hostname
    sts-nginx-1
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-0 -n wuvikr -- curl localhost
    sts-nginx-0
    [root@center-188 statefulset]# kubectl exec -it sts-nginx-1 -n wuvikr -- curl localhost
    sts-nginx-1
    

    从上面的操作中可以看出PVC的命名方式是<PVC名字>-<sts名字>-<编号>;Pod的hostname就是Pod名称+编号,使用对应的DNS 记录规则也能够访问到对应的Pod;删除Pod后,StatefulSet 仍然以编号顺序重新创建Pod,并且新Pod的名称、hostname和数据都不变。

    总结

    StatefulSet 其实就是一种特殊的 Deployment,所不同的是它是直接管理Pod的,另外它给每个Pod都加上了一个编号,这个编号体现在 Pod 的名字和 hostname 等标识信息上,不仅代表了 Pod 的创建顺序,也是 Pod 的重要网络标识。当有Pod被删除后,StatefulSet会重建并赋予Pod的原来的名称保持不变,这样,DNS的解析记录就不会变,并且会自动与之前已有的PVC继续绑定上,这样数据也就不会变。

    StatefulSet 就是以这种编号的方式实现了Pod的拓扑状态和存储状态的管理,几乎完美的实现了有状态应用的编排应用。

    参考:

    • 深入剖析Kubernetes-张磊
  • 相关阅读:
    #1071 : 小玩具
    #1063 : 缩地
    #1124 : 好矩阵
    hiho#1145 : 幻想乡的日常
    hiho#14
    hiho 毁灭者问题
    西南民大oj(递推)
    西南民大oj(矩阵快速幂)
    西南民大oj(两园交求面积)
    hdu2844(多重背包)
  • 原文地址:https://www.cnblogs.com/wuvikr/p/14539303.html
Copyright © 2020-2023  润新知