kubernete编排技术三:StatefulSet
作者 | 朱晋君
来源 | 君哥聊技术(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地址或通过域名访问。如下图所示:
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重启后数据就不会丢失了。