存储

六、存储

现在来了解容器是如何访问外部磁盘存储的,以及如何在它们之间共享存储空间。容器中的文件在磁盘上是临时存放的,这给容器中运行的较重要的应用程序带来一些问题:

  • 当容器崩溃时文件丢失。kubelet会重新启动容器,但容器会以干净的状态重启。当容器被重建时,我们可能希望新的容器可以在之前容器结束的位置继续运行,比如在物理机上重启进程。可能不需要(或者不想要)整个文件系统被持久化, 但又希望能保存实际数据的目录。
  • 同一Pod中运行多个容器时,我们可能希望容器之间能够共享文件,但容器本身是彼此隔离的。

Kubernetes 卷(Volume)这一抽象概念能够解决这两个问题。它们不像pod这样的*资源,而是被定义为pod的一部分,并和pod共享相同的生命周期(临时卷)。这意味着在pod启动时创建卷,并在删除pod时销毁卷。因此,在容器重新启动期间,卷的内容将保持不变,在重新启动容器之后,新容器可以识别前一个容器写入卷的所有文件。针对第二个问题,如果一个pod包含多个容器,那这个卷可以同时被所有的容器使用。

1 卷介绍

Kubernetes的卷是pod的一个组成部分,因此像容器一样在pod的规范中就定义了。它们不是独立的Kubernetes对象,也不能单独创建或删除。pod中的所有容器都可以使用卷,但必须先将它挂载在每个需要访问它的容器中,在每个容器中,都可以在其文件系统的任意位置挂载卷。

有多种卷类型可供选择。其中一些是通用的, 而另一些则相对于当前常用的存储技术有较大差别。

这些卷类型有各种用途,它们的使用也大同小异,因此这里主要介绍一些常用的卷。

2 常用的卷

2.1 emptyDir

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

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

下面是一个pod中使用emptyDir卷的资源清单:

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}

通过volumes参数,创建一个名为cache-volume的空卷,挂载到容器的/cache目录下。

下面通过实例演示一下,我们在pod中创建两个容器,一个容器使用Nginx作为Web服务器代理HTML页面,另一个容器使用脚本来生成HTML内容。

首先创建一个名为fortune-pod.yaml的资源清单:

apiVersion: v1   
kind: Pod        
metadata:        
  name: fortune  
spec:      
  containers:   
    - name: html-generator  # 第一个容器名为html-generator
      image: luksa/fortune  # 运行luksa/fortune镜像
      volumeMounts:
      - mountPath: /var/htdocs # 挂载到容器的该路径下
        name: html  # 名为html的卷
    - name: web-server  # 第二个容器名为web-server
      image: nginx:alpine  # 运行nginx:alpine镜像
      volumeMounts:
      - mountPath: /usr/share/nginx/html  # 挂载到容器的该路径下
        name: html  # 名为html的卷
        readOnly: true  # 只读
      ports:
      - containerPort: 80
        protocol: TCP
  volumes:
    - name: html
      emptyDir: {}

pod包含两个容器和一个挂载在两个容器中的共用的卷,但在不同的路径上。当 html-generator容器启动时,它每10秒启动一次fortune命令输出到/var/htdocs/index.html文件,index.html文件被写入卷中。一旦web-server容器启动,它就开始为/usr/share/nginx/html目录中的任意HTML文件提供服务(这是Nginx服务的默认服务文件目录)。最终的效果是,一个客户端向pod上的80端口发送一个HTTP请求,将接收当前的fortune消息作为响应。

现在创建该pod

kubectl apply -f fortune-pod.yaml

然后查看pod的运行状态

kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE     IP           NODE         NOMINATED NODE   READINESS GATES
fortune   2/2     Running   0          6m48s   10.244.1.2   k8s-node-1   <none>           <none>

可以看到运行成功,该pod运行在node-1节点上,且IP为10.244.1.2,我们尝试访问它:

curl 10.244.1.2:80
You teach best what you most need to learn.
curl 10.244.1.2:80
You can do very well in speculation where land or anything to do with dirt is concerned.

每隔几秒发送一个请求,会接收到不同的信息。说明两个容器之间通过卷共享了index.html文件。
存储

同样,也可以进入到nginx容器内部查看这个html文件:

kubectl exec -it fortune -c web-server -- /bin/sh

进入到/usr/share/nginx/html目录下:

cd /usr/share/nginx/html
cat index.html

同样可以看到该文件的变化:
存储

并且该文件是只读的:
存储

emptyDir 的一些应用场景:

  • 缓存空间,例如基于磁盘的归并排序。
  • 为耗时较长的计算任务提供检查点,以便任务能方便地从崩溃前状态恢复执行。
  • 在 Web 服务器容器服务数据时,保存内容管理器容器获取的文件。

2.2 hostPath

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

警告:

HostPath 卷存在许多安全风险,最佳做法是尽可能避免使用 HostPath。 当必须使用 HostPath 卷时,它的范围应仅限于所需的文件或目录,并以只读方式挂载。

如果通过 AdmissionPolicy 限制 HostPath 对特定目录的访问, 则必须要求 volumeMounts 使用 readOnly 挂载以使策略生效。

hostPath卷指向节点文件系统上的特定文件或目录,同一个节点上运行并在其hostPath卷中使用相同路径的pod可以看到相同的文件。
存储

通过hostPath,可以运行一个需要访问Docker内部的容器;使用 hostPath 挂载 /var/lib/docker 路径。

hostPath也是我们介绍的第一种类型的持久性存储,因为emptyDir卷的内容会在pod被删除时被删除,而hostPath卷的内容则不会被删除。如果删除了一个pod,并且下一个pod使用了指向主机上相同路径的hostPath卷,则新pod将会发现上一个pod留下的数据,但前提是必须将其调度到与第一个pod相同的节点上。

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

支持的 type 值如下:

取值 行为
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查。
DirectoryOrCreate 如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 kubelet 相同的组和属主信息。
Directory 在给定路径上必须存在的目录。
FileOrCreate 如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 kubelet 相同的组和所有权。
File 在给定路径上必须存在的文件。
Socket 在给定路径上必须存在的 UNIX 套接字。
CharDevice 在给定路径上必须存在的字符设备。
BlockDevice 在给定路径上必须存在的块设备。

当使用这种类型的卷时要小心,因为:

  • HostPath 卷可能会暴露特权系统凭据(例如 Kubelet)或特权 API(例如容器运行时套接字), 可用于容器逃逸或攻击集群的其他部分。
  • 具有相同配置(例如基于同一 PodTemplate 创建)的多个 Pod 会由于节点上文件的不同而在不同节点上有不同的行为。
  • 下层主机上创建的文件或目录只能由 root 用户写入。你需要在 特权容器 中以 root 身份运行进程,或者修改主机上的文件权限以便容器能够写入 hostPath 卷。

下面是一个hostPath的资源清单配置:

apiVersion: v1
kind: Pod
metadata:
  name: test-pd
spec:
  containers:
  - image: k8s.gcr.io/test-webserver
    name: test-container
    volumeMounts:
    - mountPath: /test-pd
      name: test-volume
  volumes:
  - name: test-volume
    hostPath:
      # 宿主上目录位置
      path: /data
      # 此字段为可选
      type: Directory

hostPath的特点在于,只要节点可以挂载任何存储,那么节点上的pod就可以使用该存储,因此它具有很强的灵活性,可以与很多远程存储对接。

3 持久卷

当运行在一个pod中的应用程序需要将数据保存到磁盘上,并且即使该pod重新调度到另一个节点时也要求具有相同的数据可用。这就不能使用到目前为止我们提到的任何卷类型,由于这些数据需要可以从任何集群节点访问,因此必须将其存储在某种类型的网络存储(NAS) 中。

持久卷(PersistentVolume,PV)是集群中的一块存储,由管理员事先供应,或者 使用存储类(Storage Class)来动态供应。持久卷是集群资源,就像节点也是集群资源一样。

持久卷声明(PersistentVolumeClaim,PVC)表达的是用户对存储的请求。概念上与Pod类似。Pod会耗用节点资源,而PVC申领会耗用PV资源。Pod可以请求特定数量的资源(CPU和内存);同样PVC申领也可以请求特定的大小和访问模式 (例如,可以要求PV卷能够以ReadWriteOnce、ReadOnlyMany或ReadWriteMany模式之一来挂载。

举个例子:首先由集群管理员设置底层存储,然后通过Kubernetes API服务器创建持久卷并注册。在创建持久卷时,管理员可以指定其大小和所支持的访问模式。当集群用户需要在其pod中使用持久化存储时,他们首先创建持久卷声明清单,指定所需要的最低容量要求和访问模式,然后用户将持久卷声明清单提交给Kubernetes API服务器,Kubernetes将找到可匹配的待久卷并将其绑定到持久卷声明。整个流程如图所示:
存储

一旦绑定关系建立,则PVC绑定就是排他性的,无论该PVC是如何与PV建立的绑定关系。PVC与PV之间的绑定是一种一对一的映射。

注意,由于持久卷是集群资源,持久卷不属于任何命名空间。如下图所示:
存储

3.1 持久卷的类型

PV 持久卷是用插件的形式来实现的。Kubernetes 目前支持以下插件(参考官方文档):

以下的持久卷已被弃用。这意味着当前仍是支持的,但是 Kubernetes 将来的发行版会将其移除。

  • cinder - Cinder(OpenStack 块存储)(于 v1.18 弃用
  • flocker - Flocker 存储(于 v1.22 弃用
  • quobyte - Quobyte 卷 (于 v1.22 弃用
  • storageos - StorageOS 卷(于 v1.22 弃用

旧版本的 Kubernetes 仍支持这些“树内(In-Tree)”持久卷类型:

  • photonPersistentDisk - Photon 控制器持久化盘。(v1.15 之后 不可用
  • scaleIO - ScaleIO 卷(v1.21 之后 不可用

3.2 持久卷的创建

下面是一个是NFS类型的PV资源清单示例:

apiVersion: v1
kind: PersistentVolume  # 类型为PV资源
metadata:
  name: pv0003
spec:
  capacity:
    storage: 5Gi  # 容量为5G
  volumeMode: Filesystem
  accessModes:  # 访问模式
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle # 回收策略
  storageClassName: slow  # 存储类
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:  # nfs服务
    path: /tmp
    server: 172.17.0.2

3.3 访问模式

通过accessModes参数设置访问模式。PV可以用资源提供者所支持的任何方式挂载到宿主系统上。 如下表所示,每个不同类型的卷插件所支持的模式不同。

卷插件 ReadWriteOnce ReadOnlyMany ReadWriteMany ReadWriteOncePod
AWSElasticBlockStore - - -
AzureFile -
AzureDisk - - -
CephFS -
Cinder - - -
CSI 取决于驱动 取决于驱动 取决于驱动 取决于驱动
FC - -
FlexVolume 取决于驱动 -
Flocker - - -
GCEPersistentDisk - -
Glusterfs -
HostPath - - -
iSCSI - -
Quobyte -
NFS -
RBD - -
VsphereVolume - - (Pod 运行于同一节点上时可行) -
PortworxVolume - -
StorageOS - - -

访问模式有:

  • ReadWriteOnce

    卷可以被一个节点以读写方式挂载。 ReadWriteOnce 访问模式也允许运行在同一节点上的多个 Pod 访问卷。

  • ReadOnlyMany

    卷可以被多个节点以只读方式挂载。

  • ReadWriteMany

    卷可以被多个节点以读写方式挂载。

  • ReadWriteOncePod

    卷可以被单个 Pod 以读写方式挂载。 如果你想确保整个集群中只有一个 Pod 可以读取或写入该 PVC, 请使用ReadWriteOncePod 访问模式。这只支持 CSI 卷以及需要 Kubernetes 1.22 以上版本。

3.4 回收策略

通过persistentVolumeReclaimPolicy参数设置回收策略。目前的回收策略有:

  • Retain -- 手动回收
  • Recycle -- 基本擦除 (rm -rf /thevolume/*)
  • Delete -- 诸如 AWS EBS、GCE PD、Azure Disk 或 OpenStack Cinder 卷这类关联存储资产也被删除

目前,仅 NFS 和 HostPath 支持回收(Recycle)。 AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

3.5 阶段

每个卷会处于以下阶段(Phase)之一:

  • Available(可用)-- 卷是一个空闲资源,尚未绑定到任何声明上去;
  • Bound(已绑定)-- 该卷已经绑定到某声明;
  • Released(已释放)-- 所绑定的声明已被删除,但是资源尚未被集群回收;
  • Failed(失败)-- 卷的自动回收操作失败。

命令行能够显示绑定到某 PV 卷的 PVC 对象名称。

上一篇:[K8s]Kubernetes 基础(下)


下一篇:部署一个生产级别的 Kubernetes 应用(以Wordpress为例)