YAZONG 我的开源

Kubernetes(十一)落地实践(11.6)StatefulSet --- 有状态应用的守护者

  , , ,
0 评论0 浏览

概要

K8S如何来化解有状态的应用。

image.png

之前使用的编排对象都是deployment,之前开发的服务也都可以使用deployment管理起来。

但是deployment并不能胜任所有的工作。

之前的服务都有一个特征,

首先都是无状态的,每个POD都是无差别的,并且它们的实例之间没有顺序性。

但是在实际的场景下,并不是所有的应用都具备此种条件,特别是分布式应用,对于分布式应用,它们的多个实例之间,往往会存在一种关系,主从ZK、ETCD、redis,都有这种特征。

image.png

对于这种多个实例、不对等的应用,K8S设计了专门的编排对象,叫做StatefulSet。

根据实际的应用情况,StatefulSet抽象了两种有状态的场景,主要解决了两个问题,

第一种是多个POD之间的顺序性,比如需要按一定的顺序去启动,像每个实例可以有自己的编号,再比如实例之间可能互相访问、互相通讯的情况,这些情况都可以通过顺序性特征来解决。

第二种是对持久存储的区分,对于持久存储,在deployment中配置持久存储的PVC,多个POD之间就可以共享同一个目录了,一个POD写入了文件,所有的POD都可以看到,对于持久存储的区分,就是让POD之间对于同一个目录是保持私有的,每个POD都可以有自己的数据,当然这种情况只针对共享存储的,如果POD本身没有共享存储的需求,那么就是区分开的,就不需要考虑这个问题了。

综上,redis主从、mysql主备、ZK集群、ETCD集群,本质都是依靠StatefulSet的这两种特性来实现的。

开始

statefulSet管理的POD,不像deployment它下面的每个实例都是独立的个体。

statefulSet有自己的编号,有自己的hostname,并且K8S通过headless service为这些有编号的POD在DNS中生成了同样的编号的DNS的记录,只要POD名字不变,那么DNS的记录也不会变,可能变的只能是POD IP,所以不能通过POD IP去访问它。

[root@node-1 ~]# cd deep-in-kubernetes/
[root@node-1 deep-in-kubernetes]# mkdir 10-statefulset
[root@node-1 deep-in-kubernetes]# cd 10-statefulset   

[root@node-1 10-statefulset]# kubectl label node gluster-01 statefulsetnode=statefulset
node/gluster-01 labeled
[root@node-1 10-statefulset]# kubectl label node gluster-02 statefulsetnode=statefulset
node/gluster-02 labeled

[root@node-1 10-statefulset]# kubectl get nodes --show-labels

image.png

顺序性

headless-service.yaml

[root@node-1 10-statefulset]# cat headless-service.yaml
apiVersion: v1
kind: Service
metadata:
#和其他yaml中的serviceName: springboot-web-svc对应起来
  name: springboot-web-svc
spec:
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
#跟以前的service区别,有这行
#这是headless service,不会有service的VIP,可以通过DNS的名字返回的是对应endpoint的IP列表。
  clusterIP: None
  selector:
    app: springboot-web

statefulset.yaml

[root@node-1 10-statefulset]# cp statefulset.yaml statefulset-modify.yaml
#修改
  template:
    metadata:
      labels:
        app: springboot-web
    spec:
      nodeSelector:
        statefulsetnode: statefulset
      containers:
[root@node-1 10-statefulset]# cat statefulset-modify.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: springboot-web
spec:
#跟前面deployment的区别就是多了serviceName
#serviceName告诉StatefulSet用哪个headless service去保证每个POD的解析。
#和headless-service.yaml中的springboot-web-svc名字对应起来。
  serviceName: springboot-web-svc
  replicas: 2
  selector:
    matchLabels:
      app: springboot-web
  template:
    metadata:
      labels:
        app: springboot-web
    spec:
      nodeSelector:
        statefulsetnode: statefulset
      containers:
      - name: springboot-web
        image: hub.mooc.com/kubernetes/springboot-web:v1
		#端口
        ports:
        - containerPort: 8080
		#健康检查
        livenessProbe:
          tcpSocket:
            port: 8080
          initialDelaySeconds: 20
          periodSeconds: 10
          failureThreshold: 3
          successThreshold: 1
          timeoutSeconds: 5
        #健康检查
		readinessProbe:
          httpGet:
            path: /hello?name=test
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 20
          periodSeconds: 10
          failureThreshold: 1
          successThreshold: 1
          timeoutSeconds: 5

运行

[root@node-1 10-statefulset]# kubectl apply -f headless-service.yaml  
service/springboot-web-svc created

#注意这里的service无IP。
[root@node-1 10-statefulset]# kubectl get service
NAME                                                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
springboot-web-svc                                       ClusterIP   None            <none>        80/TCP    12s


[root@node-1 10-statefulset]# kubectl apply -f statefulset-modify.yaml 
statefulset.apps/springboot-web created

[root@node-1 10-statefulset]# kubectl get statefulset           
NAME             READY   AGE
springboot-web   2/2     3m55s

[root@node-1 10-statefulset]# kubectl get pods -l app=springboot-web -o wide
NAME               READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES
springboot-web-0   1/1     Running   0          3m58s   10.233.99.11   gluster-02   <none>           <none>
springboot-web-1   1/1     Running   0          3m29s   10.233.19.13   gluster-01   <none>           <none>

测试顺序性

之前deployment并不是这样,是有一个唯一的入口做负载均衡,随机的去访问到某一个POD。

这里可以通过编号,具体的指定其中一个POD,访问方式也是一样的,这样就可以在任何地方通过这个名字+编号的方式去访问某一个实例,并且让每个POD都具备了不同的磁盘空间。包括实例之间具体的通讯都可以使用这种方式。

这就是顺序性带来的能力。

发现它们的文件并没有共享,都访问的是自己的空间。

#加-w监控下statefulset-modify.yaml中服务的创建过程(动态一行行打印出过程)。
[root@node-1 10-statefulset]# kubectl get pods -l app=springboot-web -o wide -w

image.png

#当web-0处于running状态/1-READY时,那么web -1才会去创建。这样的过程。

这里跟deployment创建的名字相差比较大,deployment创建的名字是相对固定的,而spring-web在这里是来源于StatefulSet的名字,后面固定格式-数字编号,编号从0开始,-0,-1,-2,并且是先启动-0的,-0启动起来并通过健康检查之后,才会出现-1的,按顺序依次创建容器,这就是POD的命名规则。

#删POD,看一下重建的过程,还是从0开始启动,不会说因为删掉了,就同时把它们启动起来。启动的过程始终会保持顺序性。

[root@node-1 10-statefulset]# kubectl get pods -l app=springboot-web -o wide -w

image.png

#这里看一个从正常运行到创建再删除的全部过程。这个命令输出的结果是动态显示的。

image.png

#看下hostname跟POD名字一样

[root@gluster-02 ~]# crictl exec -it 47d8af0778bd8 sh
/ # hostname
springboot-web-0
[root@gluster-01 ~]# crictl exec -it bc63e75fdfe22 sh
/ # hostname
springboot-web-1
#测试DNS:如何通过具体的名字访问到某一个POD?
#POD NAME “.”后面要跟一个SERVICE NAME “.” 后面要跟NAMESPACE(不加也可以)

#在上述两个容器中,互相ping对应的服务都能ping通。
#但至少要加后缀service,否则在gluster-02不能ping通gluster-01的POD。
/ # ping springboot-web-0.springboot-web-svc
PING springboot-web-0.springboot-web-svc (10.233.99.12): 56 data bytes
64 bytes from 10.233.99.12: seq=0 ttl=64 time=0.030 ms
64 bytes from 10.233.99.12: seq=1 ttl=64 time=0.090 ms
/ # ping springboot-web-0.springboot-web-svc.default
PING springboot-web-0.springboot-web-svc.default (10.233.99.12): 56 data bytes
64 bytes from 10.233.99.12: seq=0 ttl=64 time=0.057 ms
64 bytes from 10.233.99.12: seq=1 ttl=64 time=0.048 ms
#这里ping后,都是各自POD的虚拟IP。
/ # ping springboot-web-1.springboot-web-svc
PING springboot-web-1.springboot-web-svc (10.233.19.14): 56 data bytes
64 bytes from 10.233.19.14: seq=0 ttl=64 time=0.035 ms
64 bytes from 10.233.19.14: seq=1 ttl=64 time=0.107 ms
/ # ping springboot-web-1.springboot-web-svc.default
PING springboot-web-1.springboot-web-svc.default (10.233.19.14): 56 data bytes
64 bytes from 10.233.19.14: seq=0 ttl=64 time=0.029 ms
64 bytes from 10.233.19.14: seq=1 ttl=64 time=0.064 ms

#这就是在不同容器ping不同POD的结果。
/ # ping springboot-web-1
ping: bad address 'springboot-web-1'
/ # ping springboot-web-0
ping: bad address 'springboot-web-0'
#是否能互相ping通可以看下容器hosts中的设计。
/ # cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
10.233.99.12    springboot-web-0.springboot-web-svc.default.svc.cluster.local   springboot-web-0

#是否能互相ping通也可以查看DNS规则
/ # cat /etc/resolv.conf 
search default.svc.cluster.local svc.cluster.local cluster.local
#这俩容器中的nameserver是一样的
#配置了这个DNS SERVER的地址,是一个LOCAL DNS CACHE。
nameserver 169.254.25.10
#它默认的搜索范围是这几个,所以在default命名空间下ping POD时,只需要在前面加POD NAME 和SERVICE NAME就可以了。
options ndots:5


#也能ping通其他服务
/ # ping 172.16.1.25
PING 172.16.1.25 (172.16.1.25): 56 data bytes
64 bytes from 172.16.1.25: seq=0 ttl=64 time=0.123 ms
64 bytes from 172.16.1.25: seq=1 ttl=64 time=0.092 ms
/ # ping 10.233.5.14
PING 10.233.5.14 (10.233.5.14): 56 data bytes
64 bytes from 10.233.5.14: seq=0 ttl=62 time=0.734 ms
64 bytes from 10.233.5.14: seq=1 ttl=62 time=1.934 ms
/ # ping 10.200.70.160
PING 10.200.70.160 (10.200.70.160): 56 data bytes
64 bytes from 10.200.70.160: seq=0 ttl=64 time=0.111 ms
64 bytes from 10.200.70.160: seq=1 ttl=64 time=0.072 ms

持久存储(基于11-4/5)

[root@node-1 10-statefulset]# kubectl delete -f statefulset-modify.yaml
statefulset.apps "springboot-web" deleted

区分持久存储的前提是要有共享存储。也就是上一节的实验环境,要有底层的存储服务,用storageClass可以去动态的创建PV。

在这个基础之上,就可以看statefulSet-volume。

如果POD对存储有持久性、并且有独占的需要,就需要用到volumeClaimTemplate,它会为每一个POD自动生成一个PVC,从而保证每个POD有一个独立的volume。

上面”顺序性”案例用到的都是springboot-web服务,并不够真实。

statefulset-volume.yaml

[root@node-1 10-statefulset]# cp statefulset-volume.yaml statefulset-volume-modify.yaml 
#修改
  template:
    metadata:
      labels:
        app: springboot-web
    spec:
      nodeSelector:
        statefulsetnode: statefulset
      containers:
[root@node-1 10-statefulset]# cat statefulset-volume-modify.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: springboot-web
spec:
#跟前面deployment的区别就是多了serviceName
#serviceName告诉StatefulSet用哪个headless service去保证每个POD的解析。和headless-service.yaml中的springboot-web-svc名字对应起来。
  serviceName: springboot-web-svc
  replicas: 2
  selector:
    matchLabels:
      app: springboot-web
  template:
    metadata:
      labels:
        app: springboot-web
spec:
      nodeSelector:
        statefulsetnode: statefulset
      containers:
      - name: springboot-web
        image: hub.mooc.com/kubernetes/springboot-web:v1
        ports:
        - containerPort: 8080
		#健康检查
        livenessProbe:
          tcpSocket:
            port: 8080
          initialDelaySeconds: 20
          periodSeconds: 10
          failureThreshold: 3
          successThreshold: 1
          timeoutSeconds: 5
		#健康检查  
        readinessProbe:
          httpGet:
            path: /hello?name=test
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 20
          periodSeconds: 10
          failureThreshold: 1
          successThreshold: 1
          timeoutSeconds: 5
        volumeMounts:
        - name: data
		#挂载目录
          mountPath: /mooc-data
#跟POD的模板相似
#这里是创建PVC的模板(跟PVC的定义非常相似,功能:自动去创建多个PVC,是一个PVC的模板。)
#为什么这里不指定一个PVC呢?
#像上一节讲的那样,先创建PVC,再指定一下。这样就不共享了吗?但是不满足现在的需求。
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteOnce
	  #指定storageClassName
      storageClassName: glusterfs-storage-class
      resources:
        requests:
          storage: 1Gi

运行

[root@node-1 10-statefulset]# kubectl apply -f statefulset-volume-modify.yaml
statefulset.apps/springboot-web created
[root@node-1 10-statefulset]# kubectl get statefulset
NAME             READY   AGE
springboot-web   2/2     5m42s

[root@node-1 10-statefulset]# kubectl get pvc
[root@node-1 10-statefulset]# kubectl get pv
[root@node-1 10-statefulset]# kubectl get pods -l app=springboot-web -o wide

#发现已经有两个PVC,名字是data-springboot-web-数字编号,状态是BOUND,自动根据storageClass自动创建出了PV,跟预期结果一致。

#上一节”顺序性”是通过storeClass自动创建的,这里的PVC也是自动创建的,但是通过volumeClaimTemplates自动创建的PVC。

image.png

#尝试写文件

[root@gluster-01 ~]# crictl ps
CONTAINER           IMAGE               CREATED                  STATE               NAME                ATTEMPT             POD ID
a85e930aaa9f0       98458caa43631       11 minutes ago           Running             springboot-web      0                   1351cc24a6358

[root@gluster-02 ~]#  crictl ps
CONTAINER           IMAGE               CREATED                  STATE               NAME                ATTEMPT             POD ID
fec914b9074de       98458caa43631       11 minutes ago           Running             springboot-web      0                   3b68bedbfd885

#互相交替写入

[root@gluster-01 ~]# crictl exec -it a85e930aaa9f0 sh
/ # cd /mooc-data/
/mooc-data # ls
/mooc-data # echo "hello1" > hello1
/mooc-data # cat hello1
hello1
/mooc-data # cat hello1 
hello1
/mooc-data # ls
hello1
/mooc-data # echo "hahah1" > /mooc-data/file
/mooc-data # cat file
hahah1

[root@gluster-02 ~]# crictl exec -it fec914b9074de sh
/ # cd /mooc-data/
/mooc-data # ls
/mooc-data # echo "hello2" > hello2
/mooc-data # cat hello2
hello2
/mooc-data # ls
hello2
/mooc-data # cat file
cat: can't open 'file': No such file or directory
/mooc-data # echo "hahah2" > /mooc-data/file
/mooc-data # cat file
hahah2

发现它们的文件并没有共享,都访问的是自己的空间。

说明持久化存储已经确实具体绑定到了某一个编号的POD上面。并且让每个POD都具备了不同的磁盘空间。


标题:Kubernetes(十一)落地实践(11.6)StatefulSet --- 有状态应用的守护者
作者:yazong
地址:https://blog.llyweb.com/articles/2022/12/09/1670591293154.html