概要
K8S如何来化解有状态的应用。
之前使用的编排对象都是deployment,之前开发的服务也都可以使用deployment管理起来。
但是deployment并不能胜任所有的工作。
之前的服务都有一个特征,
首先都是无状态的,每个POD都是无差别的,并且它们的实例之间没有顺序性。
但是在实际的场景下,并不是所有的应用都具备此种条件,特别是分布式应用,对于分布式应用,它们的多个实例之间,往往会存在一种关系,主从ZK、ETCD、redis,都有这种特征。
对于这种多个实例、不对等的应用,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
顺序性
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
#当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
#这里看一个从正常运行到创建再删除的全部过程。这个命令输出的结果是动态显示的。
#看下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。
#尝试写文件
[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