Volume、PV、PVC、StorageClass由来
先思考一个问题,为什么会引入Volume
这样一个概念?
“
答案很简单,为了实现数据持久化,数据的生命周期不随着容器的消亡而消亡。
”
在没有介绍Kubernetes Volume
之前,先来回顾下Docker Volume
,Docker Volume
常用使用方式有两种,
volumes
通过这种方式,
Docker
管理宿主机文件系统的一部分,默认位于
/var/lib/docker/volumes
目录中,由于在创建时没有创建指定数据卷,
docker
自身会创建默认数据卷;bind mounts
通过这种方式,可以把容器内文件挂载到宿主机任意目录。
既然有了Docker Volume
,为啥Kubernetes
又搞出了自己的Volume
?谷歌的标新立异?
“
答案是否定的,Kubernetes Volume
和Docker Volume
概念相似,但是又有不同的地方,Kubernetes Volume
与Pod
的生命周期相同,但与容器的生命周期不相关。当容器终止或重启时,Volume
中的数据也不会丢失。当Pod
被删除时,Volume
才会被清理。并且数据是否丢失取决于Volume
的具体类型,比如emptyDir
类型的Volume
数据会丢失,而持久化类型的数据则不会丢失。另外Kubernetes
提供了将近20
种Volume
类型。
”
现在有了Kubernetes
的Volume
,我们就可以完全可以在Yaml
编排文件中填写上Volume
是字段,如下nfs
所示:
....
volumes:
- name: static-nfs
nfs:
server: 12.18.17.240
path: /nfs/data/static
如果你使用ceph
作为存储插件,你可以在编排文件中这样定义:
volumes:
- name: ceph-vol
cephfs:
monitors:
- 12.18.17.241:6789
- 12.18.17.242:6789
user: admin
secretRef:
name: ceph-secret
readOnly: true
当然只要是Kubernetes
已经实现的数据卷类型,你都可以按照如上方式进行直接在Yaml
编排文件中定义使用。
看到这里其实已经完成了80%
的工作,那么为什么还要设计多此一举的PV
呢?这个问题先搁置下,后面会有说明。
在没有说明为什么要设计多此一举的PV PVC
之前,先来看看什么是PV PVC
?
“
PV
是对持久化存储数据卷的一种描述。
”
PV
通常是由运维人员提前在集群里面创建等待使用的一种数据卷。如下所示:
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.244.1.4
path: "/nfs"
“
PVC
描述的是持久化存储的属性,比如大小、读写权限等。
”
PVC
通常由开发人员创建,如下所示:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
而用户创建的PV PVC
必须绑定完成之后才能被利用起来。而PV PVC
绑定起来的前提是PV
中spec
中声明字段大小、权限等必须满足PVC
的要求。
成功绑定之后,就可以在Pod Yaml
编排文件中定义和使用。如下所示:
apiVersion: v1
kind: Pod
metadata:
labels:
role: web
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
volumeMounts:
- name: nfs
mountPath: "/usr/share/nginx/html"
volumes:
- name: nfs
persistentVolumeClaim:
claimName: nfs
看到这里,我们还会认为仅仅是PV
对Volume
多了一层抽象,并不见得比直接在Yaml
中声明Volume
高明多少。仔细思考下,我们为什么能够直接在Yaml
中直接定义Volume
?因为Kubernetes
已经帮助我们实现了这种Volume
类型,如果我们有自己的存储类型,而Kubernetes
中并没有实现,这种是没有办法直接在Yaml
编排文件中直接定义Volume
的。这个时候PV PVC
面向对象的设计就体现出其价值了。这也是在软件开发领域经常碰到的问题,开源软件无法满足要求,但也没有提供出可扩展的接口,没办法,只能重新造轮子。
我们在开发过程中经常碰到这样一个问题,在Pod
中声明一个PVC
之后,发现Pod
不能被调度成功,原因是因为PVC
没有绑定到合适的PV
,这个时候要求运维人员创建一个PV
,紧接着Pod
调度成功。刚才上在介绍PV PVC
,它们的创建过程都是手动,如果集群中需要成千上万的PV
,那么运维人员岂不累死?在实际操作中,这种方式根本行不通。所以Kubernetes
给我们提供了一套自动创建PV
的机制Dynamic Provisioning
.在没有介绍这套自动创建机制之前,先看看Static Provisioning
,什么是Static Provisioning
?刚才人工创建PV PVC
的方式就是Static Provisioning
。你可以在PV PVC
编排文件中声明StorageClass
,如果没有声明,那么默认为"".具体交互流程如下所示:
首先由集群管理员事先去规划这个集群中的用户会怎样使用存储,它会先预分配一些存储,也就是预先创建一些 PV
;然后用户在提交自己的存储需求(PVC
)的时候,Kubernetes
内部相关组件会帮助它把PVC PV
做绑定;最后pod
使用存储的时候,就可以通过PVC
找到相应的PV
,它就可以使用了。不足之处也非常清楚,首先繁琐,然后运维人员无法预知开发人员的真实存储需求,比如运维人员创建了多个100Gi
的PV
存储,但是在实际开发过程中,开发人员只能使用10Gi
,这就造成了资源的浪费。当然Kubernetes
也为我们提供更好的使用方式,即Dynamic Provisioning
它是什么呢?
“
Dynamic Provisioning
包含了创建某种PV
所需要的参数信息,类似于一个创建PV
的模板。具体交互流程如下所示:
”
Kubernetes
集群中的控制器,会结合PVC
和StorageClass
的信息动态生成用户所需要的PV
,将PVC PV
进行绑定后,pod
就可以使用PV
了。通过 StorageClass
配置生成存储所需要的存储模板,再结合用户的需求动态创建PV
对象,做到按需分配,在没有增加用户使用难度的同时也解放了集群管理员的运维工作。
动态PV使用
Dynamic Provisioning
上面提到过,运维人员不再预分配PV
,而只是创建一个模板文件,这个模板文件正是StorageClass
。下面以NFS
为例进行说明,动态PV
的整个使用过程。
#安装nfs
yum -y install nfs-utils rpcbind
#开机自启动
systemctl enable rpcbind nfs-server
#配置nfs 文件
echo "/nfs/data *(rw,no_root_squash,sync)" >/etc/exports
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-provisioner
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get","create","list", "watch","update"]
- apiGroups: ["extensions"]
resources: ["podsecuritypolicies"]
resourceNames: ["nfs-provisioner"]
verbs: ["use"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-provisioner
subjects:
- kind: ServiceAccount
name: nfs-provisioner
namespace: logging
roleRef:
kind: ClusterRole
name: nfs-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-client-provisioner
spec:
selector:
matchLabels:
app: nfs-client-provisioner
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccount: nfs-provisioner
containers:
- name: nfs-client-provisioner
image: quay.io/external_storage/nfs-client-provisioner:latest
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfs-client
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: fuseim.pri/ifs
- name: NFS_SERVER
value: 12.18.7.20
- name: NFS_PATH
value: /nfs/data
volumes:
- name: nfs-client
nfs:
server: 12.18.7.20
path: /nfs/data
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-storage
provisioner: fuseim.pri/ifs
reclaimPolicy: Retain
这些参数是通过Kubernetes
创建存储的时候,需要指定的一些细节参数。对于这些参数,用户是不需要关心的,像这里provisioner
指的是使用nfs
的置备程序。ReclaimPolicy
就是说动态创建出来的PV
,当使用方使用结束、Pod
及 PVC
被删除后,这块PV
应该怎么处理,我们这个地方写的是Retain
,意思就是说当使用方pod PVC
被删除之后,这个PV
会保留。
- 提交完成模板文件之后,用户只需要在
Pod yaml
文件定义
PVC
,即可自动创建
PV
和
PVC
。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: es
spec:
........
template:
metadata:
labels:
app: elasticsearch
spec:
.........
initContainers:
........
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2
.......
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: nfs-storage
resources:
requests:
storage: 50Gi`
Capacity
:存储对象的大小;
AccessModes
:也是用户需要关心的,就是说使用这个PV
的方式。它有三种使用方式:ReadWriteOnce
是单node
读写访问;ReadOnlyMany
是多个node
只读访问,常见的一种数据共享方式;ReadWriteMany
是多个node
上读写访问;
StorageClassName
:StorageClassName
这个我们刚才说了,动态Provisioning
时必须指定的一个字段,就是说我们要指定到底用哪一个模板文件来生成PV
。
Kubernetes存储架构
PV Controller
: 负责
PV PVC
的绑定、生命周期管理,并根据需求进行数据卷的
Provision Delete
操作AD Controller
:负责存储设备的
Attach Detach
操作,将设备挂载到目标节点Volume Manager
:管理卷的
Mount Unmount
操作、卷设备的格式化以及挂载到一些公用目录上的操作Volume Plugins
:它主要是对上面所有挂载功能的实现。
PV Controller、AD Controller、Volume Manager
主要是进行操作的调用,而具体操作则是由
Volume Plugins
实现的。根据源码的位置可将
Volume Plugins
分为
In-Tree
和
Out-of-Tree
两类:
In-Tree
表示源码是放在
Kubernetes
内部的(常见的
NFS、cephfs
等),和
Kubernetes
一起发布、管理与迭代,缺点是迭代速度慢、灵活性差;
Out-of-Tree
的
Volume Plugins
的代码独立于
Kubernetes
,它是由存储
提供商实现的,目前主要有
Flexvolume CSI
两种实现机制,可以根据存储类型实现不同的存储插件Scheduler
:实现对
Pod
的调度能力,会根据一些存储相关的的定义去做存储相关的调度
动态PV交互流程
PV Controller
会观察
ApiServer
,如果它发现一个
PVC
已经创建完毕但仍然是未绑定的状态,它就会试图把一个
PV
和
PVC
绑定Provision
就是从远端上一个具体的存储介质创建一个
Volume
,并且在集群中创建一个
PV
对象,然后将此
PV
和
PVC
进行绑定Scheduler
进行多个维度考量完成后,把
Pod
调度到一个合适的
Node
Kubelet
不断
watch APIServer
是否有
Pod
要调度到当前所在节点Pod
调度到某个节点之后,它所定义的
PV
还没有被挂载(
Attach
),此时
AD Controller
就会调用
VolumePlugin
,把远端的
Volume
挂载到目标节点中的设备上(
/dev/vdb
);当
Volum Manager
发现一个
Pod
调度到自己的节点上并且
Volume
已经完成了挂载,它就会执行
mount
操作,将本地设备(也就是刚才得到的
/dev/vdb
)挂载到
Pod
在节点上的一个子目录中- 启动容器,并将已经挂载到本地的
Volume
映射到容器中