Kubernetes Pod进本地磁盘(local,disk,LVM) 进行流量控制
一. 继承上一章节
混合云场景业务Pod直接相互干扰
、 在离线混部
(在离线服务同时在一台机器上服务用户) 等场景下,除了对cpu
、mem
、fd
、inode
、pid
等进行隔离,还需要对 网络带宽bandwidth
、磁盘读写速度IPOS
、NBD IO
、L3 Cache
、内存带宽MBA
等都需要做到隔离和限制
因此,本章节介绍下 磁盘读写速度IPOS
的使用和实现
二. Kubernetes 具体使用和实现
Kube-apiserver控制
PersistentVolume(PV)
是 持久存储卷,集群级别资源。PersistentVolumeClaim(PVC)
是持久存储卷声明,namespace级别资源。 是用户对使用存储卷的使用需求声明
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test
namespace: test
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: csi-cephfs-sc
volumeMode: Filesystem
StorageClass
是创建PV
模板信息, 集群级别,用于动态创建pv
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-rbd-sc
parameters:
clusterID: ceph01
imageFeatures: layering
imageFormat: "2"
mounter: rbd
pool: kubernetes
provisioner: rbd.csi.ceph.com
reclaimPolicy: Delete
volumeBindingMode: Immediate
VolumeAttachment
记录了pv的相关挂载信息,如挂载到哪个node节点,由哪个volume plugin来挂载等。AD Controller
创建一个VolumeAttachment
,而External-attacher
则通过观察该VolumeAttachment
,根据其状态属性来进行存储的挂载和卸载操作。
apiVersion: storage.k8s.io/v1
kind: VolumeAttachment
metadata:
name: csi-123456
spec:
attacher: cephfs.csi.ceph.com
nodeName: 172.1.1.10
source:
persistentVolumeName: pvc-123456
status:
attached: true
CSINode
是记录csi plugin
的相关信息(如nodeId
、driverName
、拓扑信息
等). 当Node Driver Registrar
向kubelet
注册一个csi plugin
后,会创建(或更新)一个CSINode
对象,记录csi plugin
的相关信息。
apiVersion: storage.k8s.io/v1
kind: CSINode
metadata:
name: 172.1.1.10
spec:
drivers:
- name: cephfs.csi.ceph.com
nodeID: 172.1.1.10
topologyKeys: null
- name: rbd.csi.ceph.com
nodeID: 172.1.1.10
topologyKeys: null
CSI Volume Plugin
扩展各种存储类型的卷的管理能力,实现第三方存储的各种操作能力与k8s存储系统的结合。调用第三方存储的接口或命令,从而提供数据卷的创建/删除、attach/detach、mount/umount的具体操作实现,可以认为是第三方存储的代理人。前面分析组件中的对于数据卷的创建/删除、attach/detach、mount/umount操作,全是调用volume plugin来完成。
csi plugin
: csi plugin分为ControllerServer与NodeServer,各负责不同的存储操作。external plugin
: 负责watch pvc、volumeAttachment等对象,然后调用volume plugin来完成存储的相关操作Node-Driver-Registrar
: 负责实现csi plugin(NodeServer)的注册,让kubelet感知csi plugin的存在
kube-controller-manager
PV controller
: 负责pv
、pvc
的绑定与生命周期管理(如创建/删除底层存储,创建/删除pv
对象,pv
与pvc
对象的状态变更)。AD controller
: 负责创建、删除VolumeAttachment
对象,并调用volume plugin
来做存储设备的Attach/Detach
操作(将数据卷挂载到特定node
节点上/从特定node
节点上解除挂载),以及更新node.Status.VolumesAttached
等。
注意 AD controller
的Attach/Detach
操作只是修改VolumeAttachment
对象的状态,而不会真正的将数据卷挂载到节点/从节点上解除挂载,真正的节点存储挂载/解除挂载操作由kubelet
中volume manager
调用csi plugin
来完成。
kubelet
管理卷的Attach/Detach
(与AD controller
作用相同,通过kubelet
启动参数控制哪个组件来做该操作)、mount/umount
等操作。
对于csi
来说,volume manager
的Attach/Detach
操作只创建/删除VolumeAttachment
对象,而不会真正的将数据卷挂载到节点/从节点上解除挂载;csi-attacer
组件也不会做挂载/解除挂载操作,只是更新VolumeAttachment
对象,真正的节点存储挂载/解除挂载操作由kubelet
中volume manager
调用调用csi plugin
来完成。
三、磁盘限速 通用方式
本身Volume
限制分为两类:
本地磁盘/类本地磁盘
: 类似于lvm
,local disk
,NAS云盘
,底层通过底层Linux
的xfs
、ext4
、Btrfs
底层文件系统接口,进行通信,实现操作存储,从而提供容器存储服务。远程目录
: 类似与S3
、NFS
等远程目录mount
到pod
; 底层是通过kubernetes
通过grpc
接口与存储卷插件系统
进行通信,来操作存储,从而提供容器存储服务。
因此当前业绩通用作为,只能对本地磁盘/类本地磁盘
, 通过blkio
的cgroup subsystem
进行限速
.
方式一:Runtime
运行时层面限制
dongjiangdeMacBook-Pro:~ $ docker help run | grep -E 'bps|IO'
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
--blkio-weight uint16 Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)
--blkio-weight-device list Block IO weight (relative device weight) (default [])
--device-read-bps list Limit read rate (bytes per second) from a device (default [])
--device-read-iops list Limit read rate (IO per second) from a device (default [])
--device-write-bps list Limit write rate (bytes per second) to a device (default [])
--device-write-iops list Limit write rate (IO per second) to a device (default [])
通过kubelet
内置dockershim
, 解析 pod
的annotaion
,将 配置设置到blkio
配置中
# Pod
apiVersion: v1
kind: Pod
metadata:
name: xxxx
annotations:
io.kubernetes.container.blkio: '{"weight":200,"weight_device":[{"device":"rootfs","value":"200k"}],"device_read_bps":[{"device":"/dev/sda1","value":"20m"}],"device_write_bps":[{"device":"rootfs","value":"20m"}],"device_read_iops":[{"device":"rootfs","value":"200"}]"device_write_iops":[{"device":"rootfs","value":"300"}]}'
...
优势:
a. 不仅仅支持容器卷volume,对pod本身 rootfs等都可以进行设置;
缺点:
a. Container Runtime
必须是docker
;
b. 不能根据创建pvc生命周期,自动进行配置;
c. 仅支持对于direct
本地磁盘生效;
d. 对于一写多读
的 多个pod
共享的本地卷,设置是后者覆盖前者
代码解析:
// +build linux
// 只是对liunx系统生效
func UpdateBlkio(containerId string, docker libdocker.Interface) (err error) {
info, err := docker.InspectContainer(containerId) //获得docker inspect info
blkio := Blkio{}
err = json.Unmarshal([]byte(blkiolable), &blkio) //添加blkio设置
if err != nil {
return fmt.Errorf("failed to unmarshal blkio config,%s, sandboxID:%s, containerId:%s", err.Error(), sandboxID, containerId)
}
//...
blkioResource, err := getBlkioResource(&blkio, containerRoot) //获得 blkioResource object
if err != nil {
return fmt.Errorf("getBlkioResource failed. sandboxID:%s, containerId:%s, %v", sandboxID, containerId, err.Error())
}
cg := &configs.Cgroup{
Path: cpath,
Resources: &blkioResource,
}
err = blkioSubsystem.Set(cpath, cg) // blkio设置到cgroup
if err != nil {
return fmt.Errorf("blkioSubsystem.Set failed. sandboxID:%s, containerId:%s, %v", sandboxID, containerId, err.Error())
}
glog.V(4).Infof("set Blkio cgroup success. sandboxID:%s, containerId:%s, cgroup path:%v, cgroup:%+v", sandboxID, containerId, cpath, cgroupToString(cg))
return nil
}
**总结:**目前这情况对本地卷各种缺点局限性
,对kubelet
源码级别侵害比较大;
方式二:Pod 卷中 blkio 添加与设置
在kubelet
开启feature. 比如: PVCQos=true
, 可以通过如下注解添加 pod qos。
# Pod
apiVersion: v1
kind: Pod
metadata:
name: xxxx
annotations:
qos.volume.storage.cloud.cmss.com: >-
{"pvc": "snap-03", "iops": {"read": 2000, "write": 1000}, "bps":
{"read": 1000000, "write": 1000000}}
...
再通过kubelet
就可以得到pvc的挂载点和设备id。然后我们使用 cgroup
来限制 pod
的 iops
和 bps
。
我们可以只编辑 pod /sys/fs/cgroup/blkio/kubepods/pod/<Container_ID>/...
下的 cgroup
限制文件,
例如,限制 pod 的读取 iops:
echo "<block_device_maj:min> <value>" > /sys/fs/cgroup/blkio/kubepods/pod<UID>/blkio.throttle.read_iops_device
优势:
a. 用户可以使用任何容器运行时
b. k8s的方式,用户必须知道 pod 的 pv。
缺点:
a. pod
使用的rootfs
的限制不支持;(当您节点中的一个 pod 大量使用 rootfs 时,可能会影响同一节点上的其他 pod。)
b. 只能对本地磁盘生效;
c. 对于一写多读
的 多个pod
共享的本地卷,设置是后者覆盖前者;
此方法明显好一些,但是blkio不能对外挂目录生效;并且对于共享的Volume
(N个Pod中的不同container共享一个卷),配置在pod层面不合适
方案三: 基于CSI PVC和StorageClass进行 本地卷设置
集合以上两种方式,限制支持范围:
a. 仅支持本地磁盘/类本地磁盘
的iops;
b. 通过csi插件方式实现,不更改kubelet;
c. 配置在PVC
和StorageClass
, 以整个pv的生命周期做限制,不限制在pod
和container
层面;
d. 仅对pvc生效,不对pod本身rootfs做限流(rootfs 本身就是静态数据,不建议做存储和数据平凡读写);
在这个前提下实现io的blkio读写限制。
先通过storageclass.yaml
设置pv模版
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-local
provisioner: local.csi.cmsss.com
parameters:
volumeType: LVM
vgName: volumegp
fsType: ext4
lvmType: "striping"
readBPS: 1M # read 此类卷的带宽是1MB/s
writeBPS: 100K # 写 此类卷的带宽是100KB/s
readIOPS: 2000 # read 此类卷的tps是2000
writeIOPS: 1000 # write 此类卷的tps是1000
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
也可以支持pmem
和QuotaPath
的volumeType
再通过persistentVolumeClaim.yaml
申请具体pv:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lvm-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
storageClassName: csi-local
最后通过 deployment.yaml
挂载符合的pv :
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-lvm
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15.4
volumeMounts:
- name: lvm-pvc
mountPath: "/data"
volumes:
- name: lvm-pvc
persistentVolumeClaim:
claimName: lvm-pvc
优势在于: 对于pv的模版 添加 iops
和bps
配置, 可以动态生效pvc整个生命周期
。
四、总结
-
- 以支持任何容器运行时;
-
- 支持本地不同pv的限速,保证整个pv生命周期的限制不被更改;
-
- 在社区原生应用不支持情况下,可以不破坏
kubernetes
生态实现;
- 在社区原生应用不支持情况下,可以不破坏
-
- 对于
一写多读
的 多个pod
共享的本地卷,pv速率限制整个生命周期有效;
- 对于
特别说明下:
使用内置(in-tree
) 卷 , 不支持 限流
. 这一类的卷比如: empty
、 hostpath
等
最核心原因是: blkio
只能对 direct
读写请求生效
五、具体实现方式
对 csi
的 nodeserver
创建 pvc
过程中, 使用具体的storageclass
, 实现本地卷 VolumeIOLimit
func SetVolumeIOLimit(devicePath string, req *csi.NodePublishVolumeRequest) error {
readIOPS := req.VolumeContext["readIOPS"]
writeIOPS := req.VolumeContext["writeIOPS"]
readBPS := req.VolumeContext["readBPS"]
writeBPS := req.VolumeContext["writeBPS"]
// IOlimit 值解析
readBPSInt, err := getBpsLimt(readBPS)
if err != nil {
log.Errorf("Volume(%s) Input Read BPS Limit format error: %s", req.VolumeId, err.Error())
return err
}
writeBPSInt, err := getBpsLimt(writeBPS)
if err != nil {
log.Errorf("Volume(%s) Input Write BPS Limit format error: %s", req.VolumeId, err.Error())
return err
}
readIOPSInt := 0
if readIOPS != "" {
readIOPSInt, err = strconv.Atoi(readIOPS)
if err != nil {
log.Errorf("Volume(%s) Input Read IOPS Limit format error: %s", req.VolumeId, err.Error())
return err
}
}
writeIOPSInt := 0
if writeIOPS != "" {
writeIOPSInt, err = strconv.Atoi(writeIOPS)
if err != nil {
log.Errorf("Volume(%s) Input Write IOPS Limit format error: %s", req.VolumeId, err.Error())
return err
}
}
// 获得 Device major/minor 值
majMinNum := getMajMinDevice(devicePath)
if majMinNum == "" {
log.Errorf("Volume(%s) Cannot get major/minor device number: %s", req.VolumeId, devicePath)
return errors.New("Volume Cannot get major/minor device number: " + devicePath + req.VolumeId)
}
// 获得pod uid
podUID := req.VolumeContext["csi.storage.k8s.io/pod.uid"]
if podUID == "" {
log.Errorf("Volume(%s) Cannot get poduid and cannot set volume limit", req.VolumeId)
return errors.New("Cannot get poduid and cannot set volume limit: " + req.VolumeId)
}
// 写具体的pod blkio文件
podUID = strings.ReplaceAll(podUID, "-", "_")
podBlkIOPath := filepath.Join("/sys/fs/cgroup/blkio/kubepods.slice/kubepods-besteffort.slice", "kubepods-besteffort-pod"+podUID+".slice")
if !IsHostFileExist(podBlkIOPath) {
podBlkIOPath = filepath.Join("/sys/fs/cgroup/blkio/kubepods.slice/kubepods-burstable.slice", "kubepods-besteffort-pod"+podUID+".slice")
}
if !IsHostFileExist(podBlkIOPath) {
log.Errorf("Volume(%s), Cannot get pod blkio/cgroup path: %s", req.VolumeId, podBlkIOPath)
return errors.New("Cannot get pod blkio/cgroup path: " + podBlkIOPath)
}
// 设置具体pod blkio文件值
if readIOPSInt != 0 {
err := writeIoLimit(majMinNum, podBlkIOPath, "blkio.throttle.read_iops_device", readIOPSInt)
if err != nil {
return err
}
}
if writeIOPSInt != 0 {
err := writeIoLimit(majMinNum, podBlkIOPath, "blkio.throttle.write_iops_device", writeIOPSInt)
if err != nil {
return err
}
}
if readBPSInt != 0 {
err := writeIoLimit(majMinNum, podBlkIOPath, "blkio.throttle.read_bps_device", readBPSInt)
if err != nil {
return err
}
}
if writeBPSInt != 0 {
err := writeIoLimit(majMinNum, podBlkIOPath, "blkio.throttle.write_bps_device", writeBPSInt)
if err != nil {
return err
}
}
log.Infof("Seccessful Set Volume(%s) IO Limit: readIOPS(%d), writeIOPS(%d), readBPS(%d), writeBPS(%d)", req.VolumeId, readIOPSInt, writeIOPSInt, readBPSInt, writeBPSInt)
return nil
}
自测验证
配置要求
具有所需 RBAC 权限的服务帐户
功能状态
编译打包
local.csi.ecloud.cmss.com
可以编译成容器的形式。
构建容器:
$ docker build -f hack/local/Dockerfile .
用法
先决条件
使用localdisk 或者 挂载clouddisk方式,挂载或生成 lvm pvcreate
或 lvm vgcreate
$ fdisk -l
Disk /dev/sdc: 68.7 GB, 68719476736 bytes, 134217728 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
$ fdisk /dev/sdc
Command (m for help): p
Disk /dev/sdc: 68.7 GB, 68719476736 bytes, 134217728 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x72a4d30c
Device Boot Start End Blocks Id System
Command (m for help): n
Partition type:
p primary (0 primary, 0 extended, 4 free)
e extended
Select (default p): p
Partition number (1-4, default 1): 1
First sector (2048-134217727, default 2048):
Using default value 2048
Last sector, +sectors or +size{K,M,G} (2048-134217727, default 134217727): 64217727
Partition 1 of type Linux and of size 30.6 GiB is set
Command (m for help): t
Selected partition 1
Hex code (type L to list all codes): 8e
Changed type of partition 'Linux' to 'Linux LVM'
Command (m for help): w
The partition table has been altered!
$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sdb 8:16 0 100G 0 disk
|-sdb4 8:20 0 25G 0 part
`-sdb2 8:18 0 75G 0 part
sr0 11:0 1 506K 0 rom
sdc 8:32 0 64G 0 disk
`-sdc1 8:33 0 30.6G 0 part
sda 8:0 0 100G 0 disk
`-sda1 8:1 0 100G 0 part /
$ pvcreate /dev/sdc1
Physical volume "/dev/sdc1" successfully created.
$ vgcreate volumegroup1 /dev/sdc1
Volume group "volumegroup1" successfully created
$ vgdisplay
--- Volume group ---
VG Name volumegroup1
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 2
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 1
Open LV 1
Max PV 0
Cur PV 1
Act PV 1
VG Size <30.62 GiB
PE Size 4.00 MiB
Total PE 7838
Alloc PE / Size 512 / 2.00 GiB
Free PE / Size 7326 / <28.62 GiB
VG UUID V6TVTh-AcIi-hLmR-bozc-9QeA-EBnU-Mhhd6y
执行步骤
第 1 步:创建 CSI Provisioner
$ kubectl create -f ./deploy/local/provisioner.yaml
第 2 步:创建 CSI
插件
$ kubectl create -f ./deploy/local/plugin.yaml
第 3 步:创建存储类
$ kubectl create -f ./examples/storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-lvm
provisioner: local.csi.ecloud.cmss.com
parameters:
vgName: volumegroup1
fsType: ext4
pvType: localdisk
nodeAffinity: "false"
readIOPS: "2000"
writeIOPS: "1000"
readBPS: "10000"
writeBPS: "5000"
reclaimPolicy: Delete
用法:
-
vgName:定义存储类的卷组名;
-
fsType:默认为ext4,定义lvm文件系统类型,支持ext4、ext3、xfs;
-
pvType:可选,默认为云盘。定义使用的物理磁盘类型,支持clouddisk、localdisk;
-
nodeAffinity:可选,默认为 true。决定是否在 PV 中添加 nodeAffinity。 —-> true:默认,使用 nodeAffinity 配置创建 PV; —-> false:不配置nodeAffinity创建PV,pod可以调度到任意节点
-
volumeBindingMode:支持 Immediate/WaitForFirstConsumer —-> Immediate:表示将在创建 pvc 时配置卷,在此配置中 nodeAffinity 将可用; —-> WaitForFirstConsumer:表示在相关的pod创建之前不会创建volume;在配置中,nodeAffinity 将不可用;
第 4 步:使用 lvm
创建 nginx
部署
$ kubectl create -f ./examples/pvc.yaml
$ kubectl create -f ./examples/deploy.yaml
第 5 步:检查 PVC/PV 的状态
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
lvm-pvc Bound lvm-29def33c-8dae-482f-8d64-c45e741facd9 2Gi RWO csi-lvm 3h37m
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
lvm-29def33c-8dae-482f-8d64-c45e741facd9 2Gi RWO Delete Bound default/lvm-pvc csi-lvm 3h38m
第 6 步:检查 pod 的状态
- 检查 pod 中的目录
$ kubectl get pod | grep deployment-lvm
deployment-lvm-57bc9bcd64-j7r9x 1/1 Running 0 77s
$ kubectl exec -ti deployment-lvm-57bc9bcd64-j7r9x sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
# df -h | grep data
/dev/mapper/volumegroup1-lvm--9e30e658--5f85--4ec6--ada2--c4ff308b506e 2.0G 6.0M 1.8G 1% /data
- 检查主机中的目录:
$ kubectl describe pod deployment-lvm-57bc9bcd64-j7r9x | grep Node:
Node: kcs-cpu-test-m-8mzmj/172.16.0.67
$ ifconfig | grep 172.16.0.67
inet 172.16.0.67 netmask 255.255.0.0 broadcast 172.16.255.255
$ mount | grep volumegroup
/dev/mapper/volumegroup1-lvm--9e30e658--5f85--4ec6--ada2--c4ff308b506e on /var/lib/kubelet/pods/c06d5521-3d9c-4517-bdc2-e6df34b9e8f1/volumes/kubernetes.io~csi/lvm-9e30e658-5f85-4ec6-ada2-c4ff308b506e/mount type ext4 (rw,relatime,data=ordered)
/dev/mapper/volumegroup1-lvm--9e30e658--5f85--4ec6--ada2--c4ff308b506e on /var/lib/paascontainer/kubelet/pods/c06d5521-3d9c-4517-bdc2-e6df34b9e8f1/volumes/kubernetes.io~csi/lvm-9e30e658-5f85-4ec6-ada2-c4ff308b506e/mount type ext4 (rw,relatime,data=ordered)
- 检查pod disk iops和bps设置,是否生效:
$ pwd
/sys/fs/cgroup/blkio/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-podc06d5521_3d9c_4517_bdc2_e6df34b9e8f1.slice
$ cat blkio.throttle.read_bps_device
253:1 10000
$ cat blkio.throttle.write_bps_device
253:1 5000
$ cat blkio.throttle.write_iops_device
253:1 1000
$ cat blkio.throttle.read_iops_device
253:1 2000
六、未来
期待下,k8s
对 cgroup v2
, 核心支持了 读对于每一个pod
中的container
限速 和 对于远端目录
的 GRPC
请求限速
目前1.25
版本中对cgroup v2
已经达到beta状态,期待它release状态 😄