什么是Kubernetes本地存储卷?

今天我们一起来看看什么是Kubernetes本地存储卷以及本地存储卷的使用场景。

1、什么是Volumes?

容器中的文件在磁盘上一般都是临时存放的,这给容器中运行的特殊应用程序带来一些问题。 首先,当容器崩溃时,kubelet 将重新启动容器,容器中的文件将会丢失——因为容器会以干净的状态重建。 其次,当在一个 Pod 中同时运行多个容器时,常常需要在这些容器之间共享文件。 Kubernetes 抽象出 Volume 对象来解决这两个问题。

其实 Docker 中也有 Volume 的概念,但对它只有少量且松散的管理。 在 Docker 中,Volume 是磁盘上或者另外一个容器内的一个目录。 直到最近,Docker 才支持对基于本地磁盘的 Volume 的生存期进行管理。 虽然 Docker 现在也能提供 Volume 驱动程序,但是目前功能还非常有限(例如,截至 Docker 1.7,每个容器只允许有一个 Volume 驱动程序,并且无法将参数传递给卷)。

另一方面,Kubernetes 卷具有明确的生命周期——与包裹它的 Pod 相同。 因此,卷比 Pod 中运行的任何容器的存活期都长,在容器重新启动时数据也会得到保留。 当然,当一个 Pod 不再存在时,卷也将不再存在。也许更重要的是,Kubernetes 可以支持许多类型的卷,Pod 也能同时使用任意数量的卷。 卷的核心是包含一些数据的目录,Pod 中的容器可以访问该目录。 特定的卷类型可以决定这个目录如何形成的,并能决定它支持何种介质,以及目录中存放什么内容。 使用卷时, Pod 声明中需要提供卷的类型 (.spec.volumes 字段)和卷挂载的位置 (.spec.containers.volumeMounts 字段). 容器中的进程能看到由它们的 Docker 镜像和卷组成的文件系统视图。 Docker 镜像 位于文件系统层次结构的根部,并且任何 Volume 都挂载在镜像内的指定路径上。 卷不能挂载到其他卷,也不能与其他卷有硬链接。 Pod 中的每个容器必须独立地指定每个卷的挂载位置。

Kubernetes支持的Volume类型如下:

从上面我们可以看到、Kubernetes支持几十种类型的后端存储卷,但是其中有几种存储卷总是给人一种分不清楚它们之间有什么区别的感觉,尤其是local与hostPath这两种存储卷类型,看上去都像是node本地存储方案嘛。当然,还另有一种volume类型是emptyDir,也有相近之处。

那么问题来了、既然我们都已经实现容器云平台管理了、为什么还需要本地存储卷呢?本地存储卷的使用场景是什么呢?

本地存储卷的使用场景如下:

  • 特殊使用场景需求,例如临时存储空间;再比如运行cAdvisor需要能访问到node节点/sys/fs/cgroup的数据或者做本机单节点的 Kubernetes 环境功能测试等
  • 容器集群只是做小规模部署,满足开发测试、集成测试需求。
  • 作为分布式存储服务的一种补充手段,比如在一台node主机上插了一块SSD,准备给某个容器吃小灶。
  • 目前主流的两个容器集群存储解决方案是 ceph和glusterfs,二者都是典型的网络分布式存储,所有的数据读、写都是对磁盘IO和网络IO的考验,所以部署存储集群时至少要使用万兆的光纤网卡和光纤交换机。如果你都没有这些硬货的话,强上分布式存储方案的结果就是收获一个以”慢动作”见长的容器集群啦。
  • 分布式存储集群服务的规划、部署和长期的监控、扩容与运行维护是专业性很强的工作,需要有专职的技术人员做长期的技术建设投入。

下面我们一起来看看前面提到的这三种本地存储卷:emptyDir、local与hostPath。

2、emptyDir

当 Pod 指定到某个节点上时,首先创建的是一个 emptyDir 卷,并且只要 Pod 在该节点上运行,卷就一直存在。 就像它的名称表示的那样,卷最初是空的。 尽管 Pod 中的容器挂载 emptyDir 卷的路径可能相同也可能不同,但是这些容器都可以读写 emptyDir 卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir 卷中的数据也会永久删除。

通俗点讲:emptyDir类型的Volume在Pod分配到Node上时被创建,Kubernetes会在Node上自动分配一个目录,因此无需指定宿主机Node上对应的目录文件。 这个目录的初始内容为空,当Pod从Node上移除时,emptyDir中的数据会被永久删除。

注:容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃时 emptyDir 卷中的数据是安全的。

emptyDir可以在以下几种场景下使用:

  • 临时空间,例如基于磁盘的合并排序;
  • 设置检查点以从崩溃事件中恢复未执行完毕的长计算;
  • 保存内容管理器容器从Web服务器容器提供数据时所获取的文件;

默认情况下, emptyDir 卷存储在支持该节点所使用的介质上;这里的介质可以是磁盘或 SSD 或网络存储,这取决于您的环境。 但是,您可以将 emptyDir.medium 字段设置为 “Memory”,以告诉 Kubernetes 为您安装 tmpfs(基于 RAM 的文件系统)。 虽然 tmpfs 速度非常快,但是要注意它与磁盘不同。 tmpfs 在节点重启时会被清除,并且您所写入的所有文件都会计入容器的内存消耗,受容器内存限制约束。

注:在使用tmpfs文件系统作为emptyDir的存储后端时,如果遇到node节点重启,则emptyDir中的数据也会全部丢失。同时,你编写的任何文件也都将计入Container的内存使用限制。

下面我们可以通过一个示例来看看 emptyDir volume 的使用:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
  - image: busybox
    name: test-emptydir
    command: [ "sleep", "3600" ]
    volumeMounts:
    - mountPath: /data
      name: data-volume
  volumes:
  - name: data-volume
    emptyDir: {}

我们直接创建上面的资源对象、创建完成之后我们通过describe命令查看该Pod的详情:

[root@kubernetes-01 ~]# kubectl describe pod test-pod
Name:         test-pod
Namespace:    default
Priority:     0
Node:         kubernetes-03/172.16.200.13
Start Time:   Sat, 23 May 2020 22:46:42 +0800
......
Containers:
  test-emptydir:
    Container ID:  docker://008fcb1a02698f295d07304e161f16fb07b3e64911d690e2d12bedd10013ab80
    Image:         busybox
    ......
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /data from data-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-lhngs (ro)
Conditions:
......
Volumes:
  data-volume:
    Type:       EmptyDir (a temporary directory that shares a pod's lifetime)
    Medium: 
......

我们可以看到上面创建的Pod Volumes 类型是:EmptyDir。后面也提示我们了、这是一个临时目录。当然、你也可以通过下面的命令进入到容器中查看实际的卷挂载效果:

kubectl exec -it test-pod -c test-emptydir /bin/sh

3、hostPath

hostPath类型则是映射node文件系统中的文件或者目录到pod里。在使用hostPath类型的存储卷时,也可以设置type字段,支持的类型有文件、目录、File、Socket、CharDevice和BlockDevice。

hostPath可以在以下几种场景下使用:

  • 运行一个需要访问 Docker 引擎内部机制的容器;请使用 hostPath 挂载 /var/lib/docker 路径。
  • 在容器中运行 cAdvisor 时,以 hostPath 方式挂载 /sys。
  • 允许 Pod 指定给定的 hostPath 在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。

当然、除了必需的 path 属性之外,用户可以选择性地为 hostPath 卷指定 type。

注:当使用这种类型的卷时要小心,因为: 具有相同配置(例如从 podTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。 当 Kubernetes 按照计划添加资源感知的调度时,这类调度机制将无法考虑由 hostPath 使用的资源。 基础主机上创建的文件或目录只能由 root 用户写入。您需要在 特权容器 中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

下面我们还是通过一个示例来看看 hostPath volume 的使用:

apiVersion: v1
kind: Pod
metadata:
  name: test-pod2
spec:
  containers:
  - image: busybox
    name: test-hostpath
    command: [ "sleep", "3600" ]
    volumeMounts:
    - mountPath: /test-data
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # directory location on host
      path: /data
      # this field is optional
      type: Directory

我们还是同样直接创建上面的资源对象,创建完成之后我们通过describe命令查看该Pod的详情:

[root@kubernetes-01 ~]# kubectl describe pod test-pod2
Name:         test-pod2
Namespace:    default
Priority:     0
Node:         kubernetes-02/172.16.200.12
Start Time:   Sat, 23 May 2020 22:59:34 +0800
Labels:       <none>
Annotations:  cni.projectcalico.org/podIP: 172.30.218.139/32
              kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"test-pod2","namespace":"default"},"spec":{"containers":[{"command":["...
Status:       Running
IP:           172.30.218.139
IPs:
  IP:  172.30.218.139
Containers:
  test-hostpath:
......
    Environment:    <none>
    Mounts:
      /test-data from test-volume (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-lhngs (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  test-volume:
    Type:          HostPath (bare host directory volume)
    Path:          /data
    HostPathType:  Directory
......

我们可以看到上面创建的Pod Volumes 类型是:HostPath ,这是一个裸主机目录卷。我们通过下面的命令登录到容器中去,进入到挂载的test-data目录中,创建一个测试文件:

[root@kubernetes-01 ~]# kubectl get pod 
NAME                        READY   STATUS    RESTARTS   AGE
dnsutils-ds-5nn8b           1/1     Running   25         4d9h
dnsutils-ds-7t4x4           1/1     Running   25         4d9h
dnsutils-ds-vtjbr           1/1     Running   25         4d9h
my-nginx-86575b68dc-mc92g   1/1     Running   2          4d10h
my-nginx-86575b68dc-vvvgj   1/1     Running   2          4d10h
nginx-ds-99ck8              1/1     Running   2          5d
nginx-ds-ccx7s              1/1     Running   2          5d
nginx-ds-xz4ft              1/1     Running   2          5d
test-pod                    1/1     Running   0          18m
test-pod2                   1/1     Running   0          5m15s
[root@kubernetes-01 ~]# kubectl exec -it test-pod2 /bin/sh
/ # cd test-data/
/test-data # vi test-pod2.log
/test-data # cat test-pod2.log 
z0ukun
/test-data # exit

然后我们在运行该Pod的Node节点上(上面的Pod被调度到Kubernetes-02主机上了),看看是否有我们刚才在容器中创建的文件和内容:

[root@kubernetes-02 ~]# cd /data/
[root@kubernetes-02 data]# ls
k8s  test-pod2.log
[root@kubernetes-02 data]# cat test-pod2.log 
z0ukun
[root@kubernetes-02 data]# 

现在我们把该Pod删掉、再看看节点上的HostPath目录与数据会有上面变化:

[root@kubernetes-01 ~]# kubectl delete -f hostPath.yaml 
pod "test-pod2" deleted

[root@kubernetes-02 data]# ls
k8s  test-pod2.log
[root@kubernetes-02 data]# cat test-pod2.log 
z0ukun
[root@kubernetes-02 data]# 

从上面的内容我们可以看到在使用hostPath volume卷时,即便Pod已经被删除了,volume卷中的数据仍然还在。

虽然emptyDir和hostPath都可以实现本地存储卷的功能、但是二者在功能上还是有异同的:

  • 二者都是node节点的本地存储卷方式;
  • emptyDir可以选择把数据存到tmpfs类型的本地文件系统中去,hostPath并不支持这一点;
  • hostPath除了支持挂载目录外,还支持File、Socket、CharDevice和BlockDevice,既支持把已有的文件和目录挂载到容器中,也提供了“如果文件或目录不存在,就创建一个”的功能;
  • emptyDir是临时存储空间,完全不提供持久化支持;
  • hostPath的卷数据是持久化在node节点的文件系统中的,即便pod已经被删除了,volume卷中的数据还会留存在node节点上;

4、local

local 卷指的是所挂载的某个本地存储设备,例如磁盘、分区或者目录。 local 卷只能用作静态创建的持久卷。尚不支持动态配置。

相比 hostPath 卷,local 卷可以以持久和可移植的方式使用,而无需手动将 Pod 调度到节点,因为系统通过查看 PersistentVolume 所属节点的亲和性配置,就能了解卷的节点约束。 然而,local 卷仍然取决于底层节点的可用性,并不是适合所有应用程序。 如果节点变得不健康,那么local 卷也将变得不可访问,并且使用它的 Pod 将不能运行。 使用 local 卷的应用程序必须能够容忍这种可用性的降低,以及因底层磁盘的耐用性特征而带来的潜在的数据丢失风险。

注: Local volume 允许用户通过标准PVC接口以简单且可移植的方式访问node节点的本地存储。 PV的定义中需要包含描述节点亲和性的信息,k8s系统则使用该信息将容器调度到正确的node节点。

使用要求:

使用local-volume插件时,要求使用到了存储设备名或路径都相对固定,不会随着系统重启或增加、减少磁盘而发生变化。

静态provisioner配置程序仅支持发现和管理挂载点(对于Filesystem模式存储卷)或符号链接(对于块设备模式存储卷)。 对于基于本地目录的存储卷,必须将它们通过bind-mounted的方式绑定到发现目录中。

下面我们来创建一个基于Local Volumes的PV示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 100Gi
  # volumeMode field requires BlockVolume Alpha feature gate to be enabled.
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/ssd1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - example-node

注:使用 local 卷时,需要使用 PersistentVolume 对象的 nodeAffinity 字段。 它使 Kubernetes 调度器能够将使用 local 卷的 Pod 正确地调度到合适的节点。可以将 PersistentVolume 对象的 volumeMode 字段设置为 “Block”(而不是默认值 “Filesystem”),以将 local 卷作为原始块设备暴露出来。 volumeMode 字段需要启用 Alpha 功能 BlockVolume。

当使用 local 卷时,建议创建一个 StorageClass,将 volumeBindingMode 设置为 WaitForFirstConsumer。 请参考 示例。 延迟卷绑定操作可以确保 Kubernetes 在为 PersistentVolumeClaim 作出绑定决策时,会评估 Pod 可能具有的其他节点约束,例如:如节点资源需求、节点选择器、Pod 亲和性和 Pod 反亲和性。

感兴趣的同学可以手动创建一下上面的资源对象进行测试。这里需要说明一下:local volume受node节点可用性方面的限制,因此并不适用于所有应用程序。 如果node节点变得不健康,则local volume也将变得不可访问,使用这个local volume的Pod也将无法运行。 使用local voluems的应用程序必须能够容忍这种降低的可用性以及潜在的数据丢失,是否会真得导致这个后果将取决于node节点底层磁盘存储与数据保护的具体实现了。

推荐文章