首页 > 技术文章 > K8S之StatefulSet

wuvikr 2021-03-15 18:35 原文

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-张磊

推荐阅读