容器在磁盘上写的文件是临时性的,没有持久化保证。如果容器崩溃,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