Kubernetes in Action 笔记 —— 向容器挂载存储卷

Volumes 介绍

Pod 就像是一个部署着某个应用的逻辑化的计算机,可以包含一个或几个容器,运行着应用的各个进程。这些进程可以共享 CPU、RAM、网络接口等计算资源。
在普通的计算机中,应用的进程使用同一个文件系统。但是在 Pod 中,每个容器都有自己独有的、由容器镜像提供的隔离的文件系统。

容器构建时添加到镜像中的文件构成了容器的文件系统,容器启动后,运行在容器中的进程就可以修改这些文件或者添加新的文件。
但是当容器终止和重启时,所有对于文件系统的改动都不会被保留。事实上容器并不是真正意义上的重启,而是完全地被一个新容器替换掉了。这对于一些类型的应用是 OK 的,其他应用则需要在重启时至少保留一部分文件系统。
这可以通过挂载 Volume 来实现。

Mounting a filesystem into the file tree

Volumes 与 Pod

Volumes 并不是 Pod 或 Node 那样的顶层资源,而是和容器一样是 Pod 中的一个组件,因而会使用 Pod 的生命周期。Volumes 与其挂载的容器的生命周期是毫无关联的,也因此能够用来在容器重启时保持数据。

Volumes are defined at the pod level and mounted in the pod’s containers

Volumes 在 Pod 中定义,挂载到 Pod 下的容器中。

在容器重启时保持数据
Pod 中的所有 Volumes 都会在容器启动前创建,在 Pod 关闭时销毁。
应用可以向挂载到容器文件系统中的 Volume 写入数据。当容器重启时,其被替换为一个新容器,文件系统也重新从镜像创建。此时则可以再次挂载同样的 Volume,完成对之前数据的访问。
Volumes ensure that part of the container’s filesystem is persisted across restarts

通常由应用的作者决定哪些文件需要在容器重启时被保留,一般是一些代表应用状态的数据。但不包括应用的本地缓存数据,这些数据会阻止容器完成一次“全新”的重启。而全新的重启有利于应用的“自愈”。

在一个容器中挂载多个 Volume
一个 Pod 可以包含多个 Volumes,Pod 中的每个容器都可以挂载这些 Volumes 中的零个或多个。
A pod can contain multiple volumes and a container can mount multiple volumes

在多个容器间共享文件
一个 Volume 可以被同时挂载到多个容器中,从而这些容器中的应用可以共享文件。
比如可以创建一个 Pod,包含一个 Web Server 容器和一个 content-producing agent 容器。Agent 容器负责生成静态媒体内容保存至 Volume,Web 容器则将 Volume 中的内容发布给客户端。
A volume can be mounted into more than one container

在 Pod 重启时保持数据
Volume 与 Pod 的生命周期绑定,只会在 Pod 存在时存在。但是依靠某些特殊的 Volume 类型,其中的文件也可以在 Pod 和 Volume 消失后继续存在,后续也能够挂载到一个新的 Volume 中。
Pod volumes can also map to storage volumes that persist across pod restarts

如上图所示,Pod 中的 Volume 可以映射到 Pod 外面的永久存储。代表 Volume 的文件目录并不是 Pod 内部的本地路径,而是挂载的一个脱离了 Pod 生命周期的 NAS。
假如 Pod 被删除并被一个新的 Pod 所替换,同一个 NAS 可以被关联到新的 Pod 实例中,从而前一个 Pod 保持的数据可以被新的 Pod 访问。

在多个 Pod 间共享文件
取决于 external storage volume 使用的具体技术,同一个外挂存储也能够同时被关联给多个 Pod,从而这些 Pod 之间能够共享数据。
Using volumes to share data between pods

Volume 类型

当向某个 Pod 添加 Volume 时,必须指定 Volume 类型。主要的几种 Volume 类型如下:

  • emptyDir:一个简单的空目录,允许 Pod 在其生命周期内向该路径下存储数据
  • hostPath:从工作节点的文件系统向 Pod 挂载文件
  • nfs:挂载到 Pod 中的 NFS 共享
  • gcePersistentDisk (Google Compute Engine Persistent Disk), awsElasticBlockStore (Amazon Web Services Elastic Block Store), azureFile (Microsoft Azure File Service), azureDisk (Microsoft Azure Data Disk)
  • cephfs, cinder, fc, flexVolume, flocker, glusterfs, iscsi, portworxVolume, quobyte, rbd, scaleIO, storageos, photonPersistentDisk, vsphereVolume
  • configMap, secret, downwardAPI, projected:特殊类型的 Volume,用来暴露 Pod 及其他 Kubernetes 对象的信息
  • csi:一种可插拔的通过 Container Storage Interface 添加存储的方式。任何人都可以使用这种 Volume 类型来实现自己的存储驱动

使用 Volumes

使用 emptyDir 在容器重启时保留文件

emptyDir 是最简单的 Volume 类型。它最开始以空目录的形式挂载到容器的文件系统,任何写入到该路径下的文件都会在 Pod 的整个生命周期中存在。
这类 Volume 通常用于在容器重启后保留部分数据;或者当整个容器的文件系统都是只读时,令其部分文件系统可写;又或者在包含两个或以上容器的 Pod 中,在各容器间传递数据。

向 Pod 添加 emptyDir Volume
创建如下 fortune-emptydir.yml 清单文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kind: Pod
metadata:
name: fortune-emptydir
spec:
volumes:
- name: content
emptyDir: {}
containers:
- name: nginx
image: nginx:alpine
volumeMounts:
- name: content
mountPath: /usr/share/nginx/html
lifecycle:
postStart:
exec:
command:
- sh
- -c
- "ls /usr/share/nginx/html/quote || (apk add fortune && fortune > /usr/share/nginx/html/quote)"
ports:
- name: http
containerPort: 80

当容器第一次启动时,postStart hook 会执行 apk add fortune 命令安装 fortune 软件包,并执行 fortune > /usr/share/nginx/html/quote 创建 quote 文件。
后续若容器因为各种原因重启,由于 quote 文件位于挂载的 Volume 中,不会随着容器一同被销毁。
并且在新容器生成后仍会挂载到原来的路径下,即不管后续容器如何重启,quote 文件都会保持第一次创建时的状态。

若没有挂载 Volume,则 quote 文件会在容器重启时随着旧容器一同被销毁,每次新容器生成,都会在同样的路径下产生一个新的不同版本的 quote 文件。

运行 kubectl apply -f fortune-emptydir.yml 命令应用清单文件,检查容器中 Volume 的挂载状态:

1
2
3
4
$ kubectl apply -f fortune-emptydir.yml
pod/fortune-emptydir created
$ kubectl exec fortune-emptydir -- mount --list | grep nginx/html
/dev/sdb on /usr/share/nginx/html type ext4 (rw,relatime,discard,errors=remount-ro,data=ordered)

使用 emptyDir 在容器间共享文件

创建如下内容的清单文件 fortune.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: fortune
spec:
volumes:
- name: content
emptyDir: {}
containers:
- name: fortune
image: luksa/fortune-writer:1.0
volumeMounts:
- mountPath: /var/local/output
name: content
- name: nginx
image: nginx:alpine
volumeMounts:
- mountPath: /usr/share/nginx/html
name: content
readOnly: true
ports:
- containerPort: 80
name: http

其中 fortune 容器是作者自己构建的,会每隔 30s 执行一次 fortune > /var/local/output/quote 命令。fortune 每次执行都会随机输出一段名言警句类型的话,即 /var/local/output/quote 文件中的内容会每隔 30s 变化一次。
整个 Pod 包含两个容器和一个 Volume,其中 fortune 容器挂载 content Volume 到自己的 /var/local/output 路径下,而 nginx 容器挂载同一个 content Volume 到自己的 /usr/share/nginx/html 路径下。
fortune 容器会每隔 30s 生成一个新的 quote 文件,保存到挂载的 content Volume 下;而另一个 nginx 容器也挂载了 content Volume,并且将其中的 quote 文件作为 Web 服务的静态文件向外提供服务。
即同一个 Pod 中的两个容器通过挂载同一个 Volume 实现文件的共享。

运行 Pod
使用 kubectl apply -f fortune.yml 命令应用清单文件创建 Pod,再分别从两个容器中查看 Volume 下 quote 文件的内容:

1
2
3
4
5
6
7
8
$ kubectl apply -f fortune.yml
pod/fortune configured
$ kubectl exec fortune -c fortune -- cat /var/local/output/quote
Life is too important to take seriously.
-- Corky Siegel
$ kubectl exec fortune -c nginx -- cat /usr/share/nginx/html/quote
Life is too important to take seriously.
-- Corky Siegel

后两条命令输出了同样的内容。虽然两个容器查看的 quote 文件本地路径不同,它们实际上指向了同一个 Volume。

理解 external volumes 是如何挂载的

如下图所示,网络存储卷首先是被宿主节点挂载,然后再授予 Pod 访问挂载点的权限。
Network volumes are mounted by the host node and then exposed in pods

通常情况下,底层的存储技术并不允许一个 Volume 以读写模式同时挂载到一个以上的节点,但是同一个节点上的多个 Pods 可以同时以读写模式使用 Volume。
对于云环境提供的大多数存储技术,多个节点使用同一个网络存储卷的方式只有一种,即以只读模式挂载。
在设计分布式应用的架构时,考虑网络存储卷的上述限制是很有必要的。同一个 Pod 的多个副本通常不能以读写模式挂载同一个网络存储卷。

访问工作节点上的文件系统

绝大多数 Pods 不应该关注部署它们的宿主节点,不应该访问节点文件系统中的任何文件。除非这些 Pods 是系统级别的。
可以使用 hostPath 类型的 Volume 令 Pod 能够访问宿主节点。

hostPath Volume 介绍

hostPath Volume 指向宿主节点文件系统中的特定文件或目录,形成 Pod 与宿主节点之间的文件共享。
hostPath volume mounts a file or directory from the worker node’s filesystem into the container

hostPath Volume 并不适合存放数据库中的数据。因为此 Volume 的内容只是保存在某个特定的工作节点上,假如数据库 Pod 被重新分配给了另一个节点,则保存在 Volume 中的数据库数据对新的 Pod 不再可见。

通常情况下,hostPath Volume 只用在当 Pod 确实是需要读写 Node 中的文件,比如 Node 上的系统日志。
hostPath 是最危险的 Volume 类型之一,一般只用于具有特殊权限的 Pod。假如不对 hostPath 的使用加以限制,用户有可能对工作节点做任何事。
比如用户可以使用 hostPath 挂载容器的 Docker socket 文件,然后在容器内运行 Docker 客户端,接着便可以作为 root 用户在宿主节点上执行任意命令。

使用 hostPath Volume

部署如下配置的 Pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: node-explorer
spec:
volumes:
- name: host-root
hostPath:
path: /
containers:
- name: node-explorer
image: alpine
command:
- "sleep"
- "9999999999"
volumeMounts:
- mountPath: /host
name: host-root

Pod 部署完成后,即可运行 kubectl exec -it node-explorer -- sh 命令在 Pod 中运行一个交互式命令行窗口,在执行 cd /host 命令即可进入宿主节点的文件系统:

1
2
3
4
5
6
$ kubectl exec -it node-explorer -- sh
/ # cd /host
/host # ls
Release.key boot dev etc kic.txt lib lib64 media opt root sbin sys usr
bin data docker.key home kind lib32 libx32 mnt proc run srv tmp var
/host #

hostPath Volume 指向的是宿主节点的 / 路径,由此整个工作节点的文件系统都会向 Pod 开放。执行完上面的命令后,即可以修改工作节点上的任何文件。

参考资料

Kubernetes in Action, Second Edition