什么是存储插件:
容器一旦被删除,在运行时容器内部产生的所有文件数据,也会随同容器销毁而一起被清理掉,所以提供了volume方式将容器产生的临时数据持久化保存下来。
在k8s中,可通过pod.spec.volumes属性,查看默认支持的各种类型的Volume:本地存储(emptyDir / hostPath)、网络存储(如NFS)等。
k8s还提供了插件机制,允许其他类型的外部存储服务以插件方式,接入到Kubernetes系统中来。
有两种接入方式:In-Tree(在k8s源码内部实现,随k8s一起发布,更新慢不灵活) 和 Out-Of-Tree(独立于k8s,目前主要有CSI和FlexVolume两种,CSI为主流)。
存储插件的功能,最终是为应用创建出对应的volume使用。
FlexVolume插件
Flexvolume方式:需要先在每个节点上,安装存储插件的执行脚本(或可执行文件),此脚本需要实现flexvolume的相关存储接口。
当运行一个POD应用,要执行mount动作时,k8s通过kubelet调用VolumePlugin,然后直接执行插件脚本来完成的,这个插件脚本可以是shell,也可以是二进制文件,只要实现了相关的存储接口功能就行。
插件执行脚本的默认存放路径为:/usr/libexec/kubernetes/kubelet-plugins/volume/exec/<vendor~driver>/<driver>
路径中vendor~driver名称,与Pod中flexVolume.driver字段值要匹配,如foo~cifs/cifs,对应pod的driver名称就是foo/cifs。
存储接口包括:
init: 初始化存储插件时调用
attach: 将存储卷挂载到Node节点上
detach: 将存储卷从Node上卸载
waitforattach: 等待attach操作成功
isattached: 检查存储卷是否已经挂载
mountdevice: 将设备挂载到指定目录中
unmountdevice: 将设备取消挂载
mount: 将存储卷挂载到指定目录中
unmount: 将存储卷取消挂载
类似NFS存储服务,无需实现 attach/detach 这些接口,只需要实现 init/mount/umount 这三个接口即可。
执行脚本中,实现这些接口,需要返回如下json数据:
{
"status": "<Success/Failure/Not supported>",
"message": "<Reason for success/failure>",
"device": "<Path to the device attached. This field is valid only for attach & waitforattach call-outs>"
"volumeName": "<Cluster wide unique name of the volume. Valid only for getvolumename call-out>"
"attached": <True/False (Return true if volume is attached on the node. Valid only for isattached call-out)>
"capabilities": <Only included as part of the Init response>
{
"attach": <True/False (Return true if the driver implements attach and detach)>
}
}
部署案例:NFS FlexVolume 存储插件
1、到官方下载一个 NFS 的 FlexVolume 插件示例(复制脚本内容,保存到文件,并命名为nfs)
https://github.com/kubernetes/examples/blob/master/staging/volumes/flexvolume/nfs
nfs脚本内容如下:
#!/bin/bash
usage() {
err "Invalid usage. Usage: "
err "\t$0 init"
err "\t$0 mount <mount dir> <json params>"
err "\t$0 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
local NFS_SERVER=$(echo $2 | jq -r '.server')
local SHARE=$(echo $2 | jq -r '.share')
local PROTOCOL=$(echo $2 | jq -r '.protocol')
local ATIME=$(echo $2 | jq -r '.atime')
local READONLY=$(echo $2 | jq -r '.readonly')
if [ -n "${PROTOCOL}" ]; then
PROTOCOL="tcp"
fi
if [ -n "${ATIME}" ]; then
ATIME="0"
fi
if [ -n "${READONLY}" ]; then
READONLY="0"
fi
if [ "${PROTOCOL}" != "tcp" ] && [ "${PROTOCOL}" != "udp" ] ; then
err "{ \"status\": \"Failure\", \"message\": \"Invalid protocol ${PROTOCOL}\"}"
exit 1
fi
if [ $(ismounted) -eq 1 ] ; then
log '{"status": "Success"}'
exit 0
fi
mkdir -p ${MNTPATH} &> /dev/null
local NFSOPTS="${PROTOCOL},_netdev,soft,timeo=10,intr"
if [ "${ATIME}" == "0" ]; then
NFSOPTS="${NFSOPTS},noatime"
fi
if [ "${READONLY}" != "0" ]; then
NFSOPTS="${NFSOPTS},ro"
fi
mount -t nfs -o${NFSOPTS} ${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
2、将nfs脚本文件放到节点的插件目录下,并设置权限为700
mkdir /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs
mv nfs /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs
chmod 700 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/ydzs~nfs/nfs
3、创建测试Pod,并用自定义flexVolume插件来持久化容器中的数据
apiVersion: v1
kind: Pod
metadata:
name: pod-flex-demo
namespace: default
spec:
containers:
- name: busybox
image: busybox
command: [ "/bin/sh", "-c", "while true; do echo $(date) >> /data/flex_testfile; sleep 2; done" ]
imagePullPolicy: IfNotPresent
volumeMounts:
- name: test
mountPath: /data
volumes:
- name: test
flexVolume:
driver: "ydzs/nfs"
fsType: "nfs"
options:
server: xxx.xxx.xxx.xxx #NFS服务器IP
share: /data/volumes/v3
4、创建好pod后,再进入到pod容器,往里面写入数据后,查看NFS服务器上/data/volumes/v3目录下是否存在刚刚创建的数据
CSI插件
CSI存储架构由两部组成:External Components(即k8s外部组件,从k8s中存储体系中剥离出来的存储管理功能,与CSI插件交互,由K8s团队维护) + Custom Components(即CSI存储插件,由开发者实现的部分)
External Components:
1. Driver Registrar:负责将CSI插件注册到kubelet里,需要请求CSI Identity服务来获取插件信息。
2. External Provisioner:负责volume的provision阶段。当监听到一个PVC被创建,会调用CSI Controller的CreateVolume方法,创建对应PV。
3. External Attacher:负责volume的Attach阶段。会调用CSI Controller的ControllerPublish方法来完成。
Custom Components:
1. CSI Identity:向外暴露CSI插件自身的信息。
2. CSI Controller:主要实现Volume管理流程当中的Provision和Attach阶段 --- provision指创建和删除Volume,attach指将volume附着到node或脱离某个node。
3. CSI Node:主要负责Volume管理流程当中的Mount阶段 --- 即把Volume挂载至Pod容器里或从pod卸载volume。
总结:
Volume的Mount阶段,不属于External Components的职责。当需要执行Mount操作时,会直接调用CSI Node服务完成Volume的Mount阶段。
由于External Components对CSI插件的调用非常频繁,所以在实际使用CSI插件时,会将这三个外部组件作为sidecar容器,与CSI插件放置在同一个Pod中。
整个集群只需要部署一个CSI Controller,以StatefulSet或deployment方式。每个节点需要部署一个CSI Node,以DaemonSet方式。
External provisioner、External attacher与CSI插件部署在同一个StatefulSet类型的POD中,Driver registrar与CSI插件部署在同一个DaemonSet 的 Pod 中。
FlexVolume对volume的处理流程有两个阶段:Attach 与 Mount。
相比于 FlexVolume,CSI 的设计思想,把插件的职责从“两阶段处理”,扩展成了Provision、Attach 和 Mount三个阶段。其中,Provision 等价于“创建磁盘”,Attach 等价于“挂载磁盘到节点”,Mount 等价于“将磁盘格式化后,挂载到 Volume 的宿主机目录上”。
部署案例:nfs csi插件
1、下载到本地安装
git clone https://github.com/kubernetes-csi/csi-driver-nfs.git
cd csi-driver-nfs/deploy
2、先修改两个yaml文件中仓库镜像源改为国内
sed -i 's@registry.k8s.io/sig-storage@registry.aliyuncs.com/google_containers@g' csi-nfs-controller.yaml
sed -i 's@registry.k8s.io/sig-storage@registry.aliyuncs.com/google_containers@g' csi-nfs-node.yaml
sed -i 's@gcr.io/k8s-staging-sig-storage/nfsplugin:canary@registry.cn-hangzhou.aliyuncs.com/imagesfromgoogle/nfsplugin:amd64-linux-canary@g' csi-nfs-controller.yaml
sed -i 's@gcr.io/k8s-staging-sig-storage/nfsplugin:canary@registry.cn-hangzhou.aliyuncs.com/imagesfromgoogle/nfsplugin:amd64-linux-canary@g' csi-nfs-node.yaml
3、执行本地安装
cd csi-driver-nfs && ./deploy/install-driver.sh master local
# 卸载
# cd csi-driver-nfs && ./deploy/uninstall-driver.sh master local
4、查看pod
kubectl -n kube-system get pod -o wide -l app=csi-nfs-controller
kubectl -n kube-system get pod -o wide -l app=csi-nfs-node
5、创建pv
使用静态方式创建:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-csi
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
csi:
driver: nfs.csi.k8s.io
readOnly: false
volumeHandle: unique-volumeid
volumeAttributes:
server: xxx.xxx.xxx.xxx
share: /data/volumes/v3
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs-csi
spec:
volumeName: pv-nfs-csi #手动指定要绑定的pv名称
storageClassName: "" #若设置了默认的storageclass,这里要显式设置为空
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 1Gi
或用storageclass动态创建一个pv
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io #为csi-nfs-driverinfo.yaml中定义的name
parameters:
server: 116.196.98.49
share: /data/volumes/v3
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- hard
- nfsvers=4.1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs-csi-dynamic
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2Gi
storageClassName: nfs-csi