存储卷

什么是存储卷

在 k8s 中,存储卷(Volume)是一种抽象的概念,用于提供 pod 中容器的持久化存储。存储卷允许将数据存储在 pod 的生命周期之外,以便在容器重启、迁移或重新调度时保留数据。

存储卷可以连接到 pod 中的一个或多个容器,并提供持久化的存储空间。这些存储卷可以是来自 k8s 集群的本地存储、网络存储、云存储提供商或外部存储系统等。

存储卷可以用于各种用途,例如:

  • 数据持久化:将数据存储在存储卷中,以便在容器重启后仍然可用。
  • 数据共享:将存储卷连接到多个容器,使它们可以共享相同的数据。
  • 数据备份和恢复:使用存储卷来备份和还原应用程序的数据。
  • 数据迁移和复制:将存储卷从一个 pod 迁移到另一个 pod,或者将存储卷复制到其他地方。

由于 pod 本身是具有生命周期的,所以 pod 内部运行的容器及相关的数据,在 pod 中是无法持久化存储的。我们知道,docker 支持配置容器使用存储卷,已实现数据在容器之外的存储空间中持久化存储。相应的,k8s 也支持类似的存储卷功能,此处存储卷与 pod 资源绑定。

简单来说,存储卷是定义在 pod 资源之上、可被其他内容所有容器挂载的共享目录,它关联至外部的存储设备之上的存储空间,从而独立于容器自身的文件系统,而数据是否具有持久化存储能力取决于存储卷自身是否支持持久存储机制,与 pod 无关。

k8s 支持的存储类型

k8s 支持非常丰富的存储卷类型,总体上来看,大致可以分为如下三种类型:

本地存储,例如 emptyDir、HostPath;

网络存储,nfs、cinder、cephfs、AzureFile;

特殊存储资源,例如 secret、ConfigMap

对于本地存储,emptyDir 的生命周期与 pod 资源相同,所以 pod 一旦删除,存储的数据同时被删除。HostPath(node1 node2)的生命周期与节点一致,当 pod 重新被调度到其他节点时,虽然原节点上的数据没有被删除,但是 pod 不再使用此前的数据。

网络存储系统是独立于 k8s 集群之外的存储资源,数据存储的持久性与 k8s 集群解耦合。k8s 在集群中设计了一种集群级别的资源 PersistentVolume:服务器资源(管理主动分配一些存储空间)(简称 PV)来关联网络存储,用户通过 PersistentVolumeClaim:用户就要申请使用资源(服务器就会自动匹配)(简称 PVC)来申请使用存储资源。

secret 和 ConfigMap 算是 k8s 集群中两种特殊类型的存储资源。secret 用于向 pod 传递敏感信息,例如密码、私钥、证书等。这些信息直接定义在镜像中容易导致泄露,用户可以讲这些信息集中存储在 secret 中,由 pod 进行挂载,从而实现敏感数据与系统解耦。

ConfigMap 资源用于向 pod 注入非敏感数据,用户将一些非敏感的配置数据存储在 ConfigMap 对象中,然后在 pod 中使用 ConfigMap 卷引用它即可,从而帮助实现容器配置文件集中化定义和管理。

emptyDir

emptyDir:初始内容为空的本地临时目录

它是一个临时卷(Ephemeral Volume)

与 pod 一起创建和删除,生命周期与 pod 相同

emptyDir 会创建一个初始状态为空的目录

通常使用本地临时存储来设置缓存、保存日志等

例如,将 redis 的存储目录设置为 emptyDir,编辑文件redis-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: redis-pod
spec:
  containers:
    - name: redis01
      image: redis
      volumeMounts:
        - name: redis-storage
          mountPath: /data/redis
  volumes:
    - name: redis-storage
      emptyDir: {}
[root@k8s-master k8s]# vim redis-pod.yaml
[root@k8s-master k8s]# kubectl apply -f redis-pod.yaml 
pod/redis-pod created
[root@k8s-master k8s]# kubectl get pod
NAME        READY   STATUS    RESTARTS   AGE
redis-pod   1/1     Running   0          2m27s

容器启动之后我们进入容器内部,添加数据

[root@k8s-master k8s]# kubectl exec -it redis-pod /bin/bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@redis-pod:/data# redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set name yigongsui
OK

这样我们添加了一条数据,接下来我们退出并删除这个容器

127.0.0.1:6379> exit
root@redis-pod:/data# exit
exit
[root@k8s-master k8s]# kubectl delete -f redis-pod.yaml 
pod "redis-pod" deleted

删除容器后,我们再重新启动这个容器,看看数据还在不在

[root@k8s-master k8s]# kubectl apply -f redis-pod.yaml 
pod/redis-pod created
[root@k8s-master k8s]# kubectl get pod
NAME        READY   STATUS    RESTARTS   AGE
redis-pod   1/1     Running   0          16s
[root@k8s-master k8s]# kubectl exec -it redis-pod /bin/bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@redis-pod:/data# redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> get name
(nil)

发现数据不存在了,这就说明 emptyDir 的生命周期很短,与 pod 相同,只做临时存储使用

网络存储 nfs

nfs:全称是 Network File System,它最大的功能就是可以通过网络,让不同的机器、不同的操作系统可以共享彼此的文件。

k8s入门到实战(五)—— k8s存储卷详细介绍-LMLPHP

三台服务器都要安装 nfs

yum install -y nfs-utils

接下来,在 master 主节点配置:

# 主节点配置
echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports
# 创建/nfs/data文件夹(主节点)
mkdir -p /nfs/data  
# 启动rpc远程绑定(主节点)
systemctl enable rpcbind --now 
# (开机)启动nfs服务(主节点)
systemctl enable nfs-server --now 

# 配置生效(主节点)
exportfs -r 
# 查看目录
exportfs
[root@k8s-master k8s]# echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports
[root@k8s-master k8s]# mkdir -p /nfs/data
[root@k8s-master k8s]# systemctl enable rpcbind --now
[root@k8s-master k8s]# systemctl enable nfs-server --now
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.
[root@k8s-master k8s]# exportfs -r
[root@k8s-master k8s]# exportfs
/nfs/data     	<world>

接下来在 node 节点下执行,

# 查看主节点机器有哪些目录可以同步挂载(node节点)
# showmount -e 主节点的内网ip,我的内网ip是192.168.0.1
showmount -e 192.168.0.1
[root@k8s-node1 home]# showmount -e 192.168.0.1
Export list for 192.168.0.1:
/nfs/data *

接下来在 node 节点执行以下命令挂载 nfs 服务器上的共享目录到本机路径

# mkdir -p 本机目录 (名字可以任取)
# mount -t nfs 192.168.0.1:/nfs/data 本机目录
# node1 执行
mkdir -p /nfs/node1
mount -t nfs 192.168.0.1:/nfs/data /nfs/node1
# node2 执行
mkdir -p /nfs/node2
mount -t nfs 192.168.0.1:/nfs/data /nfs/node2

测试看看三个节点是否共通

# master节点新增文件
[root@k8s-master k8s]# cd /nfs/data/
[root@k8s-master data]# echo "hello master" > master.txt
# node1节点新增文件
[root@k8s-node1 home]# cd /nfs/node1/
[root@k8s-node1 node1]# echo "hello node1" > node1.txt

# 查看三个节点内数据是否共通
[root@k8s-master data]# ls
master.txt  node1.txt
[root@k8s-node1 node1]# ls
master.txt  node1.txt
[root@k8s-node2 home]# ls /nfs/node2/
master.txt  node1.txt

可以看到三个节点已经共通了

文件都共享,在 nfs 文件系统中,volumes 肯定也可以连通

测试:编辑文件nginx-nfs.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-nfs
  name: nginx-nfs
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-nfs
  template:
     metadata:
       labels:
         app: nginx-nfs
     spec:
       containers:
       - image: nginx01
         name: nginx
         volumeMounts:
           - name: nginx-storage
             mountPath: /usr/share/nginx/html
       volumes:
         - name: nginx-storage
           nfs:
             server: 192.168.0.1
             path: /nfs/data/nginx-nfs

创建一个 deployment 部署,自行测试即可,在主节点的/nfs/data/nginx-nfs目录,新建 index.html,编辑内容,在容器里进行访问测试。

主节点的/nfs/data/nginx-nfs目录也和从节点的目录共通(node1 的目录就是/nfs/node1/nginx-nfs),也都可以进行测试

结论:无论容器怎么部署怎么删除,数据都可以持久化保存了

PV & PVC

持久卷(Persistent Volume):删除 pod 后,卷不会被删除

PV:持久卷(Persistent Volume),将应用需要持久化的数据保存到指定位置

PVC:持久卷申明(Persistent Volume Claim),申明需要使用的持久卷规格

k8s 支持的存储系统非常多,要求大家全都掌握,是不现实的。为了屏蔽底层存储实现细节,方便使用,引入了 PV 和 PVC 两种资源对象。

封装机制!数据中台

k8s入门到实战(五)—— k8s存储卷详细介绍-LMLPHP

PV

Persistent Volume 是持久卷的意思,是对底层共享存储的一种抽象封装。一般情况 PV 由管理员创建和配置,它与底层具体的存储技术有关,通过插件完成与存储的对接。

PV 是存储资源的抽象,资源清单如下:

apiVersion: v1  
kind: PersistentVolume
metadata:
  name: pv2
spec:
  nfs: # 存储类型,与底层真正存储对应
  capacity:  # 存储能力,目前只支持存储空间的设置
    storage: 2Gi
  accessModes:  # 访问模式
  storageClassName: # 存储类别
  persistentVolumeReclaimPolicy: # 回收策略

accessModes 用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:

  • ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
  • ReadOnlyMany(ROX):只读权限,可以被多个节点挂载
  • ReadWriteMany(RWX):读写权限,可以被多个节点挂载
  • 需要注意的是,底层不同的存储类型可能支持的访问模式不同

回收策略(persistentVolumeReclaimPolicy)

persistentVolumeReclaimPolicy 当 PV 不再被使用了之后,对其的处理方式。目前支持三种策略:

  • Retain (保留) 保留数据,需要管理员手工清理数据
  • Recycle(回收) 清除 PV 中的数据,效果相当于执行 rm -rf /thevolume/*
  • Delete (删除) 与 PV 相连的后端存储完成 volume 的删除操作,当然这常见于云服务商的存储服务

一个 PV 的生命周期中,可能会处于4种不同的阶段:

  • Available(可用):表示可用状态,还未被任何 PVC 绑定
  • Bound(已绑定):表示 PV 已经被 PVC 绑定
  • Released(已释放):表示 PVC 被删除,但是资源还未被集群重新声明
  • Failed(失败):表示该 PV 的自动回收失败

PVC

Persistent Volume Claim 持久卷声明的意思,是用户对存储需求的一种声明。也就是向 k8s 系统发出的一种资源需求申请。

PVC 作为资源的申请,用来声明对存储空间、访问模式、存储类别需求信息。下面是资源清单文件:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc
  namespace: dev
spec:
  accessModes: # 访问模式
  selector: # 采用标签对PV选择
  storageClassName: # 存储类别
  resources: # 请求空间
    requests:
      storage: 5Gi

示例

创建一个 pv,创建一个 pvc,创建一个 pod 绑定 pvc 就可以了!

pv 做就是连接存储系统,规定一个大小的空间,权限配置

pvc 做的就是写一个声明,根据自己的使用者的要求(存储系统、大小、权限),来匹配 pv

pod 使用 pvc volumes 挂载的类型!

  1. 准备工作:在 nfs 主节点(master 节点)创建 PV 池
mkdir -p /nfs/data/01
mkdir -p /nfs/data/02
mkdir -p /nfs/data/03
  1. 创建 pv,编辑文件my-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv01-10m
spec:
  capacity:
    storage: 10M
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  nfs:
    path: /nfs/data/01
    server: 192.168.0.1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv02-1gi
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  nfs:
    path: /nfs/data/02
    server: 192.168.0.1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv03-3gi
spec:
  capacity:
    storage: 3Gi
  accessModes:
    - ReadWriteMany
  storageClassName: nfs
  nfs:
    path: /nfs/data/03
    server: 192.168.0.1
  1. 执行 yaml 文件
[root@k8s-master k8s]# kubectl apply -f my-pv.yaml 
persistentvolume/pv01-10m created
persistentvolume/pv02-1gi created
persistentvolume/pv03-3gi created
  1. 查看所有 pv
kubectl get pv
[root@k8s-master k8s]# kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv01-10m   10M        RWX            Retain           Available           nfs                     50s
pv02-1gi   1Gi        RWX            Retain           Available           nfs                     50s
pv03-3gi   3Gi        RWX            Retain           Available           nfs                     50s
  1. 创建 pvc,编辑my-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 500Mi
  storageClassName: nfs 
  1. 执行 yaml 文件
[root@k8s-master k8s]# vim my-pvc.yaml
[root@k8s-master k8s]# kubectl apply -f my-pvc.yaml 
persistentvolumeclaim/nginx-pvc created
  1. 再次查看所有 pv
[root@k8s-master k8s]# kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS   REASON   AGE
pv01-10m   10M        RWX            Retain           Available                       nfs                     7m54s
pv02-1gi   1Gi        RWX            Retain           Bound       default/nginx-pvc   nfs                     7m54s
pv03-3gi   3Gi        RWX            Retain           Available                       nfs                     7m54s

可以看到 pv02-1gi 的状态变成了 Bound,也就是说我们创建的这个 pvc 绑定到了 pv02-1gi

这是因为在 pvc 会根据我们 yaml 文件设置的存储类型以及需要的存储容量来选择绑定到最合适的 pv,我们在 pvc 中设置了存储类型为 nfs,所需的容量为 500M,所以绑定到了 pv02-1gi

  1. 删除这个 pvc,查看 pv 状态
[root@k8s-master k8s]# kubectl delete -f my-pvc.yaml 
persistentvolumeclaim "nginx-pvc" deleted
[root@k8s-master k8s]# kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS   REASON   AGE
pv01-10m   10M        RWX            Retain           Available                       nfs                     12m
pv02-1gi   1Gi        RWX            Retain           Released    default/nginx-pvc   nfs                     12m
pv03-3gi   3Gi        RWX            Retain           Available                       nfs                     12m

可以看到,状态变为了 Released,也就是释放状态

  1. 根据 my-pvc.yaml 文件再创建一个 pvc,查看 pv 状态
[root@k8s-master k8s]# kubectl apply -f my-pvc.yaml 
persistentvolumeclaim/nginx-pvc created
[root@k8s-master k8s]# kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS   REASON   AGE
pv01-10m   10M        RWX            Retain           Available                       nfs                     14m
pv02-1gi   1Gi        RWX            Retain           Released    default/nginx-pvc   nfs                     14m
pv03-3gi   3Gi        RWX            Retain           Bound       default/nginx-pvc   nfs                     14m
# 查看 pvc 绑定到了哪个 pv上
[root@k8s-master k8s]# kubectl get pvc
NAME        STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nginx-pvc   Bound    pv03-3gi   3Gi        RWX            nfs            101s

可以看到 pvc 绑定到了 pv03-3gi 上,说明正处于 Released 状态的 pv 无法绑定任何 pvc

已经了解 pv 和 pvc 的绑定关系,接下来我们创建一个 pod 去绑定 pvc

  1. 清除所有的 deployment 和 pod
[root@k8s-master k8s]# kubectl get deploy
No resources found in default namespace.
[root@k8s-master k8s]# kubectl get pod
NAME        READY   STATUS    RESTARTS   AGE
redis-pod   1/1     Running   0          16h
[root@k8s-master k8s]# kubectl delete pod redis-pod
pod "redis-pod" deleted
  1. 编辑文件my-pvc-pod.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: nginx-pod-pvc
  name: nginx-pod-pvc
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-pod-pvc
  template:
    metadata:
      labels:
        app: nginx-pod-pvc
    spec:
      containers:
      - image: nginx
        name: nginx01
        volumeMounts:
        - name: nginx-volume
          mountPath: /usr/share/nginx/html
      volumes:
        - name: nginx-volume
          persistentVolumeClaim:
            claimName: nginx-pvc
  1. 执行 yaml 文件,查看 deployment 和 pod
[root@k8s-master k8s]# kubectl apply -f my-pvc-pod.yaml 
deployment.apps/nginx-pod-pvc created
[root@k8s-master k8s]# kubectl get deploy
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
nginx-pod-pvc   2/2     2            2           19s
[root@k8s-master k8s]# kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
nginx-pod-pvc-967fcb547-rj5ll   1/1     Running   0          30s
nginx-pod-pvc-967fcb547-wbbh7   1/1     Running   0          30s
  1. 查看 pod 的详细信息,里面有关于 pvc 信息
[root@k8s-master k8s]# kubectl describe pod nginx-pod-pvc-967fcb547-rj5ll
......
Volumes:
  nginx-volume:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  nginx-pvc
    ReadOnly:   false
  kube-api-access-5rlkl:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              <none>
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
......
  1. 因为 pvc 绑定在 pv03-3gi 这个 pv 上,这个 pv 的挂载目录是/nfs/data/03,所以我们去这个目录下编辑文件
[root@k8s-master k8s]# cd /nfs/data/03
[root@k8s-master 03]# echo "hello yigongsui" > index.html
  1. 进入 pod 容器内部,访问首页,查看结果
[root@k8s-master 03]# kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
nginx-pod-pvc-967fcb547-rj5ll   1/1     Running   0          11m
nginx-pod-pvc-967fcb547-wbbh7   1/1     Running   0          11m
[root@k8s-master 03]# kubectl exec -it nginx-pod-pvc-967fcb547-rj5ll /bin/bash
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
root@nginx-pod-pvc-967fcb547-rj5ll:/# curl localhost
hello yigongsui

可以看到里面的首页也变化了

这样我们的两个容器内目录就永久存储到我们的本地目录了,这样无论 pod 怎么重新部署删除,数据都不会丢失了

生命周期

PVC 和 PV 是一一对应的,PV 和 PVC 之间的相互作用遵循以下生命周期:

  • 资源供应:管理员手动创建底层存储和 PV。
  • 资源绑定:用户创建 PVC,k8s 负责根据 PVC 的声明去寻找 PV,并绑定。

在用户定义好 PVC 之后,系统将根据 PVC 对存储资源的请求在已存在的 PV 中选择一个满足条件的。

  • 一旦找到,就将该 PV 与用户定义的 PVC 进行绑定,用户的应用就可以使用这个 PVC 了,就可以在 pod 里面去使用
  • 如果找不到,PVC 则会无限期处于 Pending 状态,直到等到系统管理员创建了一个符合其要求的 PV,PV 一旦绑定到某个 PVC 上,就会被这个 PVC 独占,不能再与其他 PVC 进行绑定了,一一绑定。
  • 资源使用:用户可在 pod 中像 volume 一样使用 pvc。

pod 使用 volume 的定义,将 PVC 挂载到容器内的某个路径进行使用。

  • 资源释放:用户删除 pvc 来释放 pv。

当存储资源使用完毕后,用户可以删除 PVC,与该 PVC 绑定的 PV 将会被标记为“已释放”,但还不能立刻与其他 PVC 进行绑定。通过之前 PVC 写入的数据可能还被留在存储设备上,只有在清除之后该 PV 才能再次使用。

这里我们查看 pv02-1gi 的绑定关系:

kubectl get pv pv02-1gi -o yaml

k8s入门到实战(五)—— k8s存储卷详细介绍-LMLPHP

在这里唯一绑定了 pvc,需要清除才可以绑定其它 pvc

我们可以使用以下命令去动态修改 k8s 配置

kubectl edit pv pv02-1gi

与 linux 的 vim 编辑器一致,点击i进入输入模式,删除这两行即可

查看 pv 状态

[root@k8s-master 03]# kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM               STORAGECLASS   REASON   AGE
pv01-10m   10M        RWX            Retain           Available                       nfs                     135m
pv02-1gi   1Gi        RWX            Retain           Available   default/nginx-pvc   nfs                     135m
pv03-3gi   3Gi        RWX            Retain           Bound       default/nginx-pvc   nfs                     135m

可以看到 pv02-1gi 的状态已经变为 Available,就可以重新绑定 pvc 了

资源回收:k8s 根据 pv 设置的回收策略进行资源的回收。

对于 PV,管理员可以设定回收策略,用于设置与之绑定的 PVC 释放资源之后如何处理遗留数据的问题。只有 PV 的存储空间完成回收,才能供新的PVC绑定和使用。

问题:pv、pvc都是我们手动创建和绑定的,十分麻烦

有没有这样一种技术可以自动帮我们绑定,我们在 pod 创建的时候,自动帮我们创建 pv 和 pvc

答:有,StorageClass

StorageClass(存储类)

官方地址:https://kubernetes.io/zh-cn/docs/concepts/storage/storage-classes/#local

在 java 中,我们知道 class 是类的概念,相当于一个模板,通过类来创建对象

那么 StorageClass 其实就相当于是 pv 的模板,通过这个模版可以自动帮我们创建 pv 并通过 pvc 自动挂载上

接下来我们去理解 StorageClass

什么是 StorageClass

在 k8s 中,StorageClass 是用于定义和管理动态供应的持久化存储的对象。它是 PersistentVolume(PV)的动态供应机制的一部分。

StorageClass 定义了一组存储配置,包括存储提供者、存储类型、I/O 特性、复制策略等。它允许管理员为不同的存储需求创建多个 StorageClass,并为每个 StorageClass 指定不同的参数。

当创建 PersistentVolumeClaim(PVC)时,可以指定所需的 StorageClass。k8s 会根据 StorageClass 的定义和可用的存储资源,动态创建与 PVC 匹配的 PersistentVolume,并将其绑定到 PVC 上。

StorageClass 的优点是可以根据应用程序的需求自动创建和配置持久化存储,无需手动预先创建 PersistentVolume。这样可以简化存储管理的工作,提高存储资源的利用率。

另外,StorageClass 还支持动态卷的回收和回收策略的定义。当 PVC 被删除时,如果定义了回收策略,k8s 会自动回收对应的 PersistentVolume,释放存储资源。

总之,StorageClass 是 k8s 中用于动态供应和管理持久化存储的重要机制,可以根据需求自动创建、配置和回收存储资源,提高存储的灵活性和利用率。

静态供应与动态供应

我们这里的 PV 是我们提前开辟好的空间申明,是静态供应。

还有一种动态供应,根据 PVC 申请的空间,来实现 PV 的创建,从而进行绑定。

在动态资源供应模式下,通过 StorageClass 和 PVC 完成资源动态绑定(系统自动生成 PV),并供 pod 使用的存储管理机制。

在一个大规模的 k8s 集群里,可能有成千上万个 PVC,这就意味着运维人员必须实现创建出这个多个 PV,此外,随着项目的需要,会有新的 PVC 不断被提交,那么运维人员就需要不断的添加新的,满足要求的 PV,否则新的 pod 就会因为 PVC 绑定不到 PV 而导致创建失败。而且通过 PVC 请求到一定的存储空间也很有可能不足以满足应用对于存储设备的各种需求。

而且不同的应用程序对于存储性能的要求可能也不尽相同,比如读写速度、并发性能等,为了解决这一问题,k8s 又为我们引入了一个新的资源对象:StorageClass,通过 StorageClass 的定义,管理员可以将存储资源定义为某种类型的资源,比如快速存储、慢速存储等,用户根据 StorageClass 的描述就可以非常直观的知道各种存储资源的具体特性了,这样就可以根据应用的特性去申请合适的存储资源了。

而 StorageClass 对象的作用,其实就是创建 PV 的模板。

示例

获取指定 ns 下的 StorageClass 命令,默认是 default:

[root@k8s-master 03]# kubectl get sc
No resources found

接下来我们开始测试:

一个 k8s 中可以有多个 StorageClass,多个模版!

  1. k3s 自带一个 local-path 存储类,我们这里也创建一个 local-path-storage.yaml 去创建存储类
apiVersion: v1
kind: Namespace
metadata:
  name: local-path-storage

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: local-path-provisioner-service-account
  namespace: local-path-storage

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: local-path-provisioner-role
rules:
  - apiGroups: [ "" ]
    resources: [ "nodes", "persistentvolumeclaims", "configmaps" ]
    verbs: [ "get", "list", "watch" ]
  - apiGroups: [ "" ]
    resources: [ "endpoints", "persistentvolumes", "pods" ]
    verbs: [ "*" ]
  - apiGroups: [ "" ]
    resources: [ "events" ]
    verbs: [ "create", "patch" ]
  - apiGroups: [ "storage.k8s.io" ]
    resources: [ "storageclasses" ]
    verbs: [ "get", "list", "watch" ]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: local-path-provisioner-bind
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: local-path-provisioner-role
subjects:
  - kind: ServiceAccount
    name: local-path-provisioner-service-account
    namespace: local-path-storage

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: local-path-provisioner
  namespace: local-path-storage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: local-path-provisioner
  template:
    metadata:
      labels:
        app: local-path-provisioner
    spec:
      serviceAccountName: local-path-provisioner-service-account
      containers:
        - name: local-path-provisioner
          image: rancher/local-path-provisioner:master-head
          imagePullPolicy: IfNotPresent
          command:
            - local-path-provisioner
            - --debug
            - start
            - --config
            - /etc/config/config.json
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config/
          env:
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
      volumes:
        - name: config-volume
          configMap:
            name: local-path-config

---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

---
kind: ConfigMap
apiVersion: v1
metadata:
  name: local-path-config
  namespace: local-path-storage
data:
  config.json: |-
    {
            "nodePathMap":[
            {
                    "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES",
                    "paths":["/opt/local-path-provisioner"]
            }
            ]
    }
  setup: |-
    #!/bin/sh
    set -eu
    mkdir -m 0777 -p "$VOL_DIR"
  teardown: |-
    #!/bin/sh
    set -eu
    rm -rf "$VOL_DIR"
  helperPod.yaml: |-
    apiVersion: v1
    kind: Pod
    metadata:
      name: helper-pod
    spec:
      containers:
      - name: helper-pod
        image: busybox
        imagePullPolicy: IfNotPresent
  1. 执行 yaml 文件
[root@k8s-master k8s]# vi local-path-storage.yaml
[root@k8s-master k8s]# kubectl apply -f local-path-storage.yaml 
namespace/local-path-storage created
serviceaccount/local-path-provisioner-service-account created
clusterrole.rbac.authorization.k8s.io/local-path-provisioner-role created
clusterrolebinding.rbac.authorization.k8s.io/local-path-provisioner-bind created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
configmap/local-path-config created
[root@k8s-master k8s]# kubectl get sc
NAME         PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  17s
  1. 把这个存储类设置为默认的存储类型,执行以下命令:
kubectl patch storageclass local-path  -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
[root@k8s-master k8s]# kubectl patch storageclass local-path  -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
storageclass.storage.k8s.io/local-path patched
[root@k8s-master k8s]# kubectl get sc
NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  119s

每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件创建持久卷。 该字段必须指定。

在很多 k8s 系统中是自带的。

  1. 我们创建一个 pod 去测试是否会自动生成 pv 和 pvc,创建文件mysql-pod.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: local-path-pvc
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
  name: mysql-pod
spec:
  containers:
    - image: mysql:5.7
      name: mysql01
      env:
        - name: MYSQL_ROOT_PASSWORD
          value: "123456"
      ports:
        - containerPort: 3306
      volumeMounts:
        - mountPath: /var/lib/mysql
          name: local-mysql-data
  volumes:
    - name: local-mysql-data
      persistentVolumeClaim:
        claimName: local-path-pvc
[root@k8s-master k8s]# vim mysql-pod.yaml
[root@k8s-master k8s]# kubectl apply -f mysql-pod.yaml 
persistentvolumeclaim/local-path-pvc created
pod/mysql-pod created
  1. 查看 pv 和 pvc 是否自动创建了

k8s入门到实战(五)—— k8s存储卷详细介绍-LMLPHP

可以看到 pv 和 pvc 都自动创建了

创建 pod 时去申请 pvc,StorageClass 会动态创建 pvc 和 pv,pvc 申请多大存储 pv 就会创建多大空间

local卷也存在自身的问题,当 pod 所在节点上的存储出现故障或者整个节点不可用时,pod 和卷都会失效,仍然会丢失数据,因此最安全的做法还是将数据存储到集群之外的存储或云存储上。同时配置,做一些灾备,保证数据不会丢失!

总结

本地存储:

  • 缺点:
    • 容易丢失
    • pod 一没有可能就不见了
    • pod 换了节点启动,数据也可能没了

存储卷:

  • nfs
    • 系统就全部连接了,数据就不会再丢失
    • 方便使用
  • 问题
    • 一个系统中可能有很多的存储类型 nfs cifs hdfs oss,用户连接不方便,希望存在一个统一平台

pv

  • k8s 提供的同一存储类型对象
    • 管理声明(文件类型、大小、使用方式,权限…回收策略)

pvc

  • 用户拿着 pvc 就可以在 k8s 中自动找到 pv 绑定,从而实现数据持久化(k8s 的持久化存储策略)

storageClass

  • pv 每次手动创建十分麻烦,希望可以动态创建pv
  • storageClass 动态创建 pv 模版实现。
    • pvc 直接在 pod 中声明就好了! 不需要在手动创建 pv 了! 自动根据用户 pod 申请的 pvc 来根据 storageClass 自动创建 pv

至此,k8s 数据持久化搞定!

03-27 14:26