存储卷理论
docker存储卷
在以往的docker应用中,容器如果跨多节点运行,容器本身也拥有一个文件系统,通常而言,将容器从A节点迁至B节点,容器就需要关闭或删除已有镜像,并且会在被迁移的节点上重新建立一个容器,而此次容器的所有数据都会被遗留在原有节点上;
因为docker容器运行的方式依赖于其底层的镜像构建方式,而底层的镜像都是只读的,写操作只能发生在镜像栈最上面所添加的可写层,这个可写层保存在我们对应容器所运行的节点上, 如果删除容器,底层的只读部分镜像不会改变,但最上层的可写层被删除,那么写入的数据也将被改变,必然会丢失;
为了改变这种胶离容器的生命周期而存储数据,即使容器删除或删除数据依然存在,在docker中我们应该使用存储卷,docker的存储卷有两种: 绑定挂载卷、docker管理卷,不管是绑定的那种卷保存的数据都还是在当前节点上, 而不同的是绑定挂载卷可以由用户手动指定,而docker管理的卷,其路径是由docker进行管理和创建的,一般而言如果没有特殊指定删除容器时并不会删除其使用的存储卷;
而到了k8s时代,当我们Pod被调度运行时,它会运行于多个数据节点中的任意一个节点中,假设我们有一个k8s集群,当调度一个Pod时,它运行在A节点我们仍然使用本地存储卷,那么该卷就是被构建在A节点上,但可能因为各种原因导致节点故障,那么K8S会自动重新创建一个Pod,而创建的这个新的Pod会被随便调度到集群中任意一个节点,而这个节点是不会存储原先数据的,那么在这种只能存储于本地的数据卷只在本地使用时似乎没有问题,但一旦跨多节点那么就无法得到稳妥的解决了
k8s存储卷
所以我们期望在分布式集群管理的Kubernetes模型中,我们的存储卷不但应该要脱离容器的生命周期,也需要与节点的生命周期相分离,那么我们就需要引入能通过网络访问的网络存储空间了, 比如NFS,只要让集群中每一个节点都挂载NFS,而后集群无论将Pod调度至何意一个节点上,所构建的存储卷只要NFS不宕机,哪怕因为各种原因Pod被重新构建到其它节点,只要重构的节点能访问到NFS,那就能让该Pod访问到NFS存储数据,而对于NFS来说,它只是一个网络存储系统,我们只需要确保节点能够访问,那么基本上都可以为我们的k8s集群提供脱离节点生命周期的外围存储系统;
Pause容器
对于Kubernetes来讲,存储卷不属于容器,而属于Pod,Pod可能会运行多个容器,而其中一个容器会做为主容器,其它的皆为辅助容器,一个Pod中的多个容器是可以使用同一个存储卷的,因为存储卷属于Pod,Pod有一个底层基础架构容器,运行于kubeadm部署之上的k8s集群之上,这个基础架构容器镜像名字就叫pause,pause在每一个node节点上都存在,它主要为是集群中的每一个Pod提供一个底层的基础支撑设备,而我们运行的每一个Pod其内部也都会有一个pause容器存在,但对我们而言不用关注它;
容器借助于内核中的六个网络名称空间提供服务,pause提供了一个基础的容器,只要加入到这个Pod的容器,都将共享底层pause的网络名称空间、IPC以及UTS,同一个Pod内的所有容器,不但能通过lo通信,而且还共享同一个主机名,除此之外还有一个功能,当我们给Pod添加一个存储卷,只要这个卷是属于Pause的,后续每一个加入这个Pause的容器都可以复制Pause容器中的存储卷,类似于docker中的–from-volume功能,对于Pod来说只需要指定这个容器需要复制Pause容器的存储卷就行,但它与docker不同的是,k8s的Pod中容器要想使用存储卷,我们需要明确定义挂载这个存储卷到本地的某个目录下,它才能被使用;
如果我们期望在Pause使用存储卷,而这个存储有时候会属于NFS\ceph\samba\local,而每一种存储设备所提供的访问方式是各不一样的,因此这里就需要两个非常重要的步骤,
- 以NFS为例,假如该节点无法通过NFS连接至NFS存储,那么该节点的Pod也必然不能访问NFS之上的存储空间,所以想要每个节点能够适配到目标存储系统上就必须要有对应的驱动,每一个容器都是共享所在节点底层的内核的,而驱动是属于内核的功能,所以要确保节点的内核先能适配外部的存储系统;
- 节点内核可能会适配N种存储系统,假设k8s集群之外部署了samba\nfs\ceph,那么当启动Pod时,这个Pod就可能会使用NFS或其它存储设备,从这个角度上来讲,Pause在接入这个存储卷时,需要明确指明它自己要连接至节点级,已经关联到哪一种类型的存储设备上, Pause在关联存储卷时,Pod必须要以指定存储设备的访问接口匹配,说白了,真正去驱动并调用这个服务的客户端不是你的节点的内核,内核只是负责把他们二者之间桥接起来而已,而真正去驱动它的,必须要去成为这个存储服务的客户端才可以;
CSI
容器存储接口 (Container Storage Interface)
当Pause要连接到NFS,必须要在连接的时候指明,文件系统类型,NFS访问地址,和服务导出来的文件系统路径,这样一来就麻烦了,目前来讲,我们基于可用的存储类型,是很多的,那为了尽可能支持不同的存储设备,我们就不得不为Kubernetes中的Pod适配每一种存储系统内置驱动客户端;
这样一来,我们的整个系统就会变得庞大无比,因此为了避免这种情形,甚至于在必要的情况下,我们允许用户自定义存储,Kubernetes为了为了使得这种功能更加灵活,提供了一种特殊的类型叫做CSI,CSI是k8s的一个插件接口,叫做容器存储接口,利用CSI用户可以方便开发自己的存储驱动插件,可以自定义使用任何类型的存储插件,为了降低用户使用的k8s的复杂度,Kubernetes内置了很多标准类型,只有标准类型满足不了我们的需要时,我们才有必要去使用CSI扩展
关键点: 第一:节点需要适配的存储设备需要内核驱动支持, 第二:自身也需要扮演成为这个存储系统的客户端,为了能够驱动这个客户端,它必须内置一些插件链接不同的存储系统,而这个插件其实就是存储驱动;
存储卷状态
根据应用本身是否存在持久存储数据,以及某一次请求与此次的请求有否关连性, 根据这种关连性分为四种状态: 1、有状态要存储, 2、有状态无持久存储, 3、无状态有存储,4、无状态无存储,而大多数跟数据存储服务相关应用跟有状态应用几乎都是需要持久存储的。
如docker, 容器本身有命令周期,为了使得容器将来终结以后可以将它删除,或是编排至其它节点上运行,意味着数据不能放在容器本地或容器自已名称空间中,
挂载说明
如k8s, pod运行时应运行在某一个节点上,节点正常pod将会始终运行在这一个节点之上, 节点故障或删除才会重构pod,当数据存放在pod上时,一旦被重构 数据 将会随着pod结束而结束,为了解决数据丢失, 此时应当将数据放置于pod外的节点存储卷中使之数据持久性,这样使得pod结束数据也不会被丢失,而某个单点的存储卷只有一定程度的持久存储性,如节点down或损坏数据都将被丢失,为了实现更强大的存储 应当使用脱离节点的共享存储设备
k8s也提供了各种的存储卷功能,对pod来讲,同一个pod内的多个容器可共享访问同一组存储卷,而对k8s来说存储卷不属于容器而属于pod, 如果两个容器都挂载了,就说明两个容器都挂载了说明共享数据了,pod底层有一个基础容器不会被启动而是靠一个独特的镜像来创建的 (pause), 此时挂载应当为: 多个Pod --> 同一个挂载目录 --> 同一个节点目录 --> 底层存储卷
存储卷类型-挂载
Kubernetes支持的存储驱动 ]# kubectl explain pod.spec.volumes, 官方挂载说明
云存储:awsEastocBlockStore、azureDisk、azureFile、gcePersistentDisk、vshpere Volume
分布式存储:cephfs、glusterfs、rbd
网络存储:nfs、iscsi、fc
临时存储:emptyDir、gitRpo(deprecated)
本地存储:hostPath、local
emptyDir|hostPath, 只在节点本地使用,一旦pod被删除,该存储卷也会被一并删除,无法数据持久只能当一个临时目录使用,或当缓存使用,emptyDir背后关连的宿主机目录也可以是内存当成是硬盘挂载使用,
特殊存储:configMap、secret、downwardAPI
自定义存储:CSI
持久卷申请:persistentVolumeClaim
...
创建流程
pod --> pvc --> pv (存储空间) ,
- 创建: 当用户创建pvc需要用到pv时,它能够向存储类申请创建对应用户请求大小的pv, 由用户请求而动态生成,pv动态攻击
- 挂载: 需要指定volumemount挂载点
本地存储卷
spac:
conteiners:
# 2、配置挂载路径
volumeMounts
- name: # 这里为挂载卷的挂载名称
mountPath: # 要挂载的路径
# 如果有多个容器,哪个容器用就挂载哪个,如果有多个容器只挂载一个,其它容器无法访问该挂载点的目录
# 1、配置挂载类型
volumes: # 挂载卷
- name: 挂载名称
挂载类型: {
} # 空键值对关连数组,使用磁盘空间大小不控制
emptyDir
空目录,只要Pod一删除,数据就没了,它大多数场景都是用在缓存的情况下,就比如我们在这个Pod中的容器可能需要用到缓存的功能,如Nginx,nginx的temp目录可能会存储一些临时文件,这样的情况,我们就可以使用emptyDir来做,让缓存存储在指定的存储文件系统之上,更好用的是emptyDir还支持直接将数据存储在内存中,直接在节点的内存当中切割一段空间出来,把这个空间模拟成一个硬盘,然后把它当缓存用,速度更快;
场景二: 我们当前这个Pod没有文件存储的需求,但是Pod内部有两个容器,这两个容器之间需要共享一些数据,那这个时候我们就可以使用emptyDir这个空间,让第一个容器在里面读写,第二个容器也可用读写,这就实现的共享数据,或者说我们的php-fpm有一个sock文件,当构建lnmp的时候,我们可以直接将这个php-fpm的sock文件放在emptyDir里面。然后供nginx使用unix://emptydir_volumes/data/php-fpm.sock;
# k8s 当中 $() 表示变量引用
apiVersion: v1
kind: Pod
metadata:
name: empty-storage
labels:
app: empty-storage
release: qa
spec:
containers:
- name: empty-nginx
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
readinessProbe:
failureThreshold: 3
initialDelaySeconds: 3
httpGet:
port: http
path: /index.html
volumeMounts: # 挂载到容器目录中
- name: test # 如果有多个容器,访问的内容相同
mountPath: /usr/share/nginx/html # 挂载点为 ikubernetes的访问目录
- name: empty-busybox
image: busybox
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"] # 注意 $() 表示变量被引号 如果用command得使用 $$()
args:
- while true; do echo $(date) >> /data/index.html;sleep 2;done
volumeMounts: # 挂载到容器目录中
- name: test # 与上一个容器共享同一个挂载点
mountPath: /data # 当这里内容被修改,上一个容器的内容也会变更
volumes:
- name: test # 挂载名称是test,
emptyDir: {
} # 类型emptyDir 容器删除内容也会被清空
hostPath
将主机节点文件系统上的文件或目录挂载至pod中, 注意是 主机节点
type类型
取值 | 行为 |
---|---|
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查。 | |
DirectoryOrCreate |
如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 Kubelet 相同的组和所有权。 |
Directory |
在给定路径上必须存在的目录。 |
FileOrCreate |
如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 Kubelet 相同的组和所有权。 |
File |
在给定路径上必须存在的文件。 |
Socket |
在给定路径上必须存在的 UNIX 套接字。 |
CharDevice |
在给定路径上必须存在的字符设备。 |
BlockDevice |
在给定路径上必须存在的块设备。 |
apiVersion: v1
kind: Pod
metadata:
name: host-storage
spec:
containers:
- name: host-busybox
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: mydir
mountPath: /usr/share/nginx/html
volumes:
- name: mydir
hostPath:
path: /data
type: Directory
# 然后在node 主机节点上创建对应的 /data目录
# MountVolume.SetUp failed for volume "mydir" : hostPath type check failed: /data is not a directory, 这个错是指,在节点上没有这个目录 而不是k8s mstaer的目录
nfs
nfs
卷能将 NFS (网络文件系统) 挂载到您的 Pod 中。 不像emptyDir
那样会在删除 Pod 的同时也会被删除,nfs
卷的内容在删除 Pod 时会被保存,卷只是被卸载掉了。 这意味着nfs
卷可以被预先填充数据,并且这些数据可以在 Pod 之间”传递”。
[root@slave1 ~]# yum install -y nfs-utils
[root@slave1 ~]# cat /etc/exports
/data/nfs 192.168.2.0/24(rw)
[root@slave1 ~]# systemctl start nfs
[root@slave1 ~]# mkdir -p /data/nfs/{1,2,3,4,5}
[root@slave1 ~]# echo '11111111' >> /data/nfs/1/index.html
[root@slave1 ~]# echo '22222222' >> /data/nfs/2/index.html
# 在节点级手动测试是否能够成功挂载
[root@slave2 ~]# mount -t nfs 192.168.2.221:/data/nfs /data/nfs
# 查看nfs服务端挂载情况 exportfs -arv
# nfs客户端查看服务端口nfs挂载路径 showmount -e NFS服务端地址
[root@master ~] cat nfs-stornge.yaml
apiVersion: v1
kind: Pod
metadata:
name: nfs-storage
spec:
containers:
- name: nfs-busybox
image: ikubernetes/myapp:v1
imagePullPolicy: IfNotPresent
volumeMounts:
- name: nfssystem
mountPath: /usr/share/nginx/html
volumes:
- name: nfssystem
nfs:
path: /data/nfs
server: 192.168.2.221
]# kubectl get pods nfs-storage -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nfs-storage 1/1 Running 0 35s 10.244.1.103 slave1
[root@master ~]# curl 10.244.1.103/1/index.html
11111111
[root@master ~]# curl 10.244.1.103/2/index.html
22222222
总结
在k8s上使用存储卷,大致分为如下步骤,首先我们先在Pod上定义存储卷,然后在Pod的spec中定义volume将存储卷定义起来,而后我们需要在容器中去定义要使用的存储卷,但这种方式必须要在spec.volumes中定义存储卷,并且必须要明确且清晰的给出访问存储系统的客户端配置信息;
但这对大多数终端用户来说,几乎是不可能完成的任务,尤其是遇到非常复杂的存储系统时更是如此,那么这种方案其实违背于K8s提出的生产者消费者模式,而生产消费指的是存储能力的一方提供存储,消费者只需要向存储端说明它要使用多少存储就可以了,而无需更多的关注存储系统的细节;
很显然,spec.volumes手动定义的方式是无法满足这种需求的,为此k8s系统为了使得存储服务也能够转为生产消费者模型机制,它为Kubernetes的volumes和volumeMounts以及和后端的存储设备之间增加了一个中间层,这个中间层就称之为PV;