Kubernetes-StatefulSet
📑 官方文档
Design Idea
对于分布式应用,在它的多个实例之间,往往有依赖关系,比如:主从关系、主备关系。这种实例之间有不对等关系,以及实例外部数据有依赖关系应用,就被称为有状态应用(Stateful Application)。
得益于“控制器模式”的设计思想,Kubernetes项目很早就在Deployment的基础上,扩展出了对“有状态应用”的初步支持,这个编排功能就是:StatefulSet。
StatefulSet把真实世界里的应用状态,抽象为了两种情况:
- 拓扑状态:应用的多个实例之间不是完全对等关系,必须按照特定顺序启动。
- 存储状态:应用的多个实例分别绑定了不同的存储数据,访问数据有幂等性。
Stateful的核心功能,就是通过某种方式记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态。
Headless Service
Service是Kubernetes中用来将一组Pod暴露给外界访问的一种机制。比如,一个Deployment有3个Pod,那么可以定义一个Service,用户只要能访问到这个Service,它就能访问到某个具体的Pod。
- 第一种方式,是以Service的VIP(Virtual IP,即:虚拟IP)方式。比如:访问10.0.23.1这个Service的IP地址时,10.0.23.1其实就是一个VIP,它会把请求转发到该Service所代理的一个Pod上。
- 第二种方式,是以Service的DNS方式。比如:访问“my-svc.my-namespace.svc.cluster.local”这条DNS记录,就可以访问到名叫my-svc的Service所代理的某一个Pod。
而第二种Server DNS方式下,具体还可以分为两种处理方法:
- 第一种处理方法,是Normal Service。这种情况下,访问”my-svc.my-namespace.svc.cluster.local”解析到的,正是my-svc这个Service的VIP,后面的流程就跟VIP方式一致了;
- 第二种处理方法,正是Headless Service。这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是my-svc代理的某一个Pod的IP地址。
这里的区别在于,Headless Service不需要分配一个VIP,而是可以直接以DNS记录的方式解析出被代理Pod的IP地址。
Practice1
下面是一个标准的Headless Service对应的YAML文件:
1 | apiVersion: v1 |
Headless Service,其实仍然是一个Service的YAML文件。只不过,它的clusterIP字段的值是:None,没有一个VIP作为头,这就是Headless的含义。所以,这个Service被创建后并不会被分配一个VIP,而是会以DNS记录的方式暴露出它所代理的Pod。
按照这样的方式创建了一个Headless Service之后,它所代理的所有Pod的IP地址,都会被绑定一个这样的DNS记录,如下所示:
1 | <pod-name>.<svc-name>.<namespace>.svc.cluster.local |
这个DNS记录,正是Kubernetes项目为Pod分配的唯一的“可解析身份”(Resolvable Identidy)。有了这个“可解析身份”,只要知道了一个Pod的名字,以及它对应的Service的名字,就可以非常确定地通过这条DNS记录访问到Pod的IP地址。
编写一个StatefulSet的YAML文件,如下所示:
1 | apiVersion: apps/v1 |
这个YAML文件中,有一个serviceName=nginx字段,用于告诉StatefulSet控制器:在执行控制循环(Control Loop)的时候,请使用nginx这个Headless Service来保证Pod的“可解析身份”。
创建上面的Service和StatefulSet:
1 | kubectl create -f headless-service.yaml |
通过上面的Pod创建过程,可以看到StatefulSet给它所管理的所有Pod的名字,进行了编号,编号规则是:<statefulset-name>-<ordinal index>。而且这些编号都是从0开始累加,与Stateful的每个Pod实例一一对应,绝不重复。
更重要的是,这些Pod的创建,也是按照编号顺序进行的。比如,在web-0进入到Running状态、并且细分状态(Conditions)成为Ready之前,web-1会一直处于Pending状态。
使用kubectl exec
命令进入到容器中查看它们的hostname:
1 | kubectl exec web-0 -- sh -c 'hostname' |
可以看到,这两个Pod的hostname与Pod名字是一致的,都被分配了对应的编号。
接下来,再试着以DNS的方式,访问一下这个Headless Service:
1 | kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh |
这个命令启动了一个一次性的Pod,因为–rm意味着Pod退出后就会被删除掉。然后,在这个Pod的容器里面,尝试用nslookup命令,解析一下Pod对应的Headless Service:
1 | nslookup web-0.nginx |
从nslookup命令的输出结果中,可以看到,在访问web-0.nginx的时候,最后解析到的,正是web-0这个Pod的IP地址;而当访问web-1.nginx的时候,解析到的则是web-1的IP地址。
这时候,如果在另一个Terminal里把这两个“有状态应用”的Pod删掉。
1 | kubectl delete pod -l app=nginx |
然后,在当前Terminal里Watch一下这两个Pod的状态变化:
1 | kubectl get pod -w -l app=nginx |
可以看到,把这两个Pod删除之后,Kubernetes会按照原先编号点顺序,创建出两个新的Pod。并且,Kubernetes依然为它们分配了原来相同的“网络身份”:web-0.nginx和web-1.nginx。
通过这种严格的对应规则,StatefulSet就保证了Pod网络标识的稳定性。
比如,如果 web-0 是一个需要先启动的主节点,web-1 是一个后启动的从节点,那么只要这个 StatefulSet 不被删除,你访问 web-0.nginx 时始终都会落在主节点上,访问 web-1.nginx 时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。
回到最开始的Terminal,再用nslookup
命令,查看一下这个新的Pod对应的Headless Service的话:
1 | nslookup web-0.nginx |
可以看到,在这个Stateful中,这两个新的Pod的“网络标识”,再次解析到了正确的IP地址。
通过这种方法,Kubernetes就成功地将Pod的拓扑状态,按照Pod的“名字+编号”的方式固定了下来。此处,Kubernets还为每一个Pod提供了一个固定并且唯一的访问入口,即:这个Pod对应的DNS记录。
这些状态,在StatefulSet的整个生命周期里都保持不变,绝不会因为对应Pod的删除或者重新创建而失效。
尽管web-0.nginx这条记录本身不会变,但它解析到的Pod的IP地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,必须使用DNS记录或者hostname的方式,而绝不应该直接访问这些Pod的IP地址。
Persistent Volume Claim
Kubernetes引入了一组叫作Persistent Volume Claim(PVC)和Persistent Volume(PV)的API对象,降低了用户声明和使用暴露Volume的门槛。
下面的例子,声明了Ceph RBD类型的Volume的Pod:
1 | apiVersion: v1 |
第一步:定义一个PVC,声明想要的Volume属性:
1 | apiVersion: v1 |
在这个PVC对象里,不需要任何关于Volume细节的字段,只有描述性的属性和定义。
- Storage: 1Gi,表示需要的Volume大小至少是1GiB;
- accessModes: ReadWriteOnce,表示这个Volume的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。
第二步:在应用的Pod中,声明使用这个PVC:
1 | apiVersion: v1 |
在这个Pod的Volumes定义中,只需要声明它的类型是persistentVolumeClaim,然后指定PVC的名字,而完全不必关系Volume本身的定义。
这时候,只要创建这个PVC对象,Kubernetes就会自动为它绑定一个符合条件的Volume,Volume来自于运维人员维护的PV(Persistent Volume)对象。
比如下面一个PV对象的YAML文件:
1 | kind: PersistentVolume |
可以看到,这个PV对象的spec.rbd字段,正是CephRBD Volume的详细定义。
所以,Kubernetes中PVC和PV的设计,实际上类似于“接口”和“实现”的思想。开发者只要知道并会使用接口,即:PVC;而运维人员则负责给接口绑定具体的实现,即:PV。这种解耦,就避免了因为向开发者暴露过多的存储系统细节而带来的隐患。此外,这种职责的分离,往往也意味着出现事故时可以更容易定位问题和明确责任,从而避免“扯皮”现象的出现。
PVC、PV的设计,也使得StatefulSet对存储状态的管理成为了可能。
Practice2
如下,在Practice1的StatefulSet的YAML文件的基础上添加一个volumeClaimTemplates
字段:
1 | apiVersion: apps/v1 |
volumeClaimTemplates
跟Deployment里Pod模板(PodTemplate)的作用类似。也就是说,凡是被这个Stateful管理的Pod,都会声明一个对应的PVC;而这个PVC的定义,就来自于volumeClaimTemplates
这个模板字段。更重要的是,这个PVC的名字,会被分配到一个与这个Pod完全一致的编号。
这个自动创建的PVC,与PV绑定成功后,就会进入到Bound状态,这就意味着这个Pod可以挂载并使用这个PV了。
创建上面的StatefulSet对象,就能看到Kubernetes集群里出现了两个PVC:
1 | kubectl create -f stateful-volume.yaml |
可以看到,这些PVC,都以“<PVC名字>-<Statul名字>-<编号>”的方式命名,并且处于Bound状态。
这个StatefulSet创建出来的所有Pod,都会声明使用编号的PVC。比如,在名叫web-0的Pod的volumes字段,它会声明使用名叫www-web-0的PVC,从而挂载到这个PVC所绑定的PV。
使用如下的指令,在Pod的Volume目录里写入一个文件,来验证上述Volume的分配情况:
1 | for i in 0 1; \ |
如上所示,通过kubectl exec
指令,在每个Pod的Volume目录里,写入了一个index.html文件。这个文件的内容,正是Pod的hostname。
1 | for i in 0 1; \ |
现在,删除这个两个Pod,看一下Volume中的文件是否会丢失。
1 | kubectl delete pod -l app=nginx |
在被删除之后,这两个Pod会按照编号的顺序被重新创建出来,验证一下:
1 | for i in 0 1; \ |
请求结果没有改变,也就是说,原先与名交web-0的Pod绑定的PV,在这个Pod被重新创建之后,依然同新的名交web-0的Pod绑定在了一起。对于Pod web-1来说,也是完全一样的情况。
Storage State
分析一下StatefulSet控制器恢复Pod的过程。
首先,把一个Pod,比如web-0,删除之后,这个Pod对应的PVC和PV,并不会被删除,而这个Volume里已经写入的数据,也依然会保存在远程存储服务里(比如在这个例子里用到的Ceph服务器)。
此时,StatefulSet控制器发现,一个名叫web-0的Pod消失了。所以,控制器就会重新创建一个新的、名字还是叫作web-0的Pod来,纠正这个不一致的情况。
需要注意的是,在这个新的Pod对象的定义里,它声明使用的PVC的名字,还是叫作:www-web-0。这个PVC的定义,还是来自于PVC模板(volumeClaimTemplates),这是StatefulSet创建Pod的标准流程。
所以,在这个新的web-0 Pod被创建出来之后,Kubernetes为它查找名叫www-web-0的PVC时,就会直接找到旧Pod遗留下来的同名的PVC,进而找到跟这个PVC绑定在一起的PV。
这样,新的Pod就可以挂载到旧Pod对应的那个Volume,并且获取在Volume里的数据。
通过这种方式,Kubernetes的StatefulSet就实现了对应用存储状态的管理。
首先,StatefulSet的控制器直接管理的是Pod。这是因为,StatefulSet里的不同Pod实例,不再像ReplicaSet中那样都是完全一样的,而是有了细微区别的。比如,每个Pod的hostname、名字等都是不同的、携带了编号的。而StatefulSet区分这些实例的方式,就是通过在Pod的名字里加上实现约定好的编号。
其次,Kubernetes通过Headless Service,为这些有编号的Pod,在DNS服务器中生成带有同样编号的DNS记录。只要StatefulSet能够保证这些Pod名字里的编号不变,那么Service里类似于web-0.nginx.default.svc.cluster.local这样的DNS记录也就不会变,而这条记录解析出来的Pod的IP地址,则会随着后端Pod的删除和再创建而自动更新。这当然是Service机制本身的能力,不需要StatefulSet操心。
最后,StatefulSet还为每一个Pod分配并创建一个编号的PVC。这样,Kubernetes就可以通过Persistent Volume机制为这个PVC绑定上对应的PV,从而保证了每一个Pod都拥有一个独立的Volume。
在这种情况下,即使Pod被删除,它所对应的PVC和PV依然会保留下来。所以当这个Pod被重新创建出来之后,Kubernetes会为它找到同样编号的PVC,挂载这个PVC对应的Volume,从而获取到以前保存在Volume里的数据。