1. 简介
我们都知道 Container
中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用 程序带来一些问题。
- 是当容器崩溃时文件丢失。(kubelet 会重新启动容器, 但容器会以干净的状态重启)
- 在同一
Pod
中运行多个容器如何共享文件
Kubernetes 卷(Volume)这一抽象概念能够解决这两个问题。
2. 背景
Docker 也有 卷(Volume)的概念,但对它只有少量且松散的管理。 Docker 卷是磁盘上或者另外一个容器内的一个目录。 Docker 提供卷驱动程序,但是其功能非常有限。
Kubernetes 支持很多类型的卷。 Pod 可以同时使用任意数目的卷类型。 临时卷类型的生命周期与 Pod 相同,但持久卷可以比 Pod 的存活期长。 当 Pod 不再存在时,Kubernetes 也会销毁临时卷;不过 Kubernetes 不会销毁 持久卷。对于给定 Pod 中任何类型的卷,在容器重启期间数据都不会丢失。
卷的核心是一个目录,其中可能存有数据,Pod 中的容器可以访问该目录中的数据。 所采用的特定的卷类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放 的内容。
使用卷时, 在 .spec.volumes
字段中设置为 Pod 提供的卷,并在 .spec.containers[*].volumeMounts
字段中声明卷在容器中的挂载位置。 容器中的进程看到的是由它们的 Docker 镜像和卷组成的文件系统视图。 Docker 镜像 位于文件系统层次结构的根部。各个卷则挂载在镜像内的指定路径上。 卷不能挂载到其他卷之上,也不能与其他卷有硬链接。Pod 配置中的每个容器必须独立指定各个卷的挂载位置。
3. emptyDir
当 Pod 分派到某个 Node 上时,emptyDir
卷会被创建,并且在 Pod 在该节点上运行期间,卷一直存在。 就像其名称表示的那样,卷最初是空的。 尽管 Pod 中的容器挂载 emptyDir
卷的路径可能相同也可能不同,这些容器都可以读写 emptyDir
卷中相同的文件。 当 Pod 因为某些原因被从节点上删除时,emptyDir
卷中的数据也会被永久删除。
说明: 容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃期间 emptyDir
卷中的数据是安全的。
资源模板如下:
apiVersion: v1
kind: Pod
metadata:
name: producer-consumer
spec:
containers:
- image: busybox
name: producer
volumeMounts:
- mountPath: /producer_dir
name: shared-volume
args:
- /bin/sh
- -c
- echo "hello world" > /producer_dir/hello; sleep 3000
- image: busybox
name: consumer
volumeMounts:
- mountPath: /consumer_dir
name: shared-volume
args:
- /bin/sh
- -c
- cat /consumer_dir/hello; sleep 3000
volumes:
- name: shared-volume
emptyDir: {}
创建一个pod,其中有两个container,一个是producer,另一个是consumer,producer负责生成hello文件并且写入内容hello world
,consumer 则负责读取hello文件中的内容,验证同一pod中的container存储共享机制。
因为 emptyDir 是 Docker Host 文件系统里的目录,其效果相当于在k8s-woker-01上执行了 docker run -v /producer_dir
和 docker run -v /consumer_dir
。通过 docker inspect
查看容器的详细配置信息,我们发现两个容器都 mount 了同一个目录:
$ docker inspect 52305cbb0dec
$ docker inspect c946762ccc8c
这里Source
指定的路径就是 emptyDir 在 Host 上的真正路径。
emptyDir 是 Host 上创建的临时目录,其优点是能够方便地为 Pod 中的容器提供共享存储,不需要额外的配置。但它不具备持久性,如果 Pod 不存在了,emptyDir 也就没有了。根据这个特性,emptyDir 特别适合 Pod 中的容器需要临时共享存储空间的场景,比如前面的生产者消费者用例。
4. hostPath
警告:
HostPath 卷存在许多安全风险,最佳做法是尽可能避免使用 HostPath。 当必须使用 HostPath 卷时,它的范围应仅限于所需的文件或目录,并以只读方式挂载。
hostPath
卷能将主机节点文件系统上的文件或目录挂载到你的 Pod 中。 虽然这不是大多数 Pod 需要的,但是它为一些应用程序提供了强大的逃生舱。
例如,hostPath
的一些用法有:
- 运行一个需要访问 Docker 内部机制的容器;可使用
hostPath
挂载/var/lib/docker
路径。 - 允许 Pod 指定给定的
hostPath
在运行 Pod 之前是否应该存在,是否应该创建以及应该以什么方式存在。
除了必需的 path
属性之外,用户可以选择性地为 hostPath
卷指定 type
。
支持的 type
值如下:
取值 | 行为 |
---|---|
空字符串(默认)用于向后兼容,这意味着在安装 hostPath 卷之前不会执行任何检查。 | |
DirectoryOrCreate |
如果在给定路径上什么都不存在,那么将根据需要创建空目录,权限设置为 0755,具有与 kubelet 相同的组和属主信息。 |
Directory |
在给定路径上必须存在的目录。 |
FileOrCreate |
如果在给定路径上什么都不存在,那么将在那里根据需要创建空文件,权限设置为 0644,具有与 kubelet 相同的组和所有权。 |
File |
在给定路径上必须存在的文件。 |
Socket |
在给定路径上必须存在的 UNIX 套接字。 |
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
注意: FileOrCreate
模式不会负责创建文件的父目录。 如果欲挂载的文件的父目录不存在,Pod 启动会失败。 为了确保这种模式能够工作,可以尝试把文件和它对应的目录分开挂载,如 FileOrCreate
配置所示。
hostPath FileOrCreate 配置示例
apiVersion: v1
kind: Pod
metadata:
name: test-webserver
spec:
containers:
- name: test-webserver
image: k8s.gcr.io/test-webserver:latest
volumeMounts:
- mountPath: /var/local/aaa
name: mydir
- mountPath: /var/local/aaa/1.txt
name: myfile
volumes:
- name: mydir
hostPath:
# 确保文件所在目录成功创建。
path: /var/local/aaa
type: DirectoryOrCreate
- name: myfile
hostPath:
path: /var/local/aaa/1.txt
type: FileOrCreate
5. local
local
卷所代表的是某个被挂载的本地存储设备,例如磁盘、分区或者目录。
local
卷只能用作静态创建的持久卷。尚不支持动态配置。
与 hostPath
卷相比,local
卷能够以持久和可移植的方式使用,而无需手动将 Pod 调度到节点。系统通过查看 PersistentVolume 的节点亲和性配置,就能了解卷的节点约束。
然而,local
卷仍然取决于底层节点的可用性,并不适合所有应用程序。 如果节点变得不健康,那么local
卷也将变得不可被 Pod 访问。使用它的 Pod 将不能运行。 使用 local
卷的应用程序必须能够容忍这种可用性的降低,以及因底层磁盘的耐用性特征 而带来的潜在的数据丢失风险。
下面是一个使用 local
卷和 nodeAffinity
的持久卷示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 100Gi
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 调度器使用 PersistentVolume 的 nodeAffinity
信息来将使用 local
卷的 Pod 调度到正确的节点。
PersistentVolume 对象的 volumeMode
字段可被设置为 "Block" (而不是默认值 "Filesystem"),以将 local
卷作为原始块设备暴露出来。
使用 local
卷时,建议创建一个 StorageClass 并将其 volumeBindingMode
设置为 WaitForFirstConsumer
。 延迟卷绑定的操作可以确保 Kubernetes 在为 PersistentVolumeClaim 作出绑定决策时, 会评估 Pod 可能具有的其他节点约束,例如:如节点资源需求、节点选择器、Pod 亲和性和 Pod 反亲和性。
你可以在 Kubernetes 之外单独运行静态驱动以改进对 local 卷的生命周期管理。 请注意,此驱动尚不支持动态配置。
说明: 如果不使用外部静态驱动来管理卷的生命周期,用户需要手动清理和删除 local 类型的持久卷。
6. subPath
- 同一个pod中多容器挂载同一个卷时提供隔离
- 将configMap和secret作为文件挂载到容器中而不覆盖挂载目录下的文件
6.1 同一pod中多容器挂载同一个卷时提供隔离
先创建一个共享资源的 pod 用于 设置subPath
的前后对照比较
暂未设置
subPath
,通过hostPath
的方式将文件挂载到了集群节点上
apiVersion: v1
kind: Pod
metadata:
name: hostpath-test
spec:
containers:
- image: busybox
name: test-c-01
volumeMounts:
- mountPath: /opt/test
name: shared-volume
args:
- /bin/sh
- -c
- sleep 3000
- image: busybox
name: test-c-02
volumeMounts:
- mountPath: /opt/test
name: shared-volume
args:
- /bin/sh
- -c
- sleep 3000
volumes:
- name: shared-volume
hostPath:
path: /test
type: DirectoryOrCreate
演示步骤如图所示:
此时pod 中容器挂载的卷是共享的
宿主机上的挂载情况如下
接下来加入subPath
配置,再来查看卷共享的情况
资源文件内容如下:
apiVersion: v1
kind: Pod
metadata:
name: hostpath-test
spec:
containers:
- image: busybox
name: test-c-01
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /opt/test
name: shared-volume
subPath: c01
args:
- /bin/sh
- -c
- sleep 3000
- image: busybox
name: test-c-02
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /opt/test
name: shared-volume
subPath: c02
args:
- /bin/sh
- -c
- sleep 3000
volumes:
- name: shared-volume
hostPath:
path: /test02
type: DirectoryOrCreate
演示步骤如图所示:
此时pod 中容器挂载的卷是不共享的
宿主机上的挂载情况如下
通过查看宿主机上的挂载目录可以判断出,其实是通过制定的
subPath
的创建了各自的子目录,每个容器都是用各自的subPath
,所以实现了挂载的隔离。
6.2 将configMap/secret作为文件挂载到容器中而不覆盖挂载目录下的文件
首先我们run一个普通的nginx作为前后的对照
apiVersion: v1
kind: Pod
metadata:
name: helloworld
spec:
containers:
- image: nginx
name: helloworld
imagePullPolicy: IfNotPresent
演示步骤如下:
可以看到 正常的nginx配置文件有很多
接下来 我们通过ConfigMap
方式挂载一下nginx.conf
文件
-
创建一个cm资源
普普通通,极其简单的一个配置文件
apiVersion: v1 data: nginx-conf: | worker_processes 1; events { worker_connections 1024; } http { server { listen 80; location / { root html; index index.html index.htm; } } } kind: ConfigMap metadata: name: conf-nginx
-
创建nginx-pod
也是一个很简单的nginx-pod,挂载了上面的cm资源
apiVersion: v1 kind: Pod metadata: name: helloworld spec: containers: - image: nginx name: helloworld imagePullPolicy: IfNotPresent volumeMounts: - name: config-vol # 这里不能写成 /etc/nginx/nginx.conf 是因为这样写会被解析成nginx.conf文件夹,从而导致启动失败 mountPath: /etc/nginx volumes: - name: config-vol configMap: name: conf-nginx items: - key: nginx-conf path: nginx.conf
-
演示步骤如下
这时就发现直接通过cm挂载配置文件其实是有问题的,自己想挂载的文件虽然挂载成功了,但是挂载点目录下的其他资源会丢失,这其实是个很大的隐患,因为有的服务少了部分配置文件会导致启动失败。
如果我们既想要挂载cm资源,又不想把挂载点中的其他配置文件丢失掉,这时subPath
就有作用了
apiVersion: v1
kind: Pod
metadata:
name: helloworld
spec:
containers:
- image: nginx
name: helloworld
imagePullPolicy: IfNotPresent
volumeMounts:
- name: config-vol
# 修改挂载点
mountPath: /etc/nginx/nginx.conf
# 添加subPath信息
subPath: nginx.conf
volumes:
- name: config-vol
configMap:
name: conf-nginx
items:
- key: nginx-conf
path: nginx.conf
演示步骤如下: