StatefulSet概述
StatefulSet将真实世界的应用状态抽象为两种情况
- 拓扑状态,应用的多个实例之间不是完全对等的关系,这些应用实例必须按照某些顺序启动,并且在删除后重新创建之后,必须和原来pod的网络表示一样,这样原先的访问者才能使用同样的方法来访问这个新的pod
- 存储状态,多个实例分别绑定不同的存储数据,这种情况下pod-A第一次读到的数据和间隔了一会之后读到的数据应该是同一份。
StatefulSet的核心功能,就是通过某种方式记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态
无头服务
kubernetes中可以通过Service将一组pod暴露给外界,这个service怎么被访问
-
第一种方式,以service的虚拟IP方式。其实没有这个对应的IP,只是在集群中有将这个虚拟IP转发到对应的pod的ip上的路由规则
-
第二种方式,以service的DNS方式,比如访问
my-svc.my-namespace.svc.cluster.local这条dns记录,就可以访问到名字为my-svc的service所代理的某个pod上。这种方式下还分两种处理方法- 第1种处理方法,normal service,这种情况下访问
my-svc.my-namespace.svc.cluster.local解析道德就是my-svc这个service的虚拟IP,之后的流程和第一种方式一样 - 第2种处理方法,就是
Headless Service,访问my-svc.my-namespace.svc.cluster.local解析到的直接就是my-svc代理的某个pod的地址。
- 第1种处理方法,normal service,这种情况下访问
下面是一个标准的无头服务的配置文件,和普通的Service配置文件不同的是,这里有一个clusterIP: None,表示没有一个VIP作为头,这个service被创建后不会分配虚拟IP,而是以dns记录的方式暴露其代理的pod
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
这种方式创建的Headless Service,其所代理的pod的ip地址会被绑定到一个dns记录
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
这样只要知道pod的名字和对应的service的名字,就一定会通过这条dns记录访问到pod的ip了
StatefulSet使用DNS记录来维持pod的拓扑状态
比如下面的配置文件
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
name: web
注意这个配置文件多了一个serviceName=nginx,这个字段的作用是告诉StatefulSet控制器,在控制循环时使用"nginx"这个Headless Service来保证pod的可解析身份。
此时通过kubectl创建上面的Service和StatefulSet
$ kubectl create -f svc.yaml
$ kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 10s
$ kubectl create -f statefulset.yaml
$ kubectl get statefulset web
NAME DESIRED CURRENT AGE
web 2 1 19s
$ kubectl get pods -w -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 19s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 20s
StatefulSet的pod拓扑标识
StatefulSet给它所管理的所有pod的名字进行编号,编号规则为
<statefulset name>-<ordinal index>,也就是说StatefulSet通过这种编号方式来记录pod的拓扑状态
在这两个pod创建完之后,可以通过exec命令进入容器中查看他们的hostname
$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1
可以通过另外创建一个busybox容器,使用nslookup web-0.nginx来解析对应的service下的pod
此时如果
kubectl delete pod -l app=nginx,会发现把pod删除之后,kubernets会按照原来编号的顺序,重新创建两个pod,并且分配与原来相同的网络身份web-0.nginx和web-1.nginx
StatefulSet存储状态管理机制
这个机制主要使用的是一个叫作Persistent Volume Claim的功能。
要在一个pod里声明vloume,只要在pod里加上spec.volumes字段即可,在此volume字段里需要填写响应的信息,这就会造成信息暴露。
另一方面如果在开发时,对持久化存储项目(比如Ceph等)不了解,也就不知道volume的类型,就无法编写对应的volume定义文件
这就产生了Persistent Volume Claim(PVC)和Persistent Volume(PV)对象,这样开发人员向使用一个volume,只需要简单的两部
- 第1步,定义一个PVC,声明想要的volume的属性
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
在这个PVC对象里,不需要任何关于Volume细节的字段,只有描述性的属性和定义
- 第2步,在应用的pod中,声明使用这个PVC
apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
在这个Pod的Volumes定义中,我们只需要声明它的类型是persistentVolumeClaim,然后指定PVC的名字,而完全不必关心Volume本身的定义。
接下来只要我们创这个PVC对象,Kubernetes就会自动为它绑定一个符合条件的Volume,而这个volume是由运维人员维护的Persistent Volume对象,如下是一个常见的PV对象
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-volume
labels:
type: local
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
rbd:
monitors:
# 使用 kubectl get pods -n rook-ceph 查看 rook-ceph-mon- 开头的 POD IP 即可得下面的列表
- '10.16.154.78:6789'
- '10.16.154.82:6789'
- '10.16.154.83:6789'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
keyring: /etc/ceph/keyring
在创建了这个PV对象后,由于它符合PVC的模式,kubernets就会将PVC对象绑定到这个PV上
StatefulSet存储状态管理实例
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
凡是被这个StatefulSet管理的Pod,都会声明一个对应的PVC;而这个PVC的定义就来自于volumeClaimTemplates这个模板字段。并且这个PVC的名字,会被分配一个与这个Pod完全一致的编号。
这个自动创建的PVC,与PV绑定成功后,就会进入Bound状态,这就意味着这个Pod可以挂载并使用这个PV
$ kubectl create -f statefulset.yaml
$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s
www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
这个StatefulSet创建出来的所有Pod,都会声明使用编号的PVC。比如,在名叫web-0的Pod的volumes字段,它会声明使用名叫www-web-0的PVC,从而挂载到这个PVC所绑定的PV。
即便删除掉这两个pod,在重建pod之后,两个pod依然会跟之前各自绑定的PV绑定在一起
StatefulSet控制器恢复这个Pod的过程
把一个Pod,比如web-0,删除之后,这个Pod对应的PVC和PV,并不会被删除,而这个Volume里已经写入的数据,也依然会保存在远程存储服务里
此时,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里的数据。
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的删除和再创建而自动更新。
最后,StatefulSet还为每一个Pod分配并创建一个同样编号的PVC。这样,Kubernetes就可以通过Persistent Volume机制为这个PVC绑定上对应的PV,从而保证了每一个Pod都拥有一个独立的Volume。
在这种情况下,即使Pod被删除,它所对应的PVC和PV依然会保留下来。所以当这个Pod被重新创建出来之后,Kubernetes会为它找到同样编号的PVC,挂载这个PVC对应的Volume,从而获取到以前保存在Volume里的数据。