Skip to content

数据存储

概述

  Pod 在运行过程中,一般情况下拥有独立的文件系统(文件系统来自于容器镜像)。当 Pod 的生命周期结束时,容器上面的创建的文件等也会随之消失,新的 Pod 的容器并没办法获取前一个容器写入文件系统内的任何内容,即使新启动的容器与原容器运行在同一个 Pod 中。

  在某些场景下,我们可能希望新的容器可以在之前的容器结束的位置继续运行。Kubernetes 通过定义存储卷来满足这个需求。存储卷不像 Pod 这样的顶级资源,而是被定义为 Pod 的一部分,并和 Pod 共享相同的生命周期。这意味着 Pod 启动时创建卷,并在删除 Pod 时销毁卷。因此在容器重新启动期间,卷的内容将保持不变,而重新启动容器之后,新容器可以识别前一个容器写入卷的所有文件。另外,如果一个 Pod 包含多个容器,那么这个卷可以同时被所有容器使用。

  有多种卷类型可供选择。其中一些是通用的,而另一些可能依赖于云服务商提供:

  • emptyDir: 用于存储临时数据的简单空目录。
  • gitRepo: 通过检出 Git 仓库的内容来初始化的卷。
  • hostPath: 用于将目录从工作节点的文件系统挂载到 Pod 中。
  • nfs: 挂载到 pod 中的 NFS 共享卷。
  • gcePersistentDisk(Google高效能型存储磁盘卷)、awsElasticBlockStore(AmazonWeb服务弹性块存储卷)、azureDisk(Microsoft Azure 磁盘盘卷): 用于挂载云服务商提供的特定存储类型。
  • cinder、cephfs、iscsi、flocker、glusterfs、quobyte、rbd、flexVolume、vsphere-Volume、photomPersistentDisk、scaleIO: 用于挂载其他类型的网络存储
  • configMap、secret、downwardAPI: 用于将 Kubernetes 部份资源和集群信息公开给 Pod 的特殊类型的卷。
  • persistentVolumeClaim: 一种使用预置或者动态配置的持久存储类型

类型

emptyDir

  emptyDir 顾名思义,卷从一个空目录开始,运行在 Pod 内的应用程序可以写入它需要的任何文件。因为卷的生存周期与 Pod 的生存周期相关联,所以当删除 Pod 时,卷的内容就会丢失。

  emptyDir 卷对于在同一个 Pod 中运行的容器之间共享文件特别有用。但是它也可以被单个容器用于将数据临时写入磁盘,例如在大型数据集上执行排序操作时,没有那么多内存可供使用。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
  - image: luksa/fortune
    name: html-generator
    volumeMounts:            # 挂载名为 html 的卷,用于自动生成 html 内容
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts:            # 挂载名为 html 的卷,并设为只读。通过 nginx 将卷里的内容展示出来
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:                   # 声明一个名为 html 的卷,挂载到上面的两个容器中
  - name: html
    emptyDir: {}

  一般情况下,Kubernetes 会在 Pod 所在的节点的磁盘上创建 emptyDir 卷,因此其性能取决于节点的磁盘类型。我们也可以通知 Kubernetes 在 tmfs 文件系统(使用内存而非硬盘)上创建 emptyDir。

yaml
  volumes:
  - name: html
    emptyDir:
      medium: Memory

gitRepo

  gitRepo 基本上也是一个 emptyDir 卷,它通过克隆 Git 仓库并在 Pod 启动时检出特定版本来填充数据。

注意

  在创建 gitRepo 卷后,它并不能和对应的 repo 保持同步。删除 Pod 重新创建的 Pod 将包含最新的提交。如果需要保持卷的内容是最新的,需要通过另一个容器去控制。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-volume-pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    gitRepo:
      repository: https://github.com/luksa/kubia-website-example.git
      revision: master
      directory: .

hostPath

  大多数 Pod 应该忽略它们所在的主机节点,因此不应该访问节点文件系统上的任何文件。但是某些系统级别的 Pod(通常由 DaemonSet 管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes 通过 HostPath 卷实现了这一点。

  HostPath 卷指向节点文件系统上的特定文件或目录。hostPath 卷在 Pod 被删除时,里面的内容不会被删除。

提示

  在一些场景下,会将数据库也部署到 Kubernetes 里,为了提升数据库性能,可能会将数据库 Pod 调度到固定节点,使用 hostPath 卷来提升数据库的读写性能。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
    - image: mysql:8.0.33
      name: mysql
      volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
      ports:
        - containerPort: 3306
          protocol: TCP
  volumes:
    - name: mysql-data
      hostPath:
        path: /data/mysql
        type: DirectoryOrCreate

  在配置 HostPath 时,type 参数用于表达不同的挂载类型,常用的 type 值如下:

  • 空字符串:默认选项,意味着挂载 HostPath 卷之前不会执行任何检查。
  • DirectoryOrCreate:如果给定路径的目录不存在,那么将根据需要创建一个权限为 0755 的空目录,和 Kubelet 具有相同的组和权限。
  • Directory:目录必须存在于给定的路径下。
  • FileOrCreate:如果给定路径的文件不存在,那么将根据需要创建一个权限为 0644 的空文件,和 Kubelet 具有相同的组和权限。
  • File:文件必须存在于给定的路径下。
  • Socket:UNIX 套接字,必须存在于给定路径中。
  • CharDevice:字符设备,必须存在于给定路径中。
  • BlockDevice:块设备,必须存在于给定路径中。

nfs

  在非生产集群上,可以搭建一个 NFS 服务器用于文件共享。但在生产环境上,一般不建议使用 NFS 作为后端存储,因为 NFS 存在单点故障,并且存在很大的性能瓶颈。

  在挂载 NFS 卷前,可以先检查服务器的 NFS 服务器是否正常。

bash
# 检测 NFS 服务器是否正常,以及获取挂载点
$ showmount -e 10.10.20.1
Export list for 10.10.20.1:
/home/data 10.10.0.1/16

提示

  可以参考初始化存储节点[链接]文档来初始化 NFS 存储服务器。

  在声明 Pod 时,就可以直接使用 NFS 卷来保存数据了。

yaml
  # 声明 nfs 卷
  volumes:
  - name: data
    nfs:
      server: 10.10.20.1
      path: /home/data

存储技术解耦

  在上述的卷声明的过程中,都要求 Pod 的开发人员了解集群中可用的真实网络存储的基础结构。例如,要创建支持 NFS 协议的卷,开发人员必须知道 NFS 节点所在的实际服务器。

  理想情况是,在 Kubernetes 上部署应用程序的开发人员不需要知道底层使用的是哪种存储技术,同理他们也不需要了解应该使用哪些类型的物理服务器来运行 Pod,与基础设施相关的交互是集群管理员独有的控制领域。

  持久卷的声明过如下:

  1. 集群管理员创建某类型的网络存储(NSF 接口或者类似的)
  2. 然后管理员通过向 Kubernetes API 传递 PV 申明来创建持久卷(PV)
  3. 用户创建一个持久卷声明(PVC)
  4. Kubernetes 找到一个具有足够容量的 PV,将其置于访问模式,并将 PVC 绑定到 PV
  5. 用户创建一个 Pod 并通过卷配置来引用 PVC

持久卷(PV)

  集群管理员提前创建 PV,用于解决对存储概念不是很了解的技术人员对存储的需求。和单独配置 Volume 类似,PV 也可以使用 NFS、HostPath 等常用的存储后端,并且可以提供更加高级的配置,比如访问模式、空间大小以及回收策略等。目前 PV 的提供方式有两种:

  • 静态 PV:由管理员提前手动创建,手动或自动(需 Volume 插件支持)回收。
  • 动态 PV:由 StorageClass 提供,根据 PVC 动态生成 PV 并绑定 PVC,由 StorageClass 根据配置自动管理 PV 的回收策略。
yaml
# 基于 NFS 的 PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: nfs-slow
  nfs:
    path: /home/data
    server: 10.10.20.1
---
# 基于 HostPath 的 PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: ssd-pv
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  hostPath:
    path: /data/mysql

  配置参数说明:

  • capacity:容量
  • volumeMode:卷的模式,目前支持 Filesystem(文件系统)和 Block(块),其中 Block 类型需要后端存储支持,默认为文件系统。
  • accessModes:PV 的访问模式
    • ReadWriteOnce:可以被单节点以读写模式挂载,命令行中被缩写为 RWO
    • ReadOnlyMany:可以被多个节点以只读模式挂载,命令行中被缩写为 ROX
    • ReadWriteMany:可以被多个节点以读写模式挂载,命令行中被缩写为 RWX
    • ReadWriteOncePod:只能被一个 Pod 以读写的模式挂载,命令中可以被缩写为 RWOP
  • persistentVolumeReclaimPolicy:PV 回收策略
    • Retain:保留,该策略允许 Kubernetes 管理员手动回收资源。当删除 PVC 时,PV 仍然存在,Volume 被视为已释放,管理员可以手动回收卷(删除 PV 并重新声明资源)。注意,该 PV 无法再次使用,如果此时有新的 PVC,该 PVC 将一直处于 Pending 状态。
    • Recycle:回收,如果 Volume 插件支持,Recycle 策略会对卷执行 rm -rf 清理该 PV,并使其可以用于下一个新的 PVC,但是本策略将来会被弃用,目前只有 NFS 和 HostPath 支持该策略。
    • Delete:删除,如果 Volume 插件支持,删除 PVC 时会同时删除 PV,动态卷默认为 Delete。
  • storageClassName:PV 的类,一个特定类型的 PV 只能绑定到特定类别的 PVC
  • nfs:NFS 服务配置。不同的 StorageClass 有不同的选项。
    • path:NFS 服务器上的共享目录
    • server:NFS 服务器 IP 地址

  通过以下命令查看已创建的 PV 的状态。

bash
$ kubectl get pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
nfs-pv   10Gi       RWO,ROX        Recycle          Available           nfs-slow                24s
ssd-pv   10Gi       RWO            Retain           Available           manual                  24s

  其中 STATUS 表示当前 PV 的状态,状态包括以下:

  • Available:可用,没有被 PVC 绑定的空闲资源
  • Bound:已绑定,已经被 PVC 绑定
  • Released:已释放,PVC 被删除,但是资源还未被重新使用
  • Failed:失败,自动回收失败

持久卷声明(PVC)

  Kubernetes 管理员提前创建了 PV 之后,用户就可以通过 PersistentVolumeClaim(PVC)来使用 PV 了。PVC 是其他技术人员在 Kubernetes 上对存储的申请,它可以标明一个程序需要用到什么样的后端存储、多大的空间以及以什么访问模式进行挂载。

  在实际使用时,虽然用户通过 PVC 获取存储支持,但是用户可能需要具有不同性质的 PV 来解决不同的问题,比如使用 SSD 硬盘来提高性能。所以集群管理员需要根据不同的存储后端来提供各种 PV,而不仅仅是大小和访问模式的区别,并且无须让用户了解这些卷的具体实现方式和存储类型,达到了存储的解藕,降低了存储使用的复杂度。

  在上面,我们声明了 ssd-pv,此时可以创建一个 PVC 即可与其绑定。

注意

  PV 和 PVC 绑定时,需要 StorageClassName 相同且其它参数一致才可以进行绑定。

yaml
apiVersion: v1
kind: PersistentVolumeClain
metadata:
  name: mysql-pvc
spec:
  resources:
    requrests:
      storage: 3Gi           # 申请 3GB 的存储空间
  accessModes:               # 允许单个客户端访问(同时支持读取和写入操作) 
    - ReadWriteOnce
  storageClassName: "manual"

  通过以下命令查看 PV、PVC 的状态。

bash
$ kubectl get pvc
NAME        STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
mysql-pvc   Bound    ssd-pv   10Gi       RWO            manual         7s

$ kubectl get pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM            STORAGECLASS   REASON   AGE
nfs-pv   10Gi       RWO,ROX        Recycle          Available                    nfs-slow                3m21s
ssd-pv   10Gi       RWO            Retain           Bound       prod/mysql-pvc   manual                  3m21s

  PV 和 PVC 状态显示,mysql-pvc 和 ssd-pv 已经绑定。

使用持久卷

  PVC 与 PV 绑定后,就可以在 Pod 中使用该 PVC 了。在卷声明中,可以直接通过名称引用 PVC。

yaml
apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
  - image: mysql:8.0.33
    name: mysql
    ports:
      - name: socket
        containerPort: 3306
        protocol: TCP
    env:
      - name: MYSQL_ROOT_PASSWORD
        value: root
    volumeMounts:
      - name: mysql-data
        mountPath: /var/lib/mysql
  volumes:
    - name: mysql-data
      # 通过名称引用 PVC
      persistentVolumeClaim:
        claimName: mysql-pvc

  下图展示了 Pod 直接使用,或者通过持久卷和持久卷声明使用 HostPath 持久卷的逻辑。

  使用 PV 和 PVC 这种方式从基础设施获取存储,对于应用程序开发人员(或者集群用户)来说更加简单,这是因为研发人员不需要关心底层实际使用的存储技术。

动态持久卷

  静态持久卷的管理过程,需要 Kubernetes 管理员事先声明 PV 之后,才能让 PVC 和 Pod 使用。当公司的 Kubernetes 集群很多,并且使用它们的技术人员过多时,对于 PV 的创建是一个很耗时、耗力的工作,并且达到一定规模后,过多的 PV 将难以维护。因此 Kubernetes 设计了 StorageClass 来动态管理集群中的 PV,这样 Kubernetes 管理员就无须浪费大量时间在 PV 的管理中。

  Kubernetes 管理员可以只创建 StorageClass 链接到不同的存储,如 Ceph、NFS、公有云提供的存储等,之后有存储需求的技术人员创建 PVC 指向这些 StorageClass 即可。StorageClass 会自动创建 PV 供 Pod 使用,也可以使用 StatefulSet 的 volumeClaimTemplate 自动分别为每一个 Pod 申请一个 PVC。

  定义一个 StorageClass 需要包含 provisioner、parameters 和 reclaimPolicy 字段,这些字段会在 StorageClass 需要动态制备 PV 时使用。其中制备器(Provisioner)[链接]用于决定使用哪个卷插件来制备 PV。

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:
  - debug
volumeBindingMode: Immediate

  在 Kubernetes in Setup 的初始化存储节点章节中,我们已经通过 Helm 初始化了 NFS StorageClass,那么我们就可以直接使用 PVC 指定该 StorageClass 来动态制备 PV 了。

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-logs-pvc
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs
  resources:
    requests:
      storage: 1Gi
---
apiVersion: apps:/v1
kind: Deployment
metadta:
  name: front-end
spec:
  template:
    spec:
      containers:
        - name: front-end
          image: nginx:1.24.0
          volumeMounts:
            - name: logs
              mountPath: /var/log/nginx
      volumes:
        - name: logs
          persistentVolumeClaim:
            claimName: nginx-logs-pvc

  声明 PVC 之后,我们再来查看一下 PV 和 PVC 的状态。

bash
# 获取 PVC 状态,发现该 PVC 已经处理绑定状态了
$ kubectl get pvc     
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nginx-logs-pvc   Bound    pvc-d330a54b-69e2-43a4-98d9-95fb4c6f35ff   1Gi        RWO            nfs            8s

# 获取 PV 状态,发现已经自动创建了一个 pvc-01f50db3-d28e-467e-881f-09efb4c69a89 持久卷,并且该持久卷的大小于 PVC 是相同的
$ kubectl get pv      
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                 STORAGECLASS   REASON   AGE
pvc-d330a54b-69e2-43a4-98d9-95fb4c6f35ff   1Gi        RWO            Delete           Bound    prod/nginx-logs-pvc   nfs                     12s

# 同时登录 storage.cluster.k8s 服务器,发现在 /home/data 目录下已经创建了一个文件夹
$ ll -h /home/data
总用量 0
drwxrwxrwx 2 root root 41 6月  19 00:09 prod-nginx-logs-pvc-pvc-d330a54b-69e2-43a4-98d9-95fb4c6f35ff

Released under the MIT license.