如何实现系列一:如何控制k8s pod中磁盘空间的大小

这2年基于k8s做了大量的工作,其中很多相信业界的同仁也会碰到,打算开个系列来介绍下,相关的东西如何实现,也作为这方面的一些总结

k8s集群管理必然会遇到的一个问题,磁盘空间控制问题, 业务方的代码跑在容器中,必然会生成日志和临时文件,也必然会遇到磁盘空间控制的问题,不然业务方就直接把磁盘用满了。
如果google搜索下的话基本的解决方案是:修改docker的参数 ,添加 –storage-opt dm.basesize= 参数设置为需要的值 😀

然而这不是一个可以随不同的业务和容器变化的值,即docker daemon启动后,创建的容器的根分区的大小都是这个值。并不满足我们的需求,我们需要的是根据不同的业务设置不同的大小,即我们提供给用户的容器规格除了CPU和MEM之外还有DISK的大小,当然dm.basesize这个值推荐修改(这个值也限制了镜像的大小),默认值是10G,建议修改成20G,虽然目前大部分业务镜像都是在5G之内,但是随着机器学习的兴起,相关的镜像普遍偏大,有超过10G点现象

回到正题,如何限制磁盘空间:
首先我们的容器用的磁盘都是本地盘,那么就需要从本地磁盘的方案来考虑限制。即需要有办法从本地磁盘切分不同的大小:这里推荐2个方法:LVM(lvm相关命令)以及DeviceMapper(dmsetup相关命令)。这2个方案分别在不同的地方实践过,各有利弊。即我们通过一些系统命令,在node上的物理磁盘上分配出相应大小的空间,然后通过flexvolume挂到容器中,实现控制容器磁盘大小的目的(注意这里根分区还是basesize的大小)。FlexVolume的功能请参考前文

LVM的方案

官方其实有个示例 ,不过这确实只是个示例,在实际的使用中,并无法使用,官方的示例要求你提前在物理机上创建好volume,用的时候挂上。而我们需要的是根据podSpec上的参数动态创建不同大小的volume,所以我们需要做相应的改造

前文 描述的lvm是device的模式,所以需要实现attach detach mountdevice umountdevice等操作
我们需要先pvcreate /dev/sdb2 ;vgcreate docker /dev/sdb2 这样创建好一个docker的vg用于后面的lv创建
attach中需要实现的是根据posSpec上的size设置创建相应大小的volume

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
attach() {
JSON_PARAMS=$1
SIZE=$(echo $1 | jq -r '.size')
VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.["kubernetes.io/pod.uid"]' #这里用uid作为volumeid,核心就在这个,和下面注释的地方
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
}

mountdevice中实现将volume mkfs掉,并且将目录挂载到相应的位置,这部分和官方没什么区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
domountdevice() {
MNTPATH=$1
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
}

detach和doumountdevice都是反操作,如lvremove,自己理解实现下就好,上述程序实现后,放在/usr/libexec/kubernetes/kubelet-plugins/volume/exec/kubernetes.io~lvm/lvm 即可
podSpec上需要添加如下的东西

1
2
3
4
5
6
7
8
9
10
11
volumes:
- flexVolume:
driver: kubernetes.io/lvm #注意这里和机器上文件目录是对应的
fsType: ext4
options:
size: 30Gi
volumegroup: docker
name: mnt
volumeMounts:
- mountPath: /mnt
name: mnt

DeviceMapper的方案

使用devicemapper首先得了解一些devicemapper的相关知识,其中创建thinpool的内容可以在前文中查看,也建议看看这篇文章,以及kernel上的文章

重要的几个步骤

1
2
3
4
5
6
7
8
#创建thinly-provisioned volume.
dmsetup message /dev/mapper/docker-thinpool $sector create_thin $sector
#创建具体的设备
dmsetup create $deviceName --table 0 $size thin /dev/mapper/docker-thinpool $sector
#反操作,删除设备
dmsetup remove -f $deviceName
#删除thinly-provisioned volume
dmsetup mesage /dev/mapper/docker-thinpool $sector delete $sector

之前的实现是用go写的,这里我们用bash来简单实现下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
attach() {
JSON_PARAMS=$1
SIZE=$(echo $1 | jq -r '.size')
VOLUMEID=$(echo ${JSON_PARAMS} | jq -r '.["kubernetes.io/pod.uid"]'
sector=0 #这里是个比较麻烦的地方 sector是个标识数字,不可以一样,所以需要自己先dmsetup ls找下已经占用的,然后随机找个用上,这里略过
dmsetup message /dev/mapper/docker-thinpool $sector create_thin $sector &> /tmp/${VOLUMEID} #这里创建不同大小的空间
if [ $? -ne 0 ]; then
RST=`cat /tmp/${VOLUMEID}`
RST=`echo ${RST//\"/}`
err "{ \"status\": \"Failure\", \"message\": \"Failed to create thin volume ${VOLUMEID} , Reason:${RST}\"}"
exit 1
fi
DMDEV=pod_${VOLUMEID}_$sector #注意这里由于需要有sector信息,所以巧妙的把这个保存在device设备名上,偏于后续删除的时候用
dmsetup create $DMDEV --table 0 $SIZE thin /dev/mapper/docker-thinpool $sector &> /tmp/${VOLUMEID}
if [ $? -ne 0 ]; then
RST=`cat /tmp/${VOLUMEID}`
RST=`echo ${RST//\"/}`
err "{ \"status\": \"Failure\", \"message\": \"Failed to create volume ${VOLUMEID} , Reason:${RST}\"}"
exit 1
fi
log "{\"status\": \"Success\", \"device\":\"${DMDEV}\"}"

}

mountdevice 等操作和lvm类似,这里不再赘述

总结下来说就是通过flexvolume利用一些已有的技术在本地磁盘上划分出相应空间大小的磁盘挂载到容器中,让业务往指定的目录写比如我这边都是/export目录,来达到控制磁盘空间大小的目的。 至于lvm和devicemapper的方案谁好谁坏不好简单评价,我自己的感受:lvm的方案比较成熟,周边系统文档也比较齐全;devicemapper的话我觉得最大的好处是,是thinpool的模式,用多少占多少,类似我们平常电脑上虚拟机创建动态盘的感觉,空间不是像lvm或者分区一样立刻占用掉的。这样最大的好处是我们可以创建出比物理机磁盘更大的空间,来实现超卖的功能!!!(因为实际观察磁盘多数是用不完的,lvm的模式会导致容器数受磁盘大小限制,影响我们的精细化运营)

PS: devicemapper注意选择比较高版本的内核模块,低版本的内核可能有内核bug 虽然可以通过加dm.mountopt=nodiscard 和dm.blkdiscard=false 的参数解决