09-存储类型:如何挑选合适的存储插件?
在以前玩虚拟机的时代,大家比较少考虑存储的问题,因为在通过底层 IaaS 平台申请虚拟机的时候,大多数情况下,我们都会事先预估好需要的容量,方便虚拟机起来后可以稳定的使用这些存储资源。
但是容器与生俱来就是按照可以“运行在任何地方”(run anywhere)这一想法来设计的,对外部存储有着天然的诉求和依赖,并且由于容器本身的生命周期很短暂,在容器内保存数据是件很危险的事情,所以 Docker 通过挂载 Volume 来解决这一问题,如下图所示。
(https://docs.docker.com/storage/images/types-of-mounts-volume.png)
一般来说,这些 Volume 都是和容器的生命周期进行绑定的。当然也可以单独创建,然后按需挂载到容器中。大家有兴趣可以查看目前 Docker 都适配了哪些 volume plugins(卷插件)。
现在,我们先来看看 Kubernetes 中的 Volume 跟 Docker 中的设计有什么不同。
Kubernetes 中的 Volume 是如何设计的?
Kubernetes 中的 Volume 在设计上,跟 Docker 略有不同。
我们都知道在Kubernetes 中,Pod 里包含了一组容器,这些容器是可以共享存储的,如下图所示。同时 Pod 内的容器又受制于各自的重启策略(你可以回到 05 节课,回顾一下重启策略),我们需要保证容器重启不会对这些存储产生影响。因此 Kubernetes 中 Volume 的生命周期是直接和 Pod 挂钩的,而不是 Pod 内的某个容器,即 Pod 在 Volume 在。在 Pod 被删除时,才会对 Volume 进行解绑(unmount)、删除等操作。至于 Volume 中的数据是否会被删除,取决于Volume 的具体类型。
为了丰富可以对接的存储后端,Kubernetes 中提供了很多volume plugin可供使用。我将目前的一些 plugins 做了如下的分类,方便你进行初步的了解和比较。
如下图所示,Kubelet 内部调用相应的 plugin 实现,将外部的存储挂载到 Pod 内。类似于CephFS、NFS以及 awsEBS 这一类插件,是需要管理员提前在对应的存储系统中申请好的,Kubernetes 本身其实并不负责这些Volume 的申请。
(https://miro.medium.com/max/2400/1*xsxMjNGNd6C_L9Vd3zYMWA.png)
常见的几种内置 Volume 插件
我们在前文的表格中列举了很多插件,我们在此不一一讲述其具体用法,大家有兴趣,可以到官方文档中进行进一步学习。
这里我介绍几个日常工作和生产环境中经常使用到的几个插件。
ConfigMap 和 Secret
首先来看 ConfigMap 和 Secret,这两类对象都可以通过 Volume 形式挂载到 Pod 内,我们在上一节课中其实已经有过例子来讲述其作用和用法,在此不再赘述。
Downward API
再来看看DownwardAPI,这是个非常有用的插件,可以帮助你获取 Pod 对象中定义的字段,比如 Pod 的标签(Labels)、Pod 的 IP 地址及 Pod 所在的命名空间(namespace)等。Downward API 有两种使用方法,既支持环境变量注入,也支持通过 Volume 挂载。
我们来看个 Volume 挂载的例子,如下是一个 Pod 的 yaml 文件:
$ cat downwardapi-volume-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: downwardapi-volume-demo
namespace: demo
labels:
zone: us-east-coast
cluster: downward-api-test-cluster1
rack: rack-123
annotations:
annotation1: "345"
annotation2: "456"
spec:
containers:
- name: volume-test-container
image: busybox:1.28
command: ["sh", "-c"]
args:
- while true; do
if [[ -e /etc/podinfo/labels ]]; then
echo -en '\n\n'; cat /etc/podinfo/labels; fi;
if [[ -e /etc/podinfo/annotations ]]; then
echo -en '\n\n'; cat /etc/podinfo/annotations; fi;
sleep 5;
done;
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations
我们先创建这个 Pod,并通过kubectl logs
来查看它的输出日志:
$ kubectl create -f downwardapi-volume-demo.yaml
pod/downwardapi-volume-demo created
$ kubectl get pod -n demo
NAME READY STATUS RESTARTS AGE
downwardapi-volume-demo 1/1 Running 0 5s
$ kubectl logs -n demo -f downwardapi-volume-demo
cluster="downward-api-test-cluster1"
rack="rack-123"
zone="us-east-coast"
annotation1="345"
annotation2="456"
kubernetes.io/config.seen="2020-09-03T12:01:58.1728583Z"
kubernetes.io/config.source="api"
cluster="downward-api-test-cluster1"
rack="rack-123"
zone="us-east-coast"
annotation1="345"
annotation2="456"
kubernetes.io/config.seen="2020-09-03T12:01:58.1728583Z"
kubernetes.io/config.source="api"
从上面的日志输出,我们可以看到 Downward API 可以通过 Volume 挂载到 Pod 里面,并被容器获取。
EmptyDir
在 Kubernetes 中,我们也可以使用临时存储,类似于创建一个 temp dir。我们将这种类型的插件叫作 EmptyDir,从名字就可以知道,在刚开始创建的时候,就是空的临时文件夹。在 Pod 被删除后,也一同被删除,所以并不适合保存关键数据。
在使用的时候,可以参照如下的方式使用 EmptyDir:
apiVersion: v1
kind: Pod
metadata:
name: empty-dir-vol-demo
namespace: demo
spec:
containers:
- image: busybox:1.28
name: volume-test-container
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
一般来说,EmptyDir 可以用来做一些临时存储,比如为耗时较长的计算任务存储中间结果或者作为共享卷为同一个 Pod 内的容器提供数据等等。
除此之外,我们也可以将emptyDir.medium
字段设置为“Memory”,来挂载 tmpfs (一种基于内存的文件系统)类型的 EmptyDir。比如下面这个例子:
apiVersion: v1
kind: Pod
metadata:
name: empty-dir-vol-memory-demo
namespace: demo
spec:
containers:
- image: busybox:1.28
imagePullPolicy: IfNotPresent
name: myvolumes-container
command: ['sh', '-c', 'echo container is Running ; df -h ; sleep 3600']
volumeMounts:
- mountPath: /demo
name: demo-volume
volumes:
- name: demo-volume
emptyDir:
medium: Memory
HostPath
我们再来看 HostPath,它和 EmptyDir 一样,都是利用宿主机的存储为容器分配资源。但是两者有个很大的区别,就是 HostPath 中的数据并不会随着 Pod 被删除而删除,而是会持久地存放在该节点上。
使用 HostPath 非常方便,既不需要依赖外部的存储系统,也不需要复杂的配置,还能持续存储数据。但是这里我要提醒你避免滥用:
-
避免通过容器恶意修改宿主机上的文件内容;
-
避免容器恶意占用宿主机上的存储资源而打爆宿主机;
-
要考虑到 Pod 自身的声明周期,而且 Pod 是会“漂移”重新“长”到别的节点上的,所以要避免过度依赖本地的存储。
同时使用的时候也需要额外注意,因为 Hostpath 中定义的路径是宿主机上真实的绝对路径,那么就会存在同一节点上的多个 Pod 共用一个 Hostpath 的情形,比如同一工作负载的不同实例调度到同一节点上,这会造成数据混乱,读写异常。这个时候我们就需要额外设置一些调度策略,避免这种情况发生。我们会在后面的课程中,来介绍相关的调度策略。
下面是一个使用 HostPath 的例子:
apiVersion: v1
kind: Pod
metadata:
name: hostpath-demo
namespace: demo
spec:
containers:
- image: nginx:1.19.2
name: container-demo
volumeMounts:
- mountPath: /test-pd
name: hostpath-volume
volumes:
- name: hostpath-volume
hostPath:
path: /data # 对应宿主机上的绝对路径
type: Directory # 可选字段,默认是 Directory
在上面的例子中,我们要注意hostpath.type
这个可以缺省的字段。为了保证后向兼容性,默认值是 Directory。目前这个字段还支持 DirectoryOrCreate、FileOrCreate 、File 、Socket 、CharDevice 和 BlockDevice,你可以到官方文档中去了解这几个类型的具体含义。
这个 type 可以帮助你做一些预检查,比如你期望挂载的是单个文件,如果检测到挂载路径是个目录,这个时候就会报异常,这样可以有效地避免一些误配置。
上述介绍的这几款插件,目前依然能够照常使用,也是社区自身稳定支持的插件。但是对于一些云厂商和第三方的插件,社区已经不推荐继续使用内置的方式了,而是推荐你通过 CSI(Container Storage Interface,容器存储接口)来使用这些插件。
为什么社区要采用 CSI
一开始,上述这些云厂商以及第三方的卷插件(volume plugin),都是直接内置在 Kubernetes 代码库中进行开发的,目前代码库中包含 20 多个插件。但这种方式带来了很多问题。
-
这些插件对 Kubernetes 代码本身的稳定性以及安全性引入了很多未知的风险,一个很小的 Bug 都有可能导致集群受到攻击或者无法工作。
-
这些插件的维护和 Kubernetes 的正常迭代紧密耦合在一起,一起打包和编译。即便是某个单一插件出现了 Bug,都需要通过升级 Kubernetes 的版本来修复。
-
社区需要维护所有的 volume plugin,并且要经过完整的测试验证流程,来保证可用性,这给社区的正常迭代平添了很多麻烦。
-
各个卷插件依赖的包也都要算作 Kubernetes 项目的一部分,这会让 Kubernetes 的依赖变得臃肿。
-
开发者被迫要将这些插件代码进行开源。
为此,社区早在 v1.2 版本就开始尝试用 FlexVolume 插件来解决,在 v1.8 版本 GA,并停止接收任何新增的内置 volume plugin 了。用户需要遵循 FlexVolume 约定的接口规范,自己开发可执行的程序,比如二进制程序、Shell脚本等,以命令行参数作为输入,并返回 JSON 格式的结果,这样 Kubelet 就可以通过 exec 的方式调用用户的插件程序了,如下图所示。这种方式方便了各个插件的开发、更新、维护和升级,同时也和 Kubernetes 进行了解耦。在使用的时候,需要用户提前将这些二进制的文件放到各个节点上指定的目录里面(默认是/usr/libexec/kubernetes/kubelet-plugins/volume/exec/
),方便 Kubelet 可以动态发现和调用。
(https://miro.medium.com/max/840/0*OlfKs7Isngbm5mhO)
但是在实际使用中,FlexVolume 还是有很多局限性的。比如:
-
需要一些前置依赖包,像 ceph 就需要安装
ceph-common
等依赖包; -
部署很麻烦,而且往往需要很高的执行权限,要可以访问宿主机上的根文件系统。
为了彻底解决FlexVolume 开发过程中遇到的问题,CSI采用了容器化的方式进行部署,并基于 Kubernetes 的语义使用各种已定义的对象,比如下节课我们要讲的 PV、PVC、StorageClass,等等。各家厂商只需要按照 CSI 的规范实现各自的接口即可。大家可以通过这份官方的清单列表,来查看可以用于生产环境的 CSI Driver。
社区也希望用户可以尽快体会到 CSI 带来的好处,但是并不会强迫用户立马迁移并使用新的 CSI,所以社区也着手开发了CSIMIgration
功能,来帮助用户进行“无感”迁移。目前CSIMigration
在 v1.17 时已经变为了 Beta 版本。你可以根据自己使用的volume plugin 选择开启对应的功能。
CSI 为了能更通用,在设计的时候,也变得相对复杂了一些。幸好这些开发工作都由各个厂商进行开发了,我们只需要按照各家的要求进行部署即可。由于篇幅所限,我们在此不详细解释 CSI 的工作流程了。如果你想进一步了解如何开发一个 CSI driver,可以参考这份官方开发文档。
写在最后
本节课讲的 Configmap、Secret、Downward API、EmptyDir 以及 Hostpath 都是日常频繁会使用到的 volume plugin,数据都会放在 Pod 所在的宿主机上。但是对于一些云厂商或者第三方的存储系统,我建议你直接通过 CSI 来使用。
如果你需要持久化的存储,请关注我们下一节课的内容。好的,如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。