简介
kubernetes 使用 volume 来满足它的存储需求,它支持很多的存储系统,比如 nfs、 glusterfs、cephfs等等,但是这些存储的实现方式有一个问题,就是它们的实现代码都必须合并到 Kubernetes 的代码中(称为 in-tree),这为 kubernetes 社区带来了维护上的成本。因此,kubernetes 提出了两种 out-of-tree 的方案: FlexVolume 和 csi。通过这两种方案实现的存储功能不必合并到 kubernetes 的代码仓库,由存储系统的供应商单独维护。
FlexVolume 是这篇文章主要关注的点,FlexVolume 自 1.2 版本开始就支持了。它使用基于 exec 的模型来与驱动程序对接。用户必须在每个节点(有些情况下包括主节点)上的预定义卷插件路径中安装 FlexVolume 驱动程序的可执行文件。当需要挂载 volume 的时候,由 kubelet 执行挂载命令来挂载即可。
基于 nfs 实现 FlexVolume
在探究 FlexVolume 的实现原理之前,我们可以先看一下官方提供的基于 nfs 的例子。
注: 我这里是用 minikube
启动的本地 kubernetes 集群。
为了部署基于 nfs 实现的 FlexVolume,我们首先将目录下的 nfs 复制到 deploy
文件夹下
$ cp nfs deploy
然后将 deploy/deploy.sh
中的 dummy
修改成 nfs
,表示我们使用的插件脚本是 nfs
这个可执行文件。
接着在 deploy
文件夹下构建 docker 镜像,这里要修改 Dockerfile
,将 nfs COPY
到镜像中。然后执行下面的命令(镜像标签需要修改成你自己的):
$ docker build -t joyme/nfs-flexvolume:1.0 .
$ docker push joyme/nfs-flexvolume:1.0
镜像构建并推送完成之后,我们就开始部署了。因为 FlexVolume 要求将驱动文件放在指定的目录下,最粗暴的方式就是手动将文件 scp 到集群的每个节点上。这里为了方便,我们还可以使用 kubernetes 的 Daemenset
,然后使用 hostPath 将文件放到主机之上。我们修改 deploy
文件夹下的 ds.yaml
这个部署文件。将我们刚刚推送的镜像填进去。然后执行以下命令进行部署。
$ kubectl apply -f ds.yaml
这里有个地方要注意, 默认的插件安装地址是 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/
, 但是 kubelet 的参数 --volume-plugin-dir
和 controller manager 的参数 --flex-volume-plugin-dir
都可以修改这个值,如果你启动这些组件是指定了这些参数,那就需要修改 ds.yaml
中的路径。
在集群中部署完成之后,我们可以到某个节点上检查一下/usr/libexec/kubernetes/kubelet-plugins/volume/exec/
是否存在我们刚刚部署的文件。
最后我们创建一个 nginx,挂载一个 FlexVolume。在创建之前,我们需要先启动一个 nfs server,这里为了方便,可以使用容器启动一个。
$ docker run -d --privileged --restart=always \
-v /tmp:/dws_nas_scratch \
-e NFS_EXPORT_DIR_1=/dws_nas_scratch \
-e NFS_EXPORT_DOMAIN_1=\* \
-e NFS_EXPORT_OPTIONS_1=ro,insecure,no_subtree_check,no_root_squash,fsid=1 \
-p 111:111 -p 111:111/udp \
-p 2049:2049 -p 2049:2049/udp \
-p 32765:32765 -p 32765:32765/udp \
-p 32766:32766 -p 32766:32766/udp \
-p 32767:32767 -p 32767:32767/udp \
fuzzle/docker-nfs-server:latest
使用官方提供的 nginx-nfs.yaml
文件,然后把其中的 server 地址修改一下,使用以下命令创建:
$ kubectl apply -f nginx-nfs.yaml
注意:如果出现错误,可以检查 node 上是否安装了 jq
, nfs-common
等必要的依赖包。
实现原理
在完成上面例子的过程中,关于 FlexVolume 的大多数问题都比较好解答了。我们来看一下 nfs
的实现代码:
usage() {
err "Invalid usage. Usage: "
err "\t0 init"
err "\t0 mount <mount dir> <json params>"
err "\t0 unmount <mount dir>"
exit 1
}
err() {
echo -ne* 1>&2
}
log() {
echo -ne * >&1
}
ismounted() {
MOUNT=`findmnt -n{MNTPATH} 2>/dev/null | cut -d' ' -f1`
if [ "{MOUNT}" == "{MNTPATH}" ]; then
echo "1"
else
echo "0"
fi
}
domount() {
MNTPATH=1
NFS_SERVER=(echo 2 | jq -r '.server')
SHARE=(echo 2 | jq -r '.share')
if [(ismounted) -eq 1 ] ; then
log '{"status": "Success"}'
exit 0
fi
mkdir -p {MNTPATH} &> /dev/null
mount -t nfs{NFS_SERVER}:/{SHARE}{MNTPATH} &> /dev/null
if [ ? -ne 0 ]; then
err "{ \"status\": \"Failure\", \"message\": \"Failed to mount{NFS_SERVER}:{SHARE} at{MNTPATH}\"}"
exit 1
fi
log '{"status": "Success"}'
exit 0
}
unmount() {
MNTPATH=1
if [(ismounted) -eq 0 ] ; then
log '{"status": "Success"}'
exit 0
fi
umount {MNTPATH} &> /dev/null
if [? -ne 0 ]; then
err "{ \"status\": \"Failed\", \"message\": \"Failed to unmount volume at {MNTPATH}\"}"
exit 1
fi
log '{"status": "Success"}'
exit 0
}
op=1
if ! command -v jq >/dev/null 2>&1; then
err "{ \"status\": \"Failure\", \"message\": \"'jq' binary not found. Please install jq package before using this driver\"}"
exit 1
fi
if [ "op" = "init" ]; then
log '{"status": "Success", "capabilities": {"attach": false}}'
exit 0
fi
if [# -lt 2 ]; then
usage
fi
shift
case "op" in
mount)
domount*
;;
unmount)
unmount $*
;;
*)
log '{"status": "Not supported"}'
exit 0
esac
exit 1
其实就是一段 shell 脚本,支持三个命令: init、mount、unmount。当我们在集群中为某个 pod 挂载 FlexVolume时,该 pod 所在节点的 kubelet 会调用其指定的插件脚本执行 mount 命令,然后挂载给 pod 使用。当然了,FlexVolume 还支持更复杂的插件。这个可以看官方的文档: flexvolume
部署方案
关于如何部署 FlexVolume 的插件,其实在例子中也有提到,这里可以总结一下:
-
手动部署到每个节点的指定目录下,比如我们刚刚部署的 nfs ,其实际路径是:
/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs
。其中/usr/libexec/kubernetes/kubelet-plugins/volume/exec
是默认路径,也可以通过 kubelet 的参数--volume-plugin-dir
和 controller manager 的参数--flex-volume-plugin-dir
来指定。k8s~nfs
这个路径中,k8s
是供应商,nfs
是驱动名称,在使用的时候可以这样指定: `driver: “k8s/nfs”。 -
使用 kubernetes 的 deamonset 配合 hostPath 来部署,因为 daemonset 会在每个节点上都启动 pod,然后通过 hostPath 将插件放在指定的位置即可。kubernetes 集群中 master 节点可能被设置成不允许调度。这种情况下 daemonset 默认不调度到 master 节点上,可以使用 tolerations 来解决这个问题. 具体可参考: Scheduler is not scheduling Pod for DaemonSet in Master node
-
其实除了 kubelet 要调用插件之外,controller-manager 也要调用。比如执行
init
,attach
,detach
,waitforattach
,isattached
等命令。