技术方案之 对 Kubernetes Pod进本地磁盘(local,disk,LVM) 进行流量控制

对 Kubernetes Pod进本地磁盘(local,disk,LVM) 进行流量控制

Posted by 董江 on Sunday, October 9, 2022

对 Kubernetes Pod进本地磁盘(local,disk,LVM) 进行流量控制

一. 继承上一章节

Kubernetes Pod进程网络带宽 流量控制

混合云场景业务Pod直接相互干扰在离线混部(在离线服务同时在一台机器上服务用户) 等场景下,除了对cpumemfdinodepid等进行隔离,还需要对 网络带宽bandwidth磁盘读写速度IPOSNBD IOL3 Cache内存带宽MBA 等都需要做到隔离和限制

因此,本章节介绍下 磁盘读写速度IPOS 的使用和实现

二. Kubernetes 具体使用和实现

csi plugin

Kube-apiserver控制

  1. PersistentVolume(PV) 是 持久存储卷,集群级别资源。
  2. 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
  1. 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
  1. 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
  1. CSINode 是记录csi plugin的相关信息(如nodeIddriverName拓扑信息等). 当Node Driver Registrarkubelet注册一个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来完成。

  1. csi plugin : csi plugin分为ControllerServer与NodeServer,各负责不同的存储操作。
  2. external plugin : 负责watch pvc、volumeAttachment等对象,然后调用volume plugin来完成存储的相关操作
  3. Node-Driver-Registrar : 负责实现csi plugin(NodeServer)的注册,让kubelet感知csi plugin的存在

kube-controller-manager

  1. PV controller : 负责pvpvc的绑定与生命周期管理(如创建/删除底层存储,创建/删除pv对象,pvpvc对象的状态变更)。
  2. AD controller : 负责创建、删除VolumeAttachment对象,并调用volume plugin来做存储设备的Attach/Detach操作(将数据卷挂载到特定node节点上/从特定node节点上解除挂载),以及更新node.Status.VolumesAttached等。

注意 AD controllerAttach/Detach操作只是修改VolumeAttachment对象的状态,而不会真正的将数据卷挂载到节点/从节点上解除挂载,真正的节点存储挂载/解除挂载操作由kubeletvolume manager调用csi plugin来完成。

kubelet

管理卷的Attach/Detach(与AD controller作用相同,通过kubelet启动参数控制哪个组件来做该操作)、mount/umount等操作。

对于csi来说,volume managerAttach/Detach操作只创建/删除VolumeAttachment对象,而不会真正的将数据卷挂载到节点/从节点上解除挂载;csi-attacer组件也不会做挂载/解除挂载操作,只是更新VolumeAttachment对象,真正的节点存储挂载/解除挂载操作由kubeletvolume manager调用调用csi plugin来完成。

三、磁盘限速 通用方式

本身Volume 限制分为两类:

本地磁盘/类本地磁盘: 类似于lvm,local disk, NAS云盘,底层通过底层Linuxxfsext4Btrfs底层文件系统接口,进行通信,实现操作存储,从而提供容器存储服务。 远程目录: 类似与S3NFS等远程目录 mountpod; 底层是通过kubernetes通过grpc接口与存储卷插件系统进行通信,来操作存储,从而提供容器存储服务。

因此当前业绩通用作为,只能对本地磁盘/类本地磁盘, 通过blkiocgroup 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, 解析 podannotaion,将 配置设置到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 来限制 podiopsbps。 我们可以只编辑 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. 配置在PVCStorageClass, 以整个pv的生命周期做限制,不限制在podcontainer层面; 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

也可以支持pmemQuotaPath的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的模版 添加 iopsbps配置, 可以动态生效pvc整个生命周期

四、总结

    1. 以支持任何容器运行时;
    1. 支持本地不同pv的限速,保证整个pv生命周期的限制不被更改;
    1. 在社区原生应用不支持情况下,可以不破坏kubernetes生态实现;
    1. 对于一写多读的 多个pod共享的本地卷,pv速率限制整个生命周期有效;

特别说明下: 使用内置(in-tree) 卷 , 不支持 限流. 这一类的卷比如: emptyhostpath 等 最核心原因是: blkio 只能对 direct 读写请求生效

五、具体实现方式

csinodeserver 创建 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 pvcreatelvm 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

用法:

  1. vgName:定义存储类的卷组名;

  2. fsType:默认为ext4,定义lvm文件系统类型,支持ext4、ext3、xfs;

  3. pvType:可选,默认为云盘。定义使用的物理磁盘类型,支持clouddisk、localdisk;

  4. nodeAffinity:可选,默认为 true。决定是否在 PV 中添加 nodeAffinity。 —-> true:默认,使用 nodeAffinity 配置创建 PV; —-> false:不配置nodeAffinity创建PV,pod可以调度到任意节点

  5. 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 的状态

  1. 检查 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
  1. 检查主机中的目录:
$ 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)
  1. 检查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

六、未来

期待下,k8scgroup v2, 核心支持了 读对于每一个pod中的container限速 和 对于远端目录GRPC 请求限速

目前1.25版本中对cgroup v2 已经达到beta状态,期待它release状态 😄

七、下一章节

node 状态拓扑优先级调度

「如果这篇文章对你有用,请随意打赏」

Kubeservice博客

如果这篇文章对你有用,请随意打赏

使用微信扫描二维码完成支付