容器在磁盘上写的文件是临时性的,没有持久化保证。如果容器崩溃,Kubelet会重启它,临时文件就都丢失了。此外,在Pod中运行的多个容器,可能有共享存储的需求。这两点正是K8S卷(Volume)来解决的。
Docker也有卷的概念,但是它的卷仅仅是宿主机上的一个目录,或者是另外一个容器中的目录,没有被有效的管理起来。
K8S卷具有明确的生命周期 —— 和使用它的Pod相同,Pod中所有容器都可以挂载卷。Pod存在则卷一直存在,容器重启不会导致卷数据丢失。
卷的核心,就是一个目录,但是这个目录从何而来,由什么设备后备,取决于卷类型。
容器中进程看到的文件系统视图,由Docker镜像和卷组成,Docker镜像构成根文件系统,卷则挂载到某个特定的位置。卷不能挂载到其它卷(的挂载点下面),也不能具有指向其它卷的硬链接。
K8S支持大量类型的卷:
FlexVolume是最初的卷扩展机制,从K8S 1.2版本开始出现,通过in-tree插件flexVolume进行对接。
使用FlexVolume,你可以为自己厂商的卷编写驱动,以支持K8S。驱动必须安装到每个K8S节点的卷插件目录,如果需要支持Attach操作,驱动也需要安装到Master节点。
你需要将FlexVolume驱动存放到所有节点的<plugindir>/<vendor~driver>/<driver>,plugindir默认位置为/usr/libexec/kubernetes/kubelet-plugins/volume/exec/。
如果启用了Attach/Detach功能,也就是Kubelet选项--enable-controller-attach-detach=true(默认),则也需要存放驱动到Master节点。
插件存放目录:
例如,对于名为cifs的插件,你需要存放到:/usr/libexec/kubernetes/kubelet-plugins/volume/exec/gmem.cc~cifs/cifs
vendor~driver必须和卷的flexVolume.driver字段匹配,把~换成/即可:
volumes: - flexVolume: driver: gmem.cc/cifs
从1.8开始, Flexvolume支持动态按需发现插件,不需要插件预先放到特定目录。安装好插件后,也不需要重启Kubelet或Controller manager。
一个可能的安装和升级Flexvolume插件的方式是通过DaemonSet:
flexVolume是通过命令行的方式调用插件的,就像CNI那样。
通过这些接口,你能够实现:
如果要挂载设备,你需要实现attach detach waitforattach isattached mountdevicve unmountdevice。
如果只是要挂载一个NFS目录,只需要实现mount umount。
初始化驱动,在Kubelet、Controller manager初始化时调用。格式:
<driver executable> init
如果调用成功,返回capabilities map,说明插件支持的capabilities。当前可用的capabilities包括:
由Controller manager调用。将具有指定选项的卷Attach到指定的节点上。如果操作成功,返回在节点上的设备文件路径。格式:
<driver executable> attach <json options> <node name>
由Controller manager调用。解除指定选项的卷和节点的关联。格式:
<driver executable> detach <mount device> <node name>
由Controller manager调用。等待Attach成功,超时默认10m。格式:
<driver executable> waitforattach <mount device> <json options>
由Controller manager调用。检查卷是否被Attach到节点。格式:
<driver executable> isattached <json options> <node name>
Attach往往是将分布式存储卷映射为本机设备,要使用它,还需要进行挂载。
由Kubelet调用,将设备挂载到一个全局的路径,以便由Pod进行绑定挂载。格式:
<driver executable> mountdevice <mount dir> <mount device> <json options>
由Kubelet调用,解除设备的全局挂载。一次调用就会导致所有bind挂载被umount。格式:
<driver executable> unmountdevice <mount device>
除了用户指定选项、默认JSON选项之外,以下选项自动捕获,并传递给上述命令:
kubernetes.io/pod.name kubernetes.io/pod.namespace kubernetes.io/pod.uid kubernetes.io/serviceAccount.name
由Kubelet调用,对于是实现了mountdevice+attach的驱动,如果没有实现此接口,默认行为是进行bind挂载。格式:
<driver executable> mount <mount dir> <json options>
由Kubelet调用, 对于是实现了mountdevice+attach的驱动,如果没有实现此接口,默认行为是解除bind挂载。格式:
<driver executable> unmount <mount dir>
除了用户通过卷Spec传递的选项之外,以下选项自动传递:
"kubernetes.io/fsType":"<FS type>", "kubernetes.io/readwrite":"<rw>", "kubernetes.io/fsGroup":"<FS group>", "kubernetes.io/mountsDir":"<string>", // 卷或者PV的名称 "kubernetes.io/pvOrVolumeName":"<Volume name if the volume is in-line in the pod spec; PV name if the volume is a PV>" "kubernetes.io/pod.name":"<string>", "kubernetes.io/pod.namespace":"<string>", "kubernetes.io/pod.uid":"<string>", "kubernetes.io/serviceAccount.name":"<string>", "kubernetes.io/secret/key1":"<secret1>" ... "kubernetes.io/secret/keyN":"<secretN>"
其中Secrets仅仅传递给mount/umount接口。
调用上述接口后,插件应该给出如下形式的输出:
{ // 调用结果 "status": "<Success/Failure/Not supported>", // 调用结果的原因 "message": "<Reason for success/failure>", // Attach的设备文件的路径。仅attach/waitforattach "device": "<Path to the device attached>" // 集群级别的唯一卷名称,仅getvolumename "volumeName": "<Cluster wide unique name of the volume.>" // 卷是否Attach到节点,仅isattached "attached": <True/False> // 返回插件支持capabilities,的仅Init "capabilities": { "attach": <True/False> } }
这里的flexvolume是指in-tree的卷插件,对外部Flexvolume驱动的调用,都是由它执行的。
任何一种卷插件,包括flexvolume,都需要实现VolumePlugin接口,本节阅读一下相关源码。
// 由K8S节点调用,来示例化和管理卷 type VolumePlugin interface { // 初始化插件,仅仅调用一次 Init(host VolumeHost) error // 插件的名称,必须使用命名空间化的名称,例如 example.com/volume ,只能包含一个/ // 命名空间kubernetes.io保留供K8S内部使用 GetPluginName() string // 获得唯一识别后备设备、目录或路径的名字/ID // 对于Attachable的卷,此ID必须被传递给Detach方法 // 如果指定的spec不被支持,返回错误 GetVolumeName(spec *Spec) (string, error) // 判断指定的spec能否被支持 CanSupport(spec *Spec) bool // 如果插件要求重新执行mount调用,返回true // 类似于Downward API这样自动更新的卷,依赖于此方法判断是否需要更新卷的内容 RequiresRemount() bool // 创建一个volume.Mounter NewMounter(spec *Spec, podRef *v1.Pod, opts VolumeOptions) (Mounter, error) // 创建一个volume.Unmounter NewUnmounter(name string, podUID types.UID) (Unmounter, error) // 根据卷名称、路径构建出spec,生成的spec可能是不完整的。该方法被卷管理器调用 // 通过读取磁盘上的卷目录来重新生成spec ConstructVolumeSpec(volumeName, volumePath string) (*Spec, error) // 该插件是否支持挂载选项 SupportsMountOption() bool // 是否支持针对所有节点的批量polling。用于加速Attached卷的校验 SupportsBulkVolumeVerification() bool }
扩展VolumePlugin,建模那些在Bind到Pod之前,需要先在节点上进行Mount的卷:
type DeviceMountableVolumePlugin interface { VolumePlugin NewDeviceMounter() (DeviceMounter, error) NewDeviceUnmounter() (DeviceUnmounter, error) GetDeviceMountRefs(deviceMountPath string) ([]string, error) // CanDeviceMount determines if device in volume.Spec is mountable CanDeviceMount(spec *Spec) (bool, error) }
扩展DeviceMountableVolumePlugin,建模那些在挂载之前,必须先Attach(映射为块设备)的卷:
type AttachableVolumePlugin interface { DeviceMountableVolumePlugin NewAttacher() (Attacher, error) NewDetacher() (Detacher, error) // CanAttach tests if provided volume spec is attachable CanAttach(spec *Spec) (bool, error) }
在Init阶段,卷插件通过此接口访问Kubelet,获得环境信息:
type VolumeHost interface { // 返回一个插件可以存放数据的目录,可能尚不存在 GetPluginDir(pluginName string) string // 返回一个插件可以存放数据的目录 // plugins/kubernetes.io/{PluginName}/{DefaultKubeletVolumeDevicesDirName}/{volumePluginDependentPath}/ GetVolumeDevicePluginDir(pluginName string) string // 返回所有Pod信息所在目录 GetPodsDir() string // 返回Pod专属的,某个卷插件的,某个具体卷的目录 GetPodVolumeDir(podUID types.UID, pluginName string, volumeName string) string // 返回插件存储针对某个Pod数据的目录 GetPodPluginDir(podUID types.UID, pluginName string) string GetPodVolumeDeviceDir(podUID types.UID, pluginName string) string // K8S客户端 GetKubeClient() clientset.Interface // 返回能处理指定Spec的插件 // 用于实现warap其它插件的插件。例如secret卷插件就是wrapemptyDir的插件 NewWrapperMounter(volName string, spec Spec, pod *v1.Pod, opts VolumeOptions) (Mounter, error) // 返回能处理指定Spec的插件 NewWrapperUnmounter(volName string, spec Spec, podUID types.UID) (Unmounter, error) // 从Kubelet得到云提供商信息 GetCloudProvider() cloudprovider.Interface // 得到Mounter接口,封装系统底层Mount操作 GetMounter(pluginName string) mount.Interface // kubelet所在主机名 GetHostName() string // kubelet所在主机IP GetHostIP() (net.IP, error) // 获取主机资源信息 GetNodeAllocatable() (v1.ResourceList, error) // 得到用于获取Secret内容的函数 GetSecretFunc() func(namespace, name string) (*v1.Secret, error) // 得到用于获取Configmap内容的函数 GetConfigMapFunc() func(namespace, name string) (*v1.ConfigMap, error) // 得到用于获取SA Token内容的函数 GetServiceAccountTokenFunc() func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) DeleteServiceAccountTokenFunc() func(podUID types.UID) // 用于执行宿主机命令 GetExec(pluginName string) exec.Interface // 获得节点标签 GetNodeLabels() (map[string]string, error) // 获取节点名 GetNodeName() types.NodeName // 获取事件记录器 GetEventRecorder() record.EventRecorder // 用于执行Subpath操作的接口 GetSubpather() subpath.Interface }
卷本质上是宿主机上的一个目录,该目录会被Pod(或宿主机)使用:
type Volume interface { // 宿主机的路径,卷的挂载点 GetPath() string // 用于暴露指标,例如可用空间 MetricsProvider }
该接口负责安装/挂载一个卷:
type Mounter interface { // 提供需要为Pod挂载到的卷的路径 Volume // 在Setup(挂载)之前调用,检查挂载所需要的组件(例如二进制文件)在节点上可用 // 如果返回错误,则挂载过程中止,产生一个事件,说明具体原因 // 使用 --experimental-check-mount-binaries标记可以禁用此检查 CanMount() error // 准备并挂载(解包)卷到一个自己决定的目录下。挂载点、及其内容的所有者必须是 // `fsUser` 或 'fsGroup' ,以确保能被Pod访问 // 该方法可能被多次调用,因此必须实现幂等性 // 可以返回以下类型的错误: // - TransientOperationFailure // - UncertainProgressError // - 其它类型的错误,被视为致命(final)错误 SetUp(mounterArgs MounterArgs) error // 类似上面,挂载到指定目录 SetUpAt(dir string, mounterArgs MounterArgs) error // 在Setup/SetupAt之后调用,获得Mounter的属性 GetAttributes() Attributes }
用于清理、卸载卷
type Unmounter interface { Volume // 从自己决定得路径进行卸载,并完成清理 TearDown() error // 从指定路径进行卸载,并完成清理 TearDownAt(dir string) error }
挂载相关参数:
type MounterArgs struct { // 如果设置了FsUser,则卷的所有权会被改为FsUser,并确保FsUser可以写入 // 当前仅仅被映射到Pod目录中的service account tokens支持 FsUser *int64 FsGroup *int64 FSGroupChangePolicy *v1.PodFSGroupChangePolicy DesiredSize *resource.Quantity }
flexvolume的代码位于pkg/volume/flexvolume目录下。
下面工厂函数实例化flexvolume:
func (pluginFactory) NewFlexVolumePlugin(pluginDir, name string, runner exec.Interface) (volume.VolumePlugin, error) { // 查找二进制文件 execPath := filepath.Join(pluginDir, name) driverName := utilstrings.UnescapeQualifiedName(name) flexPlugin := &flexVolumePlugin{ driverName: driverName, execPath: execPath, runner: runner, unsupportedCommands: []string{}, } // 初始化驱动,获取特性列表 call := flexPlugin.NewDriverCall(initCmd) ds, err := call.Run() if err != nil { return nil, err } flexPlugin.capabilities = *ds.Capabilities if flexPlugin.capabilities.Attach { // 如果支持Attach,则返回flexVolumeAttachablePlugin return &flexVolumeAttachablePlugin{flexVolumePlugin: flexPlugin}, nil } // 否则,返回flexVolumePlugin return flexPlugin, nil }
可以看到,驱动是否支持Attach,决定了使用何种实现。如前文所述,支持Attach的(面向设备的驱动)Flexvolume行为比较复杂,因此单独实现。
只需要mount/umount的驱动,逻辑由此实现负责。它由如下结构表示:
type flexVolumePlugin struct { // 使用的FlexVoulme驱动名称,这决定从何处寻找二进制文件 driverName string // 二进制文件路径 execPath string // 宿主机环境接口 host volume.VolumeHost // 执行命令行的接口 runner exec.Interface sync.Mutex // 驱动不支持的命令列表 unsupportedCommands []string capabilities DriverCapabilities }
公共接口的实现如下:
func (plugin *flexVolumePlugin) NewMounter(spec *volume.Spec, pod *api.Pod, _ volume.VolumeOptions) (volume.Mounter, error) { return plugin.newMounterInternal(spec, pod, plugin.host.GetMounter(plugin.GetPluginName()), plugin.runner) } func (plugin *flexVolumePlugin) newMounterInternal(spec *volume.Spec, pod *api.Pod, mounter mount.Interface, runner exec.Interface) (volume.Mounter, error) { sourceDriver, err := getDriver(spec) if err != nil { return nil, err } readOnly, err := getReadOnly(spec) if err != nil { return nil, err } var metricsProvider volume.MetricsProvider if plugin.capabilities.SupportsMetrics { metricsProvider = volume.NewMetricsStatFS(plugin.host.GetPodVolumeDir( pod.UID, utilstrings.EscapeQualifiedName(sourceDriver), spec.Name())) } else { metricsProvider = &volume.MetricsNil{} } return &flexVolumeMounter{ flexVolume: &flexVolume{ driverName: sourceDriver, execPath: plugin.getExecutable(), mounter: mounter, plugin: plugin, podName: pod.Name, podUID: pod.UID, podNamespace: pod.Namespace, podServiceAccountName: pod.Spec.ServiceAccountName, volName: spec.Name(), MetricsProvider: metricsProvider, }, runner: runner, spec: spec, readOnly: readOnly, }, nil }
可以看到,使用的Mounter是flexVolumeMounter,它的实现:
type flexVolumeMounter struct { *flexVolume runner exec.Interface spec *volume.Spec readOnly bool } func (f *flexVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error { return f.SetUpAt(f.GetPath(), mounterArgs) } // 默认挂载路径 func (f *flexVolume) GetPath() string { name := f.driverName return f.plugin.host.GetPodVolumeDir(f.podUID, utilstrings.EscapeQualifiedName(name), f.volName) } // 挂载逻辑 func (f *flexVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error { // 准备挂载点 alreadyMounted, err := prepareForMount(f.mounter, dir) if err != nil { return err } // 仅仅挂载一次 if alreadyMounted { return nil } // 调用驱动的mount命令 call := f.plugin.NewDriverCall(mountCmd) // 添加命令行参数 call.Append(dir) // 添加JSON选项 extraOptions := make(map[string]string) // Pod元数据 extraOptions[optionKeyPodName] = f.podName extraOptions[optionKeyPodNamespace] = f.podNamespace extraOptions[optionKeyPodUID] = string(f.podUID) // SA元数据 extraOptions[optionKeyServiceAccountName] = f.podServiceAccountName // 抽取Secret并传递 if err := addSecretsToOptions(extraOptions, f.spec, f.podNamespace, f.driverName, f.plugin.host); err != nil { os.Remove(dir) return err } // 隐含选项 if mounterArgs.FsGroup != nil { extraOptions[optionFSGroup] = strconv.FormatInt(int64(*mounterArgs.FsGroup), 10) } // 将VolumeSpec传递过去 call.AppendSpec(f.spec, f.plugin.host, extraOptions) // 调用命令,完成真实的挂载行为 _, err = call.Run() if isCmdNotSupportedErr(err) { err = (*mounterDefaults)(f).SetUpAt(dir, mounterArgs) } if err != nil { // 出错就删除挂载点 os.Remove(dir) return err } if !f.readOnly { // 设置卷的权限 if f.plugin.capabilities.FSGroup { volume.SetVolumeOwnership(f, mounterArgs.FsGroup, mounterArgs.FSGroupChangePolicy) } } return nil } // GetAttributes get the flex volume attributes. The attributes will be queried // using plugin callout after we finalize the callout syntax. func (f *flexVolumeMounter) GetAttributes() volume.Attributes { return (*mounterDefaults)(f).GetAttributes() } func (f *flexVolumeMounter) CanMount() error { return nil }
它内嵌了一个flexVolumePlugin,在其基础上增加Attach等能力:
type flexVolumeAttachablePlugin struct { *flexVolumePlugin }
func (plugin *flexVolumeAttachablePlugin) NewAttacher() (volume.Attacher, error) { return &flexVolumeAttacher{plugin}, nil }
flexVolumeAttacher由下面的结构表示:
type flexVolumeAttacher struct { plugin *flexVolumeAttachablePlugin }
Attach的实现:
func (a *flexVolumeAttacher) Attach(spec *volume.Spec, hostName types.NodeName) (string, error) { // 调用驱动的attach命令 call := a.plugin.NewDriverCall(attachCmd) // 传入volume spec 没用 call.AppendSpec(spec, a.plugin.host, nil) // 传入主机名 call.Append(string(hostName)) status, err := call.Run() if isCmdNotSupportedErr(err) { return (*attacherDefaults)(a).Attach(spec, hostName) } else if err != nil { return "", err } // 返回设备文件路径 return status.DevicePath, err }
由于FlexVolume使用基于命令行的接口,因此是语言无关的,你甚至可以用Shell脚本实现。
usage() { err "Invalid usage. Usage: " err "\t$0 init" err "\t$0 attach <json params> <nodename>" err "\t$0 detach <mount device> <nodename>" err "\t$0 waitforattach <mount device> <json params>" err "\t$0 mountdevice <mount dir> <mount device> <json params>" err "\t$0 unmountdevice <mount dir>" err "\t$0 isattached <json params> <nodename>" 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 } getdevice() { VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.volumeID') VG=$(echo ${JSON_PARAMS}|jq -r '.volumegroup') # LVM substitutes - with -- VOLUMEID=`echo $VOLUMEID|sed s/-/--/g` VG=`echo $VG|sed s/-/--/g` DMDEV="/dev/mapper/${VG}-${VOLUMEID}" echo ${DMDEV} } attach() { # JSON选项 JSON_PARAMS=$1 SIZE=$(echo $1 | jq -r '.size') # 卷标识,使用POD的UUID VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.["kubernetes.io/pod.uid"]' # 所属卷组(卷组需要提前准备好) VG=$(echo ${JSON_PARAMS}|jq -r '.volumegroup') # 如果逻辑卷已经存在,直接返回 DMDEV="/dev/mapper/${VG}-${VOLUMEID}" if [ -b "${DMDEV}" ]; then log "{\"status\": \"Success\", \"device\":\"${DMDEV}\"}" exit 0 fi # 开始创建逻辑卷 lvcreate -L ${SIZE} -n ${VOLUMEID} ${VG} &> /tmp/${VOLUMEID} if [ $? -ne 0 ]; then RST=`cat /tmp/${VOLUMEID}` RST=`echo ${RST//\"/}` err "{ \"status\": \"Failure\", \"message\": \"Failed to create volume ${VOLUMEID} at ${VG}, Reason:${RST}\"}" exit 1 fi /bin/rm -rf /tmp/${VOLUMEID} # 确认逻辑卷存在 if [ ! -b "${DMDEV}" ]; then err "{\"status\": \"Failure\", \"message\": \"Volume ${VOLUMEID} does not exist\"}" exit 1 fi log "{\"status\": \"Success\", \"device\":\"${DMDEV}\"}" exit 0 } detach() { log "{\"status\": \"Success\"}" exit 0 } waitforattach() { shift attach $* } domountdevice() { # 挂载路径 MNTPATH=$1 # 挂载的设备,由attach阶段创建的LV DMDEV=$2 FSTYPE=$(echo $3|jq -r '.["kubernetes.io/fsType"]') if [ ! -b "${DMDEV}" ]; then err "{\"status\": \"Failure\", \"message\": \"${DMDEV} does not exist\"}" exit 1 fi # 如果已经挂载,不做操作(幂等) if [ $(ismounted) -eq 1 ] ; then log "{\"status\": \"Success\"}" exit 0 fi # 创建文件系统 VOLFSTYPE=`blkid -o udev ${DMDEV} 2>/dev/null|grep "ID_FS_TYPE"|cut -d"=" -f2` if [ "${VOLFSTYPE}" == "" ]; then mkfs -t ${FSTYPE} ${DMDEV} >/dev/null 2>&1 if [ $? -ne 0 ]; then err "{ \"status\": \"Failure\", \"message\": \"Failed to create fs ${FSTYPE} on device ${DMDEV}\"}" exit 1 fi fi # 创建挂载点 mkdir -p ${MNTPATH} &> /dev/null # 执行挂载 mount ${DMDEV} ${MNTPATH} &> /dev/null if [ $? -ne 0 ]; then err "{ \"status\": \"Failure\", \"message\": \"Failed to mount device ${DMDEV} at ${MNTPATH}\"}" exit 1 fi log "{\"status\": \"Success\"}" exit 0 } unmountdevice() { MNTPATH=$1 if [ ! -d ${MNTPATH} ]; then log "{\"status\": \"Success\"}" exit 0 fi 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 } isattached() { log "{\"status\": \"Success\", \"attached\":true}" exit 0 } op=$1 if [ "$op" = "init" ]; then log "{\"status\": \"Success\"}" exit 0 fi if [ $# -lt 2 ]; then usage fi shift case "$op" in attach) attach $* ;; detach) detach $* ;; waitforattach) waitforattach $* ;; mountdevice) domountdevice $* ;; unmountdevice) unmountdevice $* ;; isattached) isattached $* ;; *) log "{ \"status\": \"Not supported\" }" exit 0 esac exit 1
本节的例子来自:https://blog.spider.im/post/control-disk-size-in-docker。
Leave a Reply to CSDNxm19899889 Cancel reply