上一篇文章中,我们讲了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重启后数据就不会丢失了。