K8S 中虚拟机的资源管理与Pod调度

作者: 王炳明 分类: Kubernetes 发布时间: 2021-05-26 18:22 热度:102

1. requests 和 limits 区别与使用

资源分两种:

  • 不可压缩资源:如内存、磁盘
  • 可压缩资源:CPU

每一项资源的申请,在 yaml 模板中都会有两个字段:

  • requests:定义了对应容器需要的最小资源量,是 kube-scheduler 在调度时计算的依据,kubernetes 依靠它找到合适的节点来运行 Pod。
  • limits:定义了对应容器最大可以消耗的资源上限,这是 kubelet 在创建 cgroup 的数据。它告诉了 Linux 内核什么时候你的进程可以为了清理空间而被杀死

对于 CPU 来说

  • requests:其值会反应在 cpu.share 文件里,假设 requests 值为 500m,则 cpu.share 计算方法:(500/1000)*1024
  • limits:其值会反应在 cpu.cfs_quota_us 和 cpu.cfs_period_us 文件里,若 pod 的 cpu 使用超过了 limit ,性能会开始受限,而如果内存使用超过了 limit。假设 limits 值为 500m,则 (500/1000)*100ms = 50 ms = 500000,而 cpu.cfs_period_us 则固定为 100 ms = 1000000

对于内存来说

  • rqeuests:由于内存是不可压缩资源,因为 requests 只用于调度使用,不会表现在 cgroup 的文件里。
  • limits:其值会反应在 memory.limit_in_bytes,若 pod 内存使用超过了 limit,容器就会被 OOM。假设 limits 值为 256Mi,则 memory.limit_in_bytes 的值为 256*1024*1024=268435456

limits 和 requests 的设置要合理,如果不设置它们,或者设置得非常低,那么可能会有不好的影响。

假设你没有配置内存requests来运行Pod,而配置了一个较高的limits。正如我们所知道的Kubernetes默认会把requests的值指向limits,如果没有合适的资源的节点的话,Pod可能会调度失败,即使它实际需要的资源并没有那么多。

另一方面,如果你运行了一个配置了较低requests值的Pod,你其实是在鼓励内核oom-kill掉它。为什么?假设你的Pod通常使用100MiB内存,你却只为它配置了50MiB内存requests。如果你有一个拥有75MiB内存空间的节点,那么这个Pod会被调度到这个节点。当Pod内存消耗扩大到100MiB时,会让这个节点压力变大,这个时候内核可能会选择杀掉你的进程。

所以我们要正确配置Pod的内存requests和limits。

延伸阅读

  1. 为命名空间配置默认的内存请求和限制
  2. 为命名空间配置默认的 CPU 请求和限制
  3. 配置命名空间的最小和最大内存约束
  4. 为命名空间配置 CPU 最小和最大约束
  5. 为命名空间配置内存和 CPU 配额
  6. 配置命名空间下 Pod 配额

2. KubeVirt VM 的资源如何申请?

下面是 KubeVirt 创建虚拟机时的配置文件,我截取了与资源(cpu、内存)相关的内容

spec:
  domain:
    memory:
      guest: 
      hugepages:
    cpu:
      threads: 1
      cores: 1
      sockets: 1
      dedicatedCpuPlacement:
      features:
      isolateEmulatorThread:
      model:
    resources:
      overcommitGuestOverhead: true
      limits:
        memory: 
        cpu: 
      requests:
        memory:
        cpu:

可以将其分成四个部分

  1. spec.domain.memory:指定内存特性,guest 用于设置在虚拟机内可见的内存大小(其值必须介于requests和limits之间,),hugepages 用于设置大页。
  2. spec.domain.cpu:CPU 拓扑相关的设置
  3. spec.domain.resources.requests:用于调度使用的资源数据
  4. spec.domain.resources.limits:限制Pod最大可使用的资源

上面这些都是可选项,但内存和cpu必须指定一个,比如

  • spec.domain.memoryspec.domain.resources.[requests/limits].memory 必须指定一个
  • spec.domain.cpuspec.domain.resources.[requests/limits].cpu 必须指定一个

如果不指定,KubeVirt 会为其设置默认值。

同时还有一些规则,需要注意

  • 如果没有指定 spec.domain.resources.[requests/limits].cpu,那么创建出来的 Pod 的 requests.cpu的值默认为 100m (也即 0.1 cpu)
  • 如果指定了 spec.domain.resources.limits.[cpu/memory] 却没有指定 spec.domain.resources.requests.[cpu/memory],那 么 requests.cpu 默认会取 limits.cpu 去调度

  • 如果没有指定 spec.domain.resources.[requests/limits].memory,那么 创建出来的 Pod 的 requests.memory 的值会取 spec.domain.memory 的值

3. QoS 的分类及使用场景(驱逐)

根据 limits 和 requests 的大小关系,QoS 可以分为三类:

  • Guaranteed(资源可保证的):Pod 里所有的容器的 requests 和 limits 都相等的情况(若只指定 limits 而没有指定 requests,则他们默认是相等,也属这个类型)
  • Burstable (可超频的):Pod 里只要有一个容器的 requests 小于 limits 的情况
  • BestEffort(不受限制的):Pod 里的容器都没有设置 requests 和 limits 的情况

QoS 有什么用呢?

主要是 worker 节点上的 kubelet 在对 Pod 进行 Eviction(即驱逐)时需要用到的。

对于 Eviction 只要掌握两点:

第一点:在什么时候触发

在运行 kubelet 的时候,有两个参数专门用来设置触发 Eviction 的阈值: --eviction-hard--eviction-soft ,分别代表 Hard 和 Soft 两种模式

  • Hard 模式:Hard Eviction 模式下,Eviction 过程会在阈值达到之后立刻开始
  • Soft 模式:Soft Eviction 允许你为 Eviction 过程设置一段“优雅时间”,比如 imagefs.available=2m,就意味着当 imagefs 不足的阈值达到 2 分钟之后,kubelet 才会开始 Eviction 的过程。

    --eviction-hard--eviction-soft 的参数值可以包含如下驱逐条件:

K8S 中虚拟机的资源管理与Pod调度插图

当然了,这两个参数是选填的,如果你没有填写的话,默认值是

memory.available<100Mi
nodefs.available<10%
nodefs.inodesFree<5%
imagefs.available<15%

举例说明,如果一个节点有 10Gi 内存,希望在可用内存下降到 1Gi 以下时引起驱逐操作, 则驱逐阈值可以使用下面任意一种方式指定(但不是两者同时)。

  • memory.available<10%
  • memory.available<1Gi

第二点:触发了会怎样

当 kubelet 检测到资源已经到达了 Eviction 的阈值后,就会将把 worker 节点设置为 MemoryPressure 或者 DiskPressure 状态,从而避免新的 Pod 被调度到这个节点上。

同时,它还会选择一些 Pod 进行删除处理以便对部分资源进行回收。那么删除哪些 Pod 呢?难道有什么标准吗?这时候 QoS 就派上用场了。

  • 优先删除的,自然是 BestEffort 类别的 Pod,因为这些家伙是个无底洞
  • 其次才是 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。
  • 最后,才是 Guaranteed 类别,Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。

当然两个 Pod 处于同一类别,Kubernetes 还会根据 Pod 的优先级来进行进一步地排序和选择。

4. K8S 的节点资源是如何管理的?

通过 kubectl describe node <node_name> 可以查看该节点的详细信息,包括资源的总量、可使用资源的情况及资源使用情况。

如下图所示,Capacity 记录该节点上的资源总量,Allocatable 记录的是该节点上,所有可分配的资源总量(会扣减掉 kubelet 中预留给宿主机和集群服务的资源),经过验证,内存除了会扣掉预留的内存,还会固定扣掉 100M 的内存。

K8S 中虚拟机的资源管理与Pod调度插图(1)

节点资源的使用情况,如下图所示:

  • 右上角,是该 worker 节点上,每个 Pod 的资源分配情况
  • 左下角,是该 worker 节点上,每项资源的分配情况,分别是右上角对应列的总和

K8S 中虚拟机的资源管理与Pod调度插图(2)

右上角仅列出了 cpu 和 memory 的具体分配情况,但对于其他项(磁盘、大页、其他设备等),则没有展示。

5. K8S 下的cgroup如何限制资源的?

以一台虚拟机为例

K8S 中虚拟机的资源管理与Pod调度插图(3)

可见 limits.cpu 及 limits.memory 都为 0,即表示 Pod 在运行过程中,如果宿主机资源充足,那 Pod 的资源使用将不受限制。

那么对应到 cgroup 上,该如何验证这一事实呢?

在 /sys/fs/cgroup 目录下有很多的 cgroup subsystem

$ ls -l  /sys/fs/cgroup 
total 0
drwxr-xr-x 5 root root  0 May 24 08:00 blkio
lrwxrwxrwx 1 root root 11 May 24 08:00 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 May 24 08:00 cpuacct -> cpu,cpuacct
drwxr-xr-x 5 root root  0 May 24 08:00 cpu,cpuacct
drwxr-xr-x 3 root root  0 May 24 08:00 cpuset
drwxr-xr-x 5 root root  0 May 24 08:00 devices
drwxr-xr-x 3 root root  0 May 24 08:00 freezer
drwxr-xr-x 3 root root  0 May 24 08:00 hugetlb
drwxr-xr-x 5 root root  0 May 24 08:00 memory
lrwxrwxrwx 1 root root 16 May 24 08:00 net_cls -> net_cls,net_prio
drwxr-xr-x 3 root root  0 May 24 08:00 net_cls,net_prio
lrwxrwxrwx 1 root root 16 May 24 08:00 net_prio -> net_cls,net_prio
drwxr-xr-x 3 root root  0 May 24 08:00 perf_event
drwxr-xr-x 5 root root  0 May 24 08:00 pids
drwxr-xr-x 5 root root  0 May 24 08:00 systemd

我们本次关心的是 cpu 和 memory

cpu

首先是 cpu ,进入 /sys/fs/cgroup/cpu/kubepods.slice/ 底下有两个目录

$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/ | grep ^d
drwxr-xr-x  8 root root 0 May 24 08:15 kubepods-besteffort.slice
drwxr-xr-x 10 root root 0 May 25 03:12 kubepods-burstable.slice

besteffort 和 burstable 是 QoS 策略,上面已经介绍过了,通过如下命令,查看 pod 对应的策略是 Burstable

$ kubectl get po virt-launcher-vm1-jtkxd -o yaml | grep qosClass
  qosClass: Burstable

因此进入 /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/ 目录,发现有很多 pod 级的目录

$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/ | grep pod
drwxr-xr-x 4 root root 0 May 24 08:18 kubepods-burstable-pod3971431c_1317_49bf_a05f_6943e952e1ff.slice
drwxr-xr-x 4 root root 0 May 25 03:12 kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
drwxr-xr-x 4 root root 0 May 24 08:15 kubepods-burstable-pod756018ec_c8ef_4ebf_80be_a9054d2fc354.slice
drwxr-xr-x 4 root root 0 May 24 08:18 kubepods-burstable-pod9bf7adaa_8451_42d0_aa6d_bc837e586069.slice
drwxr-xr-x 4 root root 0 May 24 08:16 kubepods-burstable-podabbf2ed8_2b12_45de_8575_82d34c9e7a54.slice
drwxr-xr-x 4 root root 0 May 24 08:15 kubepods-burstable-podd05a5359_a3fe_43ae_86c9_3e9c47cb48ce.slice
drwxr-xr-x 4 root root 0 May 24 08:16 kubepods-burstable-podf3507d66_d4ec_42ea_8f22_82b5ce228018.slice
drwxr-xr-x 4 root root 0 May 24 08:16 kubepods-burstable-podf4de8156_7fbb_453e_a95a_b3dee574e9a1.slice

通过如下命令找到我们 Pod uid

$ kubectl get pod virt-launcher-vm1-jtkxd -o jsonpath='{.metadata.uid}'
6bdcbae5-7a5a-4c6e-aa4d-70b80c57569d

因此进入 kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice 目录,又看到了两个容器级的目录

$ ls -l /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice | grep ^d
drwxr-xr-x 2 root root 0 May 25 03:12 docker-17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2.scope
drwxr-xr-x 2 root root 0 May 25 03:12 docker-df09b5b65b5b8e0e64d794222e8696265d3400fd990d2bc62fa0b7fc15b99582.scope

通过如下命令找到容器 id

$ kubectl get pod virt-launcher-vm1-jtkxd -o jsonpath='{.status.containerStatuses[*].containerID}'
docker://17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2

进入 docker-17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2.scope 目录,有如下目录

$ ls -l
-rw-r--r-- 1 root root 0 May 25 03:12 cgroup.clone_children
--w--w--w- 1 root root 0 May 25 03:12 cgroup.event_control
-rw-r--r-- 1 root root 0 May 25 03:12 cgroup.procs
-r--r--r-- 1 root root 0 May 25 03:12 cpuacct.stat
-rw-r--r-- 1 root root 0 May 25 03:12 cpuacct.usage
-r--r--r-- 1 root root 0 May 25 03:12 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 May 25 03:12 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 May 25 03:12 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 May 25 03:12 cpu.rt_period_us
-rw-r--r-- 1 root root 0 May 25 03:12 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 May 25 03:12 cpu.shares
-r--r--r-- 1 root root 0 May 25 03:12 cpu.stat
-rw-r--r-- 1 root root 0 May 25 03:12 notify_on_release
-rw-r--r-- 1 root root 0 May 25 03:12 tasks

其中最主要的是两个文件

  • cpu.cfs_period_us:用来配置时间周期长度, 取值范围为1毫秒(ms)到1秒(s)
  • cpu.cfs_quota_us:用来配置当前cgroup在设置的周期长度内所能使用的CPU时间数。如果cfs_quota_us的值为-1(默认值),表示不受cpu时间的限制。

两个文件配合起来设置CPU的使用上限。比如如下配置,限制了最多能使用 2个 cpu (在 100ms 内能使用 200ms 的cpu)

cpu.cfs_period_us = 100ms
cpu.cfs_quota_us = 200ms

有了这些背景知识,我们来看下该 Pod 下的容器的配置,cpu.cfs_quota_us = -1 表明了 cpu 时间不受限制 ,符合我前面通过 kubectl describe node <node> 出来看到的资源信息。

$ cat cpu.cfs_period_us
100000
$ cat cpu.cfs_quota_us
-1

内存

同理,内存的限制也可以在如下目录中找到 memory.limit_in_bytes 文件,它的内容是一个非常大的值,远超过计算机所拥有的总内存,这也意味着 memory 的使用也是不受限制的。

$ cd /sys/fs/cgroup/memory/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice/docker-17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2.scope/

$ cat memory.limit_in_bytes
9223372036854771712

上面目录的寻找过程,需要多次去查看对应的配置 (QoS) id (pod id 和 container id),过程虽然不复杂但也比较麻烦。

这里有一种更快捷的方法

# 先找出 container id
kubectl get pod virt-launcher-vm1-jtkxd -o jsonpath='{.status.containerStatuses[*].containerID}'
docker://17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2

# 再通过 这个 id 去替换下面 moby 后面的 id,找到 config.json ,再通过 jq 查到 cgroupsPath cat /run/containerd/io.containerd.runtime.v2.task/moby/17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2/config.json | jq ".linux.cgroupsPath"
"kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice:docker:17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2"

# cgroupsPath 根据冒号可以分成两部分
# 前面一部分是目录 kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
# 后面一部分是容器 id  docker:17dcbd945e7615c4d5347e6e40cec98401e4d2ba5b2b5fea02633706b1a295d2
# 最后使用 find 去找这个目录,可以找出所有子系统的目录
$ find /sys/fs -name kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/freezer/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/cpu,cpuacct/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/blkio/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/perf_event/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/memory/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/devices/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/pids/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/hugetlb/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/net_cls,net_prio/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice
/sys/fs/cgroup/systemd/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod6bdcbae5_7a5a_4c6e_aa4d_70b80c57569d.slice

绑核

虚拟机的绑核是通过 cpuset 这一子系统控制管理的,在创建一台有 20个 vcpu 的虚拟机后,通过上面的逻辑进入对应的 cgroup 目录(如下)

/sys/fs/cgroup/cpuset/kubepods.slice/kubepods-podd48928f6_9669_4873_8ebc_e170d8079aa0.slice/docker-b240e949a632455b2dce325ee3c53390c15fbbf5488a1a085ef69534386f19c6.scope

查看 cpuset.cpus 文件的内容,它记录的是该 Pod 绑定的 cpu 范围,一共 20 个 vcpu,符合预期

$ cat cpuset.cpus
1-2,8-15,17-18,24-31

6. 虚拟机是如何限制资源的使用的?

从上面的验证过程,我们可以得出结论, 利用 Harvester 创建出来的虚拟机对应的 Pod 使用的资源是不受限制的。

问题来了,虚拟机进程是由于该 Pod 里的 virt-controller 容器创建出来的,和容器同属于一个 cgroup ,那是不是就意味着虚拟机的资源使用也是不受限制的呢?

并不是。

登陆到 Pod 里,使用 virsh dumpxml 1 可以看到 xml 中的各项资源与我们在 Harvester 上的模板是一致的。

当 Pod 里的 k8s_compute_virt-launcher 容器通过 /usr/bin/virt-launcher 命令调用 kvm-qemu 创建出虚拟机时,会读取 xml 中的内存、cpu 等资源信息,并通过 -m 参数(-m 1024, 指定虚拟机可使用的最大内存)和 -smp 参数(-smp 1,sockets=1,dies=1,cores=1,threads=1, 指定虚拟机最多可使用的 cpu 核心)来限制虚拟机可使用的资源。

/usr/libexec/qemu-kvm -name guest=default_vm1,debug-threads=on -S -object secret,id=masterKey0,format=raw,file=/var/lib/libvirt/qemu/domain-1-default_vm1/master-key.aes -Machine pc-q35-rhel8.3.0,accel=tcg,usb=off,dump-guest-core=off -cpu EPYC,acpi=on,ss=on,monitor=on,hypervisor=on,erms=on,mpx=on,pcommit=on,clwb=on,pku=on,la57=on,3dnowext=on,3dnow=on,npt=on,vme=off,fma=off,avx=off,f16c=off,avx2=off,rdseed=off,sha-ni=off,xsavec=off,fxsr-opt=off,misalignsse=off,3dnowprefetch=off,osvw=off,topoext=off,nrip-save=off -m 1024 -overcommit mem-lock=off -smp 1,sockets=1,dies=1,cores=1,threads=1 -object iothread,id=iothread1 -uuid 133bf63e-9459-5126-9b21-b56e9b3d17b3 -smbios type=1,manufacturer=KubeVirt,product=None,uuid=133bf63e-9459-5126-9b21-b56e9b3d17b3,family=KubeVirt -no-user-config -nodefaults -chardev socket,id=charmonitor,fd=18,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc -no-shutdown -boot strict=on -device pcie-root-port,port=0x10,chassis=1,id=pci.1,bus=pcie.0,multifunction=on,addr=0x2 -device pcie-root-port,port=0x11,chassis=2,id=pci.2,bus=pcie.0,addr=0x2.0x1 -device pcie-root-port,port=0x12,chassis=3,id=pci.3,bus=pcie.0,addr=0x2.0x2 -device pcie-root-port,port=0x13,chassis=4,id=pci.4,bus=pcie.0,addr=0x2.0x3 -device pcie-root-port,port=0x14,chassis=5,id=pci.5,bus=pcie.0,addr=0x2.0x4 -device pcie-root-port,port=0x15,chassis=6,id=pci.6,bus=pcie.0,addr=0x2.0x5 -device pcie-root-port,port=0x16,chassis=7,id=pci.7,bus=pcie.0,addr=0x2.0x6 -device qemu-xhci,id=usb,bus=pci.2,addr=0x0 -device virtio-scsi-pci-non-transitional,id=scsi0,bus=pci.3,addr=0x0 -device virtio-serial-pci-non-transitional,id=virtio-serial0,bus=pci.4,addr=0x0 -blockdev {"driver":"host_device","filename":"/dev/disk-0","aio":"native","node-name":"libvirt-1-storage","cache":{"direct":true,"no-flush":false},"auto-read-only":true,"discard":"unmap"} -blockdev {"node-name":"libvirt-1-format","read-only":false,"discard":"unmap","cache":{"direct":true,"no-flush":false},"driver":"raw","file":"libvirt-1-storage"} -device virtio-blk-pci-non-transitional,bus=pci.5,addr=0x0,drive=libvirt-1-format,id=ua-disk-0,bootindex=1,write-cache=on,werror=stop,rerror=stop -netdev tap,fd=20,id=hostua-default -device virtio-net-pci-non-transitional,host_mtu=1400,netdev=hostua-default,id=ua-default,mac=52:54:00:23:41:0e,bus=pci.1,addr=0x0,romfile= -chardev socket,id=charserial0,fd=21,server,nowait -device isa-serial,chardev=charserial0,id=serial0 -chardev socket,id=charchannel0,fd=22,server,nowait -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=org.qemu.guest_agent.0 -device usb-tablet,id=ua-tablet,bus=usb.0,port=1 -vnc vnc=unix:/var/run/kubevirt-private/e75f07b3-cddc-4e82-92a1-7043950711e5/virt-vnc -device VGA,id=video0,vgamem_mb=16,bus=pcie.0,addr=0x1 -device virtio-balloon-pci-non-transitional,id=balloon0,bus=pci.6,addr=0x0 -sandbox on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny -msg timestamp=on

而这些参数最终会作用到虚拟机里,在虚拟机里使用 free -m 看到的内存就是 1G (有点差异是正常的),使用 lscpu 看到的 cpu 就是 1核

K8S 中虚拟机的资源管理与Pod调度插图(4)

举个不是那么恰当的例子:

  • 宿主机的资源,就像一个小湖泊
  • Pod 就好比,一条船,可以使用整个湖泊的水域
  • 虚拟机就好比,船上一个盒子
  • 虚拟机里的程序就好比,盒子里的一棵树

如果没有虚拟机,经过一定的时间,完全有可能长得非常好,甚至超过湖泊的大小,但由于有了盒子的存在,就限制了其生长,它能使用的空间就只有例子的大小。

7. 一个虚拟机的 Pod 有几个容器?

假如创建一个没有任何数据盘的虚拟机,一个 Pod 会有几个容器呢?

第一种情况

在后台通过 kubectl apply -f vm.yaml 的方式创建的虚拟机,会有两个容器

$ kubectl get po
NAME                          READY   STATUS    RESTARTS   AGE
virt-launcher-wbm-vm1-h29mn   2/2     Running   0          16h

第二种情况

在 Harvester 前端创建,只会有一个容器

$ kubectl get po
NAME                          READY   STATUS    RESTARTS   AGE
virt-launcher-wbm-vm2-f6874       1/1     Running   0          16h

通过观察,在后台使用命令去创建的 Pod 里的两个容器分别是:

  1. virt-controller:负责启动 qemu-kvm 进程,也即虚拟机
  2. volumecontainerdisk:负责挂载磁盘给虚拟机使用

而 volumecontainerdisk 这个容器在 Harvester 创建的虚拟机里并没有,原因是Harvester 创建的虚拟机,它的磁盘使用的是 DataVolume(而使用命令创建的虚拟机使用的是 image 直接挂载),它并不会额外起一个容器去挂载。

8. 调度时 Pod 所需资源的计算

如果一个 Pod 包含多个容器,那该 Pod 所需的资源也应该是这些容器的 requests 之和。

而在虚拟机场景下,如果你定义了一个 YAML 文件,虚拟机的系统盘使用的是 image 文件(而不是共享存储),那么创建出来的 Pod 里会多出一个容器用来挂载磁盘,这个上面已经介绍过,本节的问题是,这些由 KubeVirt 内部自已多创建的容器的 requests 和 limits 资源该设置多少默认值呢?

通过阅读 KubeVirt 的代码得知,虚拟机 Pod 的容器分为两种:virt-controller 和 volumecontainerdisk,而这两种容器的 requests 和 limits 的默认值各不一样。

  • virt-controller 容器,其默认的 requests.cpu 是 100m, limits.cpu 是 0,requests.memory 是 0,limits.memory 是 0

K8S 中虚拟机的资源管理与Pod调度插图(5)

  • volumecontainerdisk 容器,其默认的requests.cpu 是 10m,limits.cpu 是 100m,requests.memory 是 1M,limits.memory 是 40M

K8S 中虚拟机的资源管理与Pod调度插图(6)

综合一下,汇总成表格如下

K8S 中虚拟机的资源管理与Pod调度插图(7)

kubectl describe node <node_name> 相比,基本都能对得上

K8S 中虚拟机的资源管理与Pod调度插图(8)

只有 requests.memory 这个有点差异,这是因为上面表格记录的是你没有指定值的时候的默认值,而实际上 virt-controller 的 requests.memory 会使用 Harvester 上模板中内存的值,再加上各种 Pod 运行所需的各种开销(具体有哪些开销,如何计算,下一节会讲到),合计 243382920 ,也即 232Mi。

9. 调度时 Pod 的各种开销计算

为了方便讲解演示,避免多个容器带来计算的干扰,我使用 Harvester 创建一个不包含 volumecontainerdisk 的虚拟机,其规格是 1Gi 内存,1 vCPU。

使用 kubectl describe node <node_name> 查看资源信息

K8S 中虚拟机的资源管理与Pod调度插图(3)

其中 cpu.request、cpu.limits、memory.limits 上面都已经介绍过了。

这里着重看一下 memory.requests 的大小,与我模板里的 1Gi (1024 Mi)差了 172 Mi。

这 172 Mi 就是 K8S 在调度的时候,默认为你加上的一些 Pod 的额外开销

实际上,在 Kubernetes v1.18 之后,就会默认开启 PodOverhead 这一特性。

如果我创建一个 2vcpu、2Gi 内存的虚拟机,额外开销又变成了 182 Mi 了。可见 Pod 的额外开销与Pod 的规格有关。

那么这 172 Mi 究竟是怎么计算的呢?

通过阅读 KubeVirt 对应代码:kubevirt/pkg/virt-controller/services/template.go:getMemoryOverhead() ,可以知道 Pod 所需的各种开销,包括:

  • 页表开销(pagetable overhead):每 512 bit 需要 1bit 的开销,比如 1G 内存就需要 2Mi 的开销
  • 共享库(shared libraries overhead)开销:固定为 138 Mi
  • CPU 表的开销(CPU table overhead):每个 vcpu 核 8 Mi
  • 固定的 IOThread 开销:8Mi
  • video RAM overhead:16Mi
  • pflash 开销:当Cpu 构架是 arm64时,需要额外 128Mi 开销
  • VFIO 开销:如果有 VFIO 设备,则需要额外 1Gi 的开销

而经过计算,这些开销累计之和,就是我们上面算出来的 172 Mi ,完全吻合:

开销项 开销值
pagetable overhead 1 * 1024 * 1024 * 1024 / 512 = 2Mi
shared libraries overhead 138 Mi
CPU table overhead 1 * 8Mi
IOThread overhead 8 Mi
video RAM overhead 16 Mi
pflash overhead 0 Mi
VFIO overhead 0 Mi
合计 2 + 138 + 8 + 8 + 16 = 172 Mi

10. 虚拟机的进程树分析

创建一个虚拟机后,在 worker 上就会启动一个 containerd-shim-runc-v2 的进程,这个进程是由 containerd 服务创建的,

K8S 中虚拟机的资源管理与Pod调度插图(9)

根据这个主进程的 pid ,借助 pstree (yum install psmisc -y)就会看到进程树

K8S 中虚拟机的资源管理与Pod调度插图(10)

可以看到 virt-launcher 启动了两个进程

  1. virt-launcher 进程:这个进程用于启动 libvirtd 服务,因为该进程必须先于 qemu-kvm 进程启动,该进程运行参数中有 --no-fork true
  2. qemu-kvm 进程:虚拟机进程

11. 宿主机的资源占用是准确的吗?

如果是通过 Harvester 创建的虚拟机,并不准确。

当你通过创建了一个虚拟机, KubeVirt 会根据你的模板信息,计算出 Kind 为 Pod 的 YAML 里 cpu 和 内存信息。

例如使用 1cpu, 1GiB 内存的模板去创建虚拟机,KubeVirt 生成的 YAML 如下:

    spec:
      domain:
        resources:
          requests:
            memory: 1Gi
        cpu:
          cores: 1
          sockets: 1
          threads: 1

而转译成 Pod 的 YAML 如下

    resources:
      limits:
        devices.kubevirt.io/tun: "1"
      requests:
        cpu: 100m
        devices.kubevirt.io/tun: "1"
        ephemeral-storage: 50M
        memory: 1196Mi

可以看到我们要创建的是 1 cpu 的虚拟机,在 Pod 中的有 requests.cpu 固定只有 0.1 vcpu,kube-controller 调度的时候,就是拿着 0.1 vcpu 去检查节点资源 是否满足的。这样就有可能在一台宿主机上创建出远超宿主机所能承载的虚拟机数量。

比如一台宿主机有64cpu,原则上只能创建 2 台32 cpu 的虚拟机,可由于 requests.cpu 永远只有 0.1 vcpu,因此理论上可以创建出640台宿主机(不考虑内存占用的情况)。

相比 cpu,内存资源的调度就比较合理了,在调度的时候,除了会算上虚拟机申请的内存之外 ,还会加上 Pod 的各项额外开销,因此内存不会出现过载的情况。

12. 验证虚拟机资源是否受限?

12.1 验证内存

使用 free 查看可用内存为 389 M,接着使用 fallocate 创建一个 400M 大文件。$ nohup stress –vm 1 –vm-bytes 15000M –vm-keep &

最后利用 Python 去读取这个文件,由于读取全部文件的大小需要至少 400M 内存,所以会报内存错误,或者被 OOM Kill 了。

K8S 中虚拟机的资源管理与Pod调度插图(11)

12.2 压测 cpu

在 1 vcpu 的虚拟机里面先下载安装 stress,然后用 stress 命令指定一个 cpu 直接压

$ wget http://resource.wcs.8686c.com/k8s/stress-1.0.4-16.el7.x86_64.rpm && rpm -ivh stress-1.0.4-16.el7.x86_64.rpm
$ stress -c 1

在虚拟机所在的宿主机上使用 top -p <qemu-kvm-pid> 可以看到 cpu 最多也就 109%,修改 stress 参数改用两个 cpu ,再次 top 查看,最多也只能使用 109% 的cpu

K8S 中虚拟机的资源管理与Pod调度插图(12)

13. K8S 调度的大致过程

调度分为两个步骤:

  1. Predicates:过滤器,从所有的 Node 中,挑选出所有满足 Pod 要求的 Node
  2. Priorities:称重器,从所有满足要求的 Node 中,挑选出最优的 Node

13.1 Predicates:断言

Predicates 中有众多的过滤器,可将过滤器分成四种类型

第一种类型: GeneralPredicates

相关的过滤器有:

  • PodFitsResources:检查宿主机的 CPU 和内存资源等是否够用。
  • PodFitsHost:检查宿主机的名字是否跟 Pod 的 spec.nodeName 一致
  • PodFitsHostPorts:检查Pod 申请的宿主机端口(spec.nodePort)是不是跟已经被使用的端口有冲突。
  • PodMatchNodeSelector:检查Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配

经过调度的两个阶段(Predicates 和 Priorities)选出了一台最合适的节点之后,该节点的 kubelet 在收到 Pod 创建请求后,会再次执行一个 Admit 操作来进行二次确认。这里二次确认的规则,就是执行一遍 GeneralPredicates ,跑一遍上述的所有的过滤器。

第二种类型:与 Volume 相关的过滤规则

相关的过滤器有:

  • NoDiskConflict:检查多个 Pod 声明挂载的持久化 Volume 是否有冲突
  • MaxPDVolumeCountPredicate:检查一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目
  • VolumeZonePredicate:检查持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配。
  • VolumeBindingPredicate:检查该 Pod 对应的 PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配。

第三种类型:与宿主机相关的过滤规则。

相关的过滤器有:

  • PodToleratesNodeTaints:检查 Pod 是否允许调度到有污点的节点上
  • NodeMemoryPressurePredicate:检查的是当前节点的内存是不是已经不够充足

第四种类型:与Pod 相关的过滤规则

相关的过滤器有:

  • PodAffinityPredicate:检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系

当然还有其他过滤器,这里只列出了比较典型的过滤器。

13.2 Priorities:优先级

在 Predicates 阶段完成了节点的“过滤”之后,Priorities 阶段的工作就是为这些节点打分。这里打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。

相关的称重器有:

  • LeastRequestedPriority:空闲资源(CPU 和 Memory)越多的节点,分数越高
  • BalancedResourceAllocation:请求资源与节点空闲资源越接近的节点,分数越高
  • ImageLocalityPriority:如果待调度 Pod 需要使用的镜像很大,并且已经存在于某些 Node 上,那么这些 Node 的得分就会比较高。
  • NodeAffinityPriority

  • TaintTolerationPriority
  • InterPodAffinityPriority

以上稳重器得到的分数,累计分数最高的那个节点就是最佳的节点

如果你想要自定义称重器的一些配置,可以创建一个 ConfigMap 对象来配置哪些规则需要开启、哪些规则需要关闭。并且,你可以通过为 Priorities 设置权重,来控制调度器的调度行为。

13.3 调度的总结

关于调度有两点需要注意:

  1. 每个 Node 都会并发走一遍上面四种类型的所有过滤器(每一个 Node 一个 Goroutine)
  2. 四种类型的过滤器的执行顺序,是有讲究的,比如不会把亲和和反亲和的过滤器,放在CPU、内存资源检查的过滤器之前,因为没意义
  3. 多个 Pod 的调度过程是串行的,必须得等一个 Pod 调度完成后,才会调度另一个 Pod

14. K8S 中 Pod 的优先级与抢占规则

Pod 的优先级主要体现在这两个地方:

  1. 当有多个 Pod 在队列中等待调度时,高优先级的 Pod 会优先被选出调度
  2. 当一个 Pod 因为资源问题调度失败,会剔除低优先级的 Pod,这个过程也叫 抢占。

抢占的过程如下:

  1. 检查 Pod 调度失败的原因,来确定该 Pod 是否可以通过抢占解决创建问题
  2. 确定可以通过抢占解决创建问题后,就要找出所有可以通过抢占来成功创建该Pod的节点,方法是复制所有节点的信息,尝试删除该节点上的每一个Pod,这个删除还要考虑优先级,优先级低的会优先尝试,如果刚好找到删除一个或几个 Pod(称为”牺牲者”,Victims)就可以满足原Pod(称为”抢占者”,Preemptor)的创建需求,就返回该节点。
  3. 得到所有的牺牲者列表后,还要挑选出最合适的牺牲者,判断原则就是尽量减少抢占对整个系统的影响。比如,需要抢占的 Pod 越少越好,需要抢占的 Pod 的优先级越低越好,等等。
  4. 清理最佳牺牲者 Pod 的 nominatedNodeName 字段,并把抢占者的 nominatedNodeName,设置为被抢占的 Node 的名字,开启一个 Goroutine,同步地删除牺牲者。
  5. 抢占者重新进行一次调度的流程,如果在抢占的过程中,有新的资源被空闲出来,那么最终的调度结果就不一定是 nominatedNodeName 了。

一旦调度队列里有一个 Pod 有 nominatedNodeName 字段,那么其他 Pod 在 Predicates 阶段的遍历到 该 Node 的时候,会将同样的 Predicates 算法运行两遍:

  • 第一遍, 调度器会假设上述“潜在的抢占者”已经运行在这个节点上,然后执行 Predicates 算法。这一遍是为了检查当该 Pod 与抢占者有亲和或反亲和要求的话,该 Pod 还能不能正常创建
  • 第二遍, 调度器会正常执行 Predicates 算法,即:不考虑任何“潜在的抢占者”。这一遍是为了检查当抢占者不创建在该节点的时候,该 Pod 还能不能正常创建。

而只有这两遍 Predicates 算法都能通过时,这个 Pod 和 Node 才会被认为是可以绑定(bind)的。

15. 绑核虚拟机如何实现的?

K8S 是通过 cpuset 来实现的 cpu 核的绑定,要想开启 cpu 绑核,要满足两个条件

  1. Pod 必须是 Guaranteed 类型的 QoS
  2. Pod 的 cpu 资源的 requests 和 limits 必须是同一个相等的整数值

而对应到 KubeVirt 的 YAML 中,还需要多加一个条件:必须指定 dedicatedCpuPlacement=true

比如下面这个模板示例

spec:
  - name: nginx
    image: nginx
      spec:
        domain:
          cpu:
            cores: 2
            dedicatedCpuPlacement: true
            sockets: 1
            threads: 1
          resources:
            limits:
              memory: "1024Mi"
              cpu: "2"
            requests:
              memory: "1024Mi"
              cpu: "2"

这时候,该 Pod 就会被绑定在 2 个独占的 CPU 核上。当然,具体是哪两个 CPU 核,是由 kubelet 为你分配的。

16. 开启资源相关特性

在 kubelet 的配置文件(/usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf)的 ExecStart 添加对应的参数,可以开启更多的特性

  • 开启内存管理器 --feature-gates=MemoryManager=true
  • 设置CPU管理策略 --cpu-manager-policy=xx,xx 的可选项为:none (默认策略)和 static(允许为具有某些资源特征的Pod授予节点上更高的CPU亲和力和排他性)
  • 设置 CPU 拓扑策略 --topology-manager-policy=xx,xx 的可选项为: none 和 best-effort 、restricted、single-numa-node

修改完后,需要执行如下两个步骤生效

# 删除历史数据
rm -f /var/lib/kubelet/cpu_manager_state

# 重启服务
systemctl daemon-reload && systemctl restart kubelet

17. 遗留问题

17.1 为什么虚拟机进程的占用内存会大于虚拟机内部可见的内存

创建一个 500M 的虚拟机,在虚拟机内部可见的内存是 454M

$ free -m
              total        used        free      shared  buff/cache   available
Mem:            454          36         268           8         149         389
Swap:             0  

但是在宿主机或者Pod里查看虚拟机进程的实际占用内存却有 1.5G

K8S 中虚拟机的资源管理与Pod调度插图(13)

参考链接

weixin

文章有帮助,请作者喝杯咖啡?

发表评论