kubernete编排技术三:StatefulSet

gq_design
发布于 2022-5-1 09:25
浏览
0收藏

作者 |  朱晋君
来源 | 君哥聊技术(ID:gh_1f109b82d301)

上一篇文章中,我们讲了deployment的编排技术,也提到了这种编排技术只能编排无状态的pod。但是在我们实际生产环境中,系统复杂很多。比如分布式系统,pod之间往往有依赖关系。再比如mysql数据库,主从节点需要通过binlog同步数据,读写请可能要求发送到不同节点上。对这种有状态的应用,kubernete的解决方案是StatefulSet。

StatefulSet的解决方案是把有状态应用的状态抽象成2种状态,拓扑状态和存储状态,把这些状态记录下来,pod重新创建后,帮助新pod恢复出这些状态。拓扑状态主要是就是有类似主从关系的应用,在启动或者升级发布的时候,pod的启动顺序都要固定,比如主节点先启动,从节点后启动。存储状态就是指不同应用的存储数据要固定,比如多个pod的服务重启后各自读取到的存储数据跟重启之前一样。

拓扑状态

在之前的文章《kubernete架构体系介绍》中提到过,kubernete为每一个pod绑定一个service服务,service服务作为pod的代理访问入口,配置的IP等地址信息是固定不变的,这样即使pod重启后IP地址发生了变化,只要service的ip地址不变,可以不用关心。

外部通过service代理访问pod,必须先访问service,这无非就是2种方式,直接访问service的ip地址或通过域名访问。如下图所示:kubernete编排技术三:StatefulSet-鸿蒙开发者社区

StatefulSet在编排上的一个创新是外部应用访问pod的时候,不用在通过访问service的ip地址或者域名,而是直接访问pod的域名来访问pod,而service的名字绑定在这个pod中对pod的启动顺序进行控制。域名的格式如下:

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

这种不提供域名或ip地址的service被称作Headless Service,本质就是yaml中定义的clusterIP是None,yaml文件(HeadlessService.yaml)如下:

apiVersion: v1
kind: Service
metadata:
  name: bootservice
  labels:
    app: bootservice
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: springboot

上面的service就会控制selector选择出的携带标签app: springboot的pod。而我们重新改写之前基于deployment的yaml文件,改为基于StatefulSet的文件,文件名springboot-mybatis-statefulset.yaml内容如下:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bootstatefulset
spec:
  serviceName: "bootservice"
  selector:
    matchLabels:
      app: springboot
  replicas: 2
  template:
    metadata:
      labels:
        app: springboot
    spec:
      containers:
      - name: spingboot-mybatis
        imagePullPolicy: IfNotPresent
        image: zjj2006forever/springboot-mybatis:1.3
        ports:
        - containerPort: 8300

相比于之前基于deployment的文件,除了修改了kind为StatefulSet和pod名称之外,它多了一个serviceName字段,就是指定代理自己的headless service的名字。

我们首先执行下面命令创建这个service

kubectl create -f HeadlessService.yaml 

创建成功后可以查看这个service,CLUSTER-IP为None

[root@master k8s]# kubectl get service
NAME          TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
bootservice   ClusterIP   None         <none>        80/TCP    11s
kubernetes    ClusterIP   10.96.0.1    <none>        443/TCP   14d

接着我们创建StatefulSet,命令如下:

kubectl create -f springboot-mybatis-statefulset.yaml

成功后我们查看刚刚创建的StatefulSet

[root@master k8s]# kubectl get StatefulSet
NAME              READY   AGE
bootstatefulset   2/2     7s

注意,创建StatefulSet的时候,我们可以执行下面的命令查看pod创建过程,从输出可以看到,StatefulSet会先创建出一个pod并且编号为0,启动成功后才会开始创建第二个pod,编号为1,这样就固定了pod的创建顺序

[root@master k8s]# kubectl get pods -w -l app=springboot
NAME                READY   STATUS    RESTARTS   AGE
bootstatefulset-0   0/1     Pending   0          0s
bootstatefulset-0   0/1     Pending   0          0s
bootstatefulset-0   0/1     ContainerCreating   0          0s
bootstatefulset-0   1/1     Running             0          3s
bootstatefulset-1   0/1     Pending             0          0s
bootstatefulset-1   0/1     Pending             0          0s
bootstatefulset-1   0/1     ContainerCreating   0          0s
bootstatefulset-1   1/1     Running             0          2s

我们进入pod中查看hostname可以发现跟pod名字一样

[root@master k8s]# kubectl exec -it bootstatefulset-0 -- /bin/sh
/ # hostname
bootstatefulset-0

我们ping上面提到的域名,输出正是容器中的IP地址

/ # ping bootstatefulset-0.bootservice.default.svc.cluster.local
PING bootstatefulset-0.bootservice.default.svc.cluster.local (10.244.1.41): 56 data bytes
64 bytes from 10.244.1.41: seq=0 ttl=64 time=0.419 ms
64 bytes from 10.244.1.41: seq=1 ttl=64 time=0.065 ms
64 bytes from 10.244.1.41: seq=2 ttl=64 time=0.071 ms

nslookup看一下,解析正常,结果如下:

/ # nslookup  bootstatefulset-0.bootservice.default.svc.cluster.local
Name:      bootstatefulset-0.bootservice.default.svc.cluster.local
Address 1: 10.244.1.41 bootstatefulset-0.bootservice.default.svc.cluster.local

从上面的输出可以看出,stateful为pod生成的域名生效了。之后如果我们删除第一个bootstatefulset-0会发生什么呢?

执行下面删除命令后查看pod变化状态

kubectl delete pod bootstatefulset-0

可以看到bootstatefulset-0这个pod被删除后重新创建出来,并且编号没有变化。

[root@master kubernetes]# kubectl get pods -w -l app=springboot
NAME                READY   STATUS    RESTARTS   AGE
bootstatefulset-0   1/1     Running   0          44m
bootstatefulset-1   1/1     Running   0          44m
bootstatefulset-0   1/1     Terminating   0          44m
bootstatefulset-0   0/1     Terminating   0          44m
bootstatefulset-0   0/1     Terminating   0          44m
bootstatefulset-0   0/1     Terminating   0          44m
bootstatefulset-0   0/1     Pending       0          0s
bootstatefulset-0   0/1     Pending       0          0s
bootstatefulset-0   0/1     ContainerCreating   0          0s
bootstatefulset-0   1/1     Running             0          1s

再次进入bootstatefulset-0中ping域名,发现pod的ip地址发生了变化,但是域名正常访问,这也说明访问pod是必须使用域名而不能直接用ip地址访问

[root@master k8s]# kubectl exec -it bootstatefulset-0 -- /bin/sh
/ # ping bootstatefulset-0.bootservice.default.svc.cluster.local
PING bootstatefulset-0.bootservice.default.svc.cluster.local (10.244.1.46): 56 data bytes
64 bytes from 10.244.1.46: seq=0 ttl=64 time=0.158 ms
64 bytes from 10.244.1.46: seq=1 ttl=64 time=0.543 ms

这样StatefulSet就用pod name+编号的方式为pod的启动和升级发布固定了顺序,在主从关系情况下也能保证主节点先启动,从节点后启动。同时节点暴露的域名是固定的,外部服务需要通过域名访问。

存储状态

StatefulSet对存储的解决方案,是引入了Persistent Volume Claim和Persistent Volume,简称PVC和PV。

下面就是一个PVC的定义,这个PVC定义了一个Volume大小占用256M,挂载方式是可以读写。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: pv-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 256Mi

注:下面是来自官网的accessModes

ReadWriteOnce -- the volume can be mounted as read-write by a single node
ReadOnlyMany -- the volume can be mounted read-only by many nodes
ReadWriteMany -- the volume can be mounted as read-write by many nodes

我们可以在之前的StatefulSet声明中,可以使用这个PVC,如下:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: bootstatefulset
spec:
  serviceName: "bootservice"
  selector:
    matchLabels:
      app: springboot
  replicas: 2
  template:
    metadata:
      labels:
        app: springboot
    spec:
      containers:
      - name: spingboot-mybatis
        imagePullPolicy: IfNotPresent
        image: zjj2006forever/springboot-mybatis:1.3
        ports:
        - containerPort: 8300
    volumeMounts:
    - mountPath: "/usr/share/"
      name: pvstorage
      volumeClaimTemplates:
    - metadata:
        name: pvstorage
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
       requests:
        storage: 256Mi

像上面这样,我们只需要在模板中声明一个PVC的名字,就可以使用这个存储了。这需要kubernete为这个PVC绑定一个pv,用这个这个pv来声明volume,下面我们看一个PV的定义:

kind: PersistentVolume
apiVersion: v1
metadata:
  name: pv-volume
  labels:
    type: local
spec:
  capacity:
    storage: 512Mi
  accessModes:
    - ReadWriteOnce
  rbd:
    monitors:
    - '10.244.1.158:6789'
    - '10.244.1.159:6789'
    pool: kube
    image: foo
    fsType: ext4
    readOnly: true
    user: admin
    keyring: /etc/ceph/keyring

注:上面monitors是使用kubectl get pods -n rook-ceph -o wide看到的rook-ceph-mon-开头ip地址

接着创建上面的StatefulSet后,就会生成2个pvc,名字格式是PVC名字-StatefulSet名字-编号

执行创建命令

kubectl create -f springboot-mybatis-statefulset.yaml 

查看pod

[root@master k8s]# kubectl get pods
NAME                READY   STATUS    RESTARTS   AGE
bootstatefulset-0   1/1     Running   0          2m4s
bootstatefulset-1   1/1     Running   0          2m1s

查看pvc

kubectl get pvc -l app=springboot
NAME                          STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
pvstorage-bootstatefulset-0   Bound     pvc-12c125c7-b507-11e6-932f-5210a500005   256Mi      RWO           29s                            
pvstorage-bootstatefulset-1   Bound     pvc-12c136c7-b507-11e6-932f-5210a500005   256Mi      RWO           29s   

上面创建了这个带编号的pvc后,pod会按照编号来绑定pvc,如上bootstatefulset-0会使用pvstorage-bootstatefulset-0这个pvc,我们在每个pod中创建一个文件,然后删除pod后等待重新创建,文件依然存在。

这是因为pod被删除后,pv和pvc并没有被删除,而pod被创建出来后,因为StatefulSet的控制,pod会严格按照之前的编号顺序创建出来,而它们会重新绑定相同编号的pvc,从而绑定pvc对应的pv来获取volume里面的数据。

总结

StatefulSet也是一种Deployment,只是它的每一个pod都携带了一个唯一并且固定的编号。这个编号非常重要,因为这个编号固定了pod的拓扑关系(比如主从),固定了pod的DNS记录,有了这个序号,当pod重建时,就不会丢失之前的状态了。pvc则固定了pod的存储状态,它与pv进行绑定从而使用pv中声明的volume存储。这样pod重启后数据就不会丢失了。

分类
收藏
回复
举报
回复
    相关推荐