StatefulSet对存储状态的管理机制,主要使用的是一个叫作Persistent Volume Claim的功能。
要在一个Pod里声明Volume,只要在Pod里加上spec.volumes字段即可。然后,就可以在这个字段里定义一个具体类型的Volume了,比如:hostPath。
但是,如果你并不知道有哪些Volume类型可以用,也不会编写它们对应的Volume定义文件,要怎么办呢?
另一方面,关于Volume的管理和远程持久化存储的知识,也超越了开发者的知识储备,还会有暴露公司基础设施秘密的风险,不宜直接在Pod声明里将细节详细列出。
下面这个例子,是一个声明了Ceph RBD类型Volume的Pod:
apiVersion: v1
kind: Pod
metadata:
name: rbd
spec:
containers:
- image: kubernetes/pause
name: rbd-rw
volumeMounts:
- name: rbdpd
mountPath: /mnt/rbd
volumes:
- name: rbdpd
rbd:
monitors:
- '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
imageformat: "2"
imagefeatures: "layering"
如果不懂Ceph RBD的使用方法,那么这个Pod里Volumes字段,你十有八九也完全看不懂。其二,这个Ceph RBD对应的存储服务器的地址、用户名、授权文件的位置,也都被轻易地暴露给了全公司的所有开发人员,这是一个典型的信息被“过度暴露”的例子。
为了解决上面的问题,Kubernetes项目引入了一组叫作Persistent Volume Claim(PVC)和Persistent Volume(PV)的API对象,大大降低了用户声明和使用持久化Volume的门槛。
有了PVC之后,一个开发人员想要使用一个Volume,只需要简单的两步即可:
第一步:定义一个PVC,声明想要的Volume的属性:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
在这个PVC对象里,不需要任何关于Volume细节的字段,只有描述性的属性和定义。比如,storage: 1Gi,表示Volume大小至少是1 GiB;accessModes: ReadWriteOnce,表示这个Volume的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。
第二步:在应用的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又是从哪里来的呢?它们来自于由运维人员维护的PV(Persistent Volume)对象。
常见的PV对象的YAML文件:
kind: PersistentVolume
apiVersion: v1
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对象的spec.rbd字段,正是我们前面介绍过的Ceph RBD Volume的详细定义。而且,它还声明了这个PV的容量是10 GiB。这样,Kubernetes就会为我们刚刚创建的PVC对象绑定这个PV。
Kubernetes中PVC和PV的设计,实际上类似于“接口”和“实现”的思想。开发者只要知道并会使用“接口”,即:PVC;而运维人员则负责给“接口”绑定具体的实现,即:PV。
这种解耦,就避免了因为向开发者暴露过多的存储系统细节而带来的隐患。此外,这种职责的分离,往往也意味着出现事故时可以更容易定位问题和明确责任,从而避免“扯皮”现象的出现。
PVC、PV的设计,也使得StatefulSet对存储状态的管理成为了可能。仍然以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:1.9.1
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额外添加了一个volumeClaimTemplates字段。从名字就可以看出来,它跟Deployment里Pod模板(PodTemplate)的作用类似。也就是说,凡是被这个StatefulSet管理的Pod,都会声明一个对应的PVC;而这个PVC的定义,就来自于volumeClaimTemplates这个模板字段。更重要的是,这个PVC的名字,会被分配一个与这个Pod完全一致的编号。这个自动创建的PVC,与PV绑定成功后,就会进入Bound状态,这就意味着这个Pod可以挂载并使用这个PV了。
PVC可以理解为是一种特殊的Volume。只不过一个PVC具体是什么类型的Volume,要在跟某个PV绑定之后才知道。
PVC与PV的绑定得以实现的前提是,运维人员已经在系统里创建好了符合条件的PV(比如,我们在前面用到的pv-volume);或者,你的Kubernetes集群运行在公有云上,这样Kubernetes就会通过Dynamic Provisioning的方式,自动为你创建与PVC匹配的PV。
在使用kubectl create创建了StatefulSet之后,就会看到Kubernetes集群里出现了两个PVC:
$ 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
可以看到,这些PVC,都以“
使用如下所示的指令,在Pod的Volume目录里写入一个文件,来验证一下上述Volume的分配情况:
$ for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
通过kubectl exec指令,我们在每个Pod的Volume目录里,写入了一个index.html文件。这个文件的内容,正是Pod的hostname。比如,我们在web-0的index.html里写入的内容就是"hello web-0"。
此时,如果你在这个Pod容器里访问“http://localhost”,你实际访问到的就是Pod里Nginx服务器进程,而它会为你返回/usr/share/nginx/html/index.html里的内容:
$ for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done
hello web-0
hello web-1
继续使用kubectl delete命令删除这两个Pod,这些Volume里的文件会不会丢失呢?
$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
在被删除之后,这两个Pod会被按照编号的顺序被重新创建出来。而这时候,如果你在新创建的容器里通过访问“http://localhost”的方式去访问web-0里的Nginx服务:
# 在被重新创建出来的Pod容器里访问http://localhost
$ kubectl exec -it web-0 -- curl localhost
hello web-0
这个请求依然会返回:hello web-0。也就是说,原先与名叫web-0的Pod绑定的PV,在这个Pod被重新创建之后,依然同新的名叫web-0的Pod绑定在了一起。对于Pod web-1来说,也是完全一样的情况。
详细分析一下以上过程中,StatefulSet控制器恢复这个Pod的过程:
通过这种方式,Kubernetes的StatefulSet就实现了对应用存储状态的管理。
结合前一节的学习,梳理StatefulSet的工作原理:
StatefulSet的设计思想:StatefulSet其实就是一种特殊的Deployment,而其独特之处在于,它的每个Pod都被编号了。而且,这个编号会体现在Pod的名字和hostname等标识信息上,这不仅代表了Pod的创建顺序,也是Pod的重要网络标识(即:在整个集群里唯一的、可被访问的身份)。
有了这个编号后,StatefulSet就使用Kubernetes里的两个标准功能:Headless Service和PV/PVC,实现了对Pod的拓扑状态和存储状态的维护。
此文章为3月Day19学习笔记,内容来源于极客时间《深入剖析Kubernetes》