0%

k8s 中的存储系统 -- Volumes

关于 Kubernetes 系列其他文章的传送门

  1. 《 部署 Kubernetes 也能如此简单吗? 》

  2. 《 初识 k8s,新时代的宠儿 》

  3. 《 深度学习 Pod 》

  4. 《 学会这 5 种 Pod 控制器,搞定发布、更新、回滚,从此告别人肉运维!! 》

  5. 《 k8s 中的网络入口 – Service 资源管理 》

  6. 《 k8s 中的 7 层调度 – Ingress 》

  7. 《 k8s 中的存储系统 – Volumes 》     您当前所在位置

  8. 《 k8s 中强大的配置中心 – ConfigMap 》

  9. 《 k8s 中的部署神器 – Statefulset 控制器 》

  10. 《 k8s 中的安全守护神 – 你不知道不代表它不在 》

  11. 《 k8s 中的图形接口 – Dashboard 》

  12. 《 k8s 中的网络插件 – flannel 和 Calico 》

  13. 《 k8s 中的调度器 – 亲和性、污点和容忍性 》

  14. 《 k8s 中对资源的监控 – 资源指标、Prometheus、弹性伸缩器 HPA 》

  15. 《 k8s 中的云原生应用管理利器 – Helm 》

  16. 《 理解 k8s 高可用,让你的集群稳如泰山 》



logo

一、k8s 中的存储卷

存储卷是被挂载到 Pod 内的基础容器 pause 上,当这个 Pod 内的容器需要使用存储卷时,指明要挂载 pause 上的哪个存储卷即可。多个容器可同时挂载同一个 pause 上的存储卷,将共享使用。

不同的存储设备,访问的方式也不一样,一个节点中所有运行的容器是共享物理节点内核的,而连接存储的驱动是运行在内核中,所以首先要确保节点自己能够连接到存储设备,这个节点上的容器才能够连接到存储设备

当 pause 连接存储设备时,要明确指明让 pause 使用哪种类型的存储设备,并与对应类型的存储设备的访问接口相匹配才行,因为真正去驱动,并调用存储的客户端不是物理节点的内核,内核只是负责把二者之间桥接起来而已,而真正调存储服务的那一方,必须要能够成为存储设备的客户端才可以。

市面上的存储种类众多,k8s 自己已经内置了很多常见存储设备的客户端插件,如果内置的插件都不合适的话,可以使用自定义接口 CSI:Ccontainer Storage Interface

hostPath:定义本地存储目录的相关信息:

  • path:在宿主机的哪个路径下;
  • type:在 k8s 中,创建存储卷时,如果本地的目录不存在,要使用 type 来定义该如何操作,可选的方式有:
    • Directory:这个目录必须存在且是目录,不然就报错;
    • DirectoryOrCreate:如果目录不存在就自动新建;
    • File:这个文件必须存在且是目录,不然就保存;文件可以单独挂载;
    • FileOrCreate:如果文件不存在就自动创建;

Pod 的存储卷都是挂载到 pause 容器中的,其他容器如果需要使用,从 pause 容器中挂载到自己的容器内:

  • volumeMounts:定义挂载 pause 容器的卷存储时的参数;
    • mountPath:指定容器内的挂载点;
    • name:要挂载哪一个名称的存储卷;
    • readOnly:定义是否只读,默认 false 为读写;

使用节点级存储卷的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: default
labels:
app: myapp
spec:
containers:
- name: myapp
image: ikubernetes/myapp:v1
volumeMounts: # 定义挂载 pause 容器的卷存储时的参数
- name: web_stor
mountPath: /usr/share/nginx/html/
volumes:
- name: web_stor
hostPath:
path: /volumes/myapp # 指定宿主机上的路径
type: DirectoryOrCreate # 如果目录不存在则自动创建

Pod 被调度到哪个主机,存储卷就被创建在哪个主机上;在目录中创建一个测试页面:

1
~]# echo "test.page" > /volumes/myapp/index.html

这种方式只能提供节点性的持久性,以后这个 Pod 必须运行在这个节点上,才能使用这个文件,如果调度到其他节点,就无法访问。可以在定义 Pod 时使用 nodeName 明确告诉调度器只能调度到指定的节点上,不过不建议使用。

Local 类型

官方介绍:https://kubernetes.io/zh/docs/concepts/storage/volumes/#local

local 卷,可以挂载目录,还可以挂载某个本地存储设备,例如磁盘、分区。

定义方式:

1
2
3
4
local:
...
path: /mnt/disks/ssd1 # 可以是个目录、分区、磁盘
...

emptyDir 类型

这种类型的存储属于临时性的,Pod 一删除就没有了。主要用于:

  • Pod 中需要缓存时,可以通过此种类型把缓存目录映射到节点上的 SSD 盘;甚至可以在节点的内存中,划分一部分映射到 Pod 中作为缓存使用;
  • 当 Pod 没有持久存储的需求,但是一个 Pod 中的两个容器需要共享部分数据,就借用此类型的存储,因为一个 Pod 内不同的容器并不共享 Mount 名称空间。
参数
  • medium
    • none:空值,不定义则使用本地磁盘;
    • Memory:使用内存作为存储:
      • sizeLimit:当使用 Memory 存储时,定义最大可使用容量,不定义则无限;
配置示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-> # vim vol-emptydir.yaml 

apiVersion: v1
kind: Pod
metadata:
name: vol-emptydir-pod
spec:
volumes:
- name: html
emptyDir: {} # 参数为空则为使用本地磁盘
containers:
- name: nginx
image: nginx:1.12-alpine
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html # 第一个容器的挂载点
- name: pagegen
image: alpine
volumeMounts:
- name: html
mountPath: /html # 第二个容器的挂载点,这样两个容器使用同一个存储,看到一样的数据
command: ["/bin/sh", "-c"]
args:
- while true; do
echo $(hostname) $(date) >> /html/index.html; # 第二个容器不停的编辑 index.html 文件,第一个容器应该也能看到
sleep 10;
done

二、使用外部存储配置示例

常见使用 ceph,这里使用 nfs 作为演示。

部署 nfs

注意:在 master 节点配置 nfs

先在 master 节点部署 nfs

1
~]# yum install nfs-utils -y

创建子目录来模拟存储设备:

1
~]# mkdir /vols/v{1,2,3,4,5} -p

定义可以把目录导出给哪个网络的设备使用:

1
2
3
~]# vim /etc/exports
/vols/v1 192.168.50.0/16(rw,no_root_squash)
~]# systemctl start nfs

确认 2049 端口已启用并可以正常访问。

注意:在客户端配置

在每个 node 节点都安装 nfs,并挂载 master 的 nfs 目录,测试是否可以正常使用,之后要取消挂载,只是测试一下:

1
2
3
~]# yum install nfs-utils -y
~]# mount -t nfs 192.168.50.14:/vols/v1 /mnt
~]# umount /mnt

Pod 使用 nfs 存储的配置示例

参数:

  • path:nfs 服务器上导出的目录是什么;
  • server:nfs 服务器的地址和端口;
  • readOnly:挂载的 nfs 是否只读。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-> # vim vol-nfs.yaml

apiVersion: v1
kind: Pod
metadata:
name: vol-nfs-pod
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
name: redisport
volumeMounts:
- mountPath: /data # 定义挂载点,注意:Redis 的镜像默认使用此路径为数据持久化的默认路径,建议不要更改
name: redisdata
volumes:
- name: redisdata
nfs:
server: 192.168.50.14
path: /vols/v1

这样创建的 Pod,无论被调度到哪个节点,只要物理节点能正常连接至 nfs 服务器,那么数据就可以跨节点持久保存。如果使用 Ceph 时,只是配置方式不一致。

三、存储卷中的一些概念

PV

为了让任何人包括没有存储知识的人方便使用,实现消费者和提供者模型,k8s 在 spec.volumes后端存储 之间,加一个中间层,把存储系统提供的存储能力给抽象成 k8s 中标准类型的资源,称之为 PV

一个 PV 对应存储系统内可提供的最小的逻辑资源,如果是 nfs,应该为一个导出的目录。

pvc 的绑定(Bind)

PV 在 k8s 中是属于集群级别的资源,不属于任何一个名称空间,但是 Pod 是名称空间级别的资源,所以要在某个名称空间内的 Pod 使用 PV,就要把 PV 注册到对应的名称空间中去,而且是 1 对 1 的,比如在 A 名称空间内使用了,就不能在 B 名称空间内使用。

注册方法:在 Pod 所在的名称空间内创建一个 pvc(persistenVolumeClaim) 来请求占住 pv,当 pv 被这个 pvc 关联后,就不能被其他 pvc 关联了,这种方式被称为 Bind。之后 Pod 就可以使用 pvc,再通过 pvc 去使用 pv。

至于同一个名称空间内的不同 Pod 是否可以 Bind 到同一个 pvc,取决于后端存储是否支持多个客户端多路读写访问。

  • ReadWriteOnce,简称 RWO:单路读写;
  • ReadOnlyMany,简称 ROX:多路只读;
  • ReadWriteMany,简称 RWX:多路读写;

当配置 Pod 时,存储卷类型为 persistenVolumeClaim:

  • persistenVolumeClaim:使用 PVC 类型的存储卷:
    • claimName:指明要使用的 PVC 的名字;
    • readOnly:是否为只读的。

PV 的回收策略(Reclaim)

当删除 pvc 后,应该怎么处理 pv,使用 pv 的回收策略(reclaim)来定义:

  • 删除(Delete):删除 pvc 时,对应的 pv 也一并删除;
  • 空间回收(Recycle):删除 pvc 时,对应的 pv 上的数据全部删除;已废弃;
  • 保持不动(Return):删除 pvc 后,对应的 pv 保持原样。

PV 的供给(Provisioning)

PV 如何供给,有两种:

  • 静态:由管理员手动创建;
  • 动态:有两种方式:
    • 当用户需要在某一个名称空间当中创建 pvc 的时候,要定义用户的需求,比如要定义使用多大空间。这时根据用户的请求,按需找出存储设备中最合适的存储逻辑单元,并动态的创建成 pv,之后和 pvc 绑定起来。这种方式要求存储设备上要存在多个存储单元,在必要时要能够满足 pvc 的请求。
    • 存储系统上没有事先创建存储逻辑单元,只是有存储空间而已。当创建 pvc 时,根据用户的请求,动态的调用存储系统的管理接口(API),临时创建一个刚好满足 pvc 需求的存储单元,再把这个存储单元创建为这个 k8s 集群上的 pv,然后再与 pvc 绑定。

而 pvc 之所以能够在创建时自动触发动态创建供给,是依赖另外一个资源,叫 存储类(Storage Class)

使用前要先在 k8s 之上,把外部的存储系统,先定义为 存储类 类型的资源,之后当创建一个 pvc 时,pvc 不是向存储系统请求资源,而是向 SC 请求,而 SC 上已经配置了如何去连接外部存储设备的 API,之后通过这个 API 创建逻辑单元,并在 SC 的内部创建 pv,所以这样创建的 pv 是属于 SC 的。而如果 pvc 请求一个 pv 是通过 SC 来实现的,那么这个 pvc 也会属于 SC。

所以 k8s 中所有的 pv 和 pvc 可以分为两类,属于某个 SC 的和不属于某个 SC 的。

  • pvc 只能向其所在的 SC 内的 pv 请求绑定,不能跨 SC;
  • 如果一个 pvc 不属于任何 SC,那么它可以绑定的 pv 也一定不能属于任何 SC。

生命周期(LifeCycle)

  • Provisioning:供给;
  • Binding:绑定;
  • Using:使用;
  • Reclaiming:回收。

四、使 pv 连接 nfs 配置示例

使用外部存储

在 master 节点配置 nfs

先在 master 节点部署 nfs

1
~]# yum install nfs-utils -y

创建子目录来模拟存储设备:

1
~]# mkdir /vols/v{1,2,3,4,5,6} -p

定义可以把目录导出给哪个网络的设备使用:

1
2
3
4
5
6
7
8
9
~]# vim /etc/exports
/vols/v1 192.168.50.0/16(rw,no_root_squash)
/vols/v2 192.168.50.0/16(rw,no_root_squash)
/vols/v3 192.168.50.0/16(rw,no_root_squash)
/vols/v4 192.168.50.0/16(rw,no_root_squash)
/vols/v5 192.168.50.0/16(rw,no_root_squash)
/vols/v6 192.168.50.0/16(rw,no_root_squash)

~]# systemctl start nfs

确认 2049 端口已启用并可以正常访问。

配置静态供给 PV

定义访问模型 accessModes 的参数:

  • ReadWriteOnce,简称 RWO:单路读写;
  • ReadOnlyMany,简称 ROX:多路只读;
  • ReadWriteMany,简称 RWX:多路读写;

注意:不是所有的存储设备都支持创建 PV,比如 Local 类型就不支持。

新建一个 pv 的配置文件,把 nfs 中的 v2 目录创建为 pv;注意:pv 不属于任何名称空间,配置文件中不要包含 namespace 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
~]# vim pv.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-v2
labels:
storsys: nfs
spec:
accessModes: ["ReadWriteOnce","ReadWriteMany","ReadOnlyMany"] # 定义支持的访问模型,注意:后端的存储要支持才行,nfs 是支持的,所以这里全部开启
capacity: # 最大允许存储容量
storage: 5Gi
volumeMode: Filesystem # 定义使用什么类型的接口,nfs 提供的是文件系统接口,rbd 提供的就是块存储接口
persistentVolumeReclaimPolicy: Retain # 定义删除 pvc 后,pv 的回收策略
nfs:
server: 192.168.50.14
path: /vols/v2

创建并查看:

1
2
3
4
5
6
-> # kubectl apply -f pv.yaml
persistentvolume/pv-nfs-v2 created

-> # kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs-v2 5Gi RWO,ROX,RWX Retain Available 21s

可以看到 pv 的状态为可用,接下来创建 pvc。

定义 PVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
~]# vim pvc.yaml

apiVersion: v1
kind: persistentVolumeClaim
metadata:
name: redis-data
namespace: vol # pvc 是名称空间级资源
spec:
accessModes: # 访问模型需要是 pv 支持的
- ReadWriteOnce
volumeMode: Filesystem
resources:
requests:
storage: 3Gi # 定义需要多大空间,pv 需要满足此空间才行,如果有多个 pv 容量不一样,会匹配出最接近的

创建 ns:

1
2
~]# kubectl create ns vol
namespace/vol created

创建 pvc:

1
2
~]# kubectl apply -f pvc.yaml
persistentvolumeclaim/redis-data created

查看创建的 pvc,可以看到匹配到之前创建的 pv 上了,:

1
2
3
~]#  kubectl get pvc -n vol
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
redis-data Bound pv-nfs-v2 5Gi RWO,ROX,RWX 93s

再回头查看 pv 的状态变为绑定了:

1
2
3
~]#  kubectl get pv        
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pv-nfs-v2 5Gi RWO,ROX,RWX Retain Bound vol/redis-data 15m

创建 Pod 使用此 pvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-> # vim vol-redis-pvc.yaml
apiVersion: v1
kind: Pod
metadata:
name: redis-pvc
namespace: vol # 创建在和 pvc 同一个名称空间下
labels:
app: redis-pvc
spec:
containers:
- name: redis
image: redis:alpine
ports:
- name: redisport
containerPort: 6379
volumeMounts:
- name: redis-data
mountPath: /data # 定义挂载点,注意:Redis 的镜像默认使用此路径为数据持久化的默认路径,建议不要更改
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-data # 定义使用同一个名称空间下的哪个 pvc

创建 Pod:

1
kubectl apply -f vol-redis-pvc.yaml 

测试是否已经使用 pvc:

1
2
3
4
5
6
7
8
9
10
11
~]# kubectl exec -it -n vol redis-pvc -- /bin/sh      # 连接到 redis-pvc 的交互接口
/data # redis-cli # 进入 redis
127.0.0.1:6379> SET test-key "this is test key" # 创建一个测试的 Key
OK
127.0.0.1:6379> BGSAVE # 保存到快照中
Background saving started
/data # ls # 可以看到 /data 下已经有快照文件了
dump.rdb

~]# ls /vols/v2 # 查看 nfs 服务器的 v2 目录中,可以看到已经有数据了
dump.rdb

这样就可以使用 pvc 类型的存储卷,实现跨节点的数据持久了。

五、pvc 保护机制

在 k8s 1.11 版本之后,增加了 pvc 保护机制:如果删除正在被 Pod 使用的 pvc,删除操作会被延迟并阻塞终端,直到使用此 pvc 的 Pod 被删除时才真的删除。删除 pv 同理,会延迟删除直到关联至此 pv 的 pvc 被删除时才真的删除。