一、前言
对于一个编排系统来说,资源管理至少需要考虑以下几个方面:
-
资源模型的抽象 包括,
(1) 有哪些种类的资源,例如,CPU、内存等;
(2) 如何用数据结构表示这些资源; -
资源的调度
(1) 如何描述一个 workload 的资源申请(spec),例如,“该容器需要 4 核和 12GB~16GB 内存”;
(2) 如何描述一台 node 当前的资源分配状态,例如已分配/未分配资源量,是否支持超分等;
(3) 调度算法:如何根据 workload spec 为它挑选最合适的 node; -
资源的限额(capacity enforcement)
(1) 如何确保 workload 使用的资源量不超出预设范围(从而不会影响其他 workload);
(2) 如何确保 workload 和系统/基础服务的限额,使二者互不影响。
k8s 是目前最流行的容器编排系统,那它是如何解决这些问题的呢?
二、k8s 资源模型
对照上面几个问题,我们来看下 k8s 是怎么设计的:
- 资源模型:
(1) 抽象了 cpu/memory/device/hugepage 等资源类型;
(2) 抽象了 node 概念;
- 资源调度:
(1) 抽象了 request 和 limit 两个概念,分别表示一个容器所需要的最小(request)和最大(limit)资源量;
(2) 调度算法根据各 node 当前可供分配的资源量(Allocatable),为容器选择合适的 node; 注意,k8s 的调度只看 requests,不看 limits。
- 资源 enforcement:
(1) 使用 cgroup 在多个层面确保 workload 使用的最大资源量不超过指定的 limits。
(2) --system-reserved-cgroup=“” 和 --kube-reserved-cgroup=“” 默认为空,表示不创建,也就是系统组件和 pod 之间并没有严格隔离
一个资源申请(容器)的例子:
apiVersion: v1
kind: Pod
spec:
containers:
- name: busybox
image: busybox
resources:
limits:
cpu: 500m
memory: "400Mi"
requests:
cpu: 250m
memory: "300Mi"
command: ["md5sum"]
args: ["/dev/urandom"]
这里面 requests 和 limits 分别表示所需资源的最小和最大值,
(1) CPU 资源的单位 m 是 millicores 的缩写,表示千分之一核, 因此 cpu: 500m 就表示需要 0.5 核;
(2) 内存的单位很好理解,就是 MB、GB 等常见单位。
2.1 Node 资源抽象
$ k describe node <node>
...
Capacity:
cpu: 48
mem-hard-eviction-threshold: 500Mi
mem-soft-eviction-threshold: 1536Mi
memory: 263192560Ki
pods: 256
Allocatable:
cpu: 46
memory: 258486256Ki
pods: 256
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 800m (1%) 7200m (15%)
memory 1000Mi (0%) 7324Mi (2%)
hugepages-1Gi 0 (0%) 0 (0%)
...
分别来看下这几个部分。
2.1.1 Capacity
这台 node 的总资源量(可以简单理解为物理配置), 例如上面的输出显示,这台 node 有 48CPU、256GB 内存等等。
2.1.2 Allocatable
可供 k8s 分配的总资源量, 显然,Allocatable 不会超过 Capacity,例如上面看到 CPU 就少了 2 个,只剩下 46 个。
2.1.3 Allocated
这台 node 目前已经分配出去的资源量,注意其中的 message 也说了,node 可能会超分,所以加起来可能会超过 Allocatable,但不会超过 Capacity。
Allocatable 不超过 Capacity,这个概念上也是很好理解的; 但具体是哪些资源被划出去,导致 Allocatable < Capacity 呢?
2.2 Node 资源切分(预留)
由于每台 node 上会运行 kubelet/docker/containerd 等 k8s 相关基础服务, 以及 systemd/journald 等操作系统本身的进程,因此并不是一台 node 的所有资源都能给 k8s 创建 pod 用。 所以,k8s 在资源管理和调度时,需要把这些基础服务的资源使用量和 enforcement 单独拎出来。
为此,k8s 提出了 Node Allocatable Resources 提案,上面的 Capacity、Allocatable 等术语正是从这里来的。几点说明:
(1) 如果 Allocatable 可用,调度器会用 Allocatable,否则会用 Capacity;
(2) 用 Allocatable 是不超分,用 Capacity 是超分(overcommit);
计算公式:[Allocatable] = [NodeCapacity] - [KubeReserved] - [SystemReserved] - [HardEvictionThreshold]
分别来看下这几种类型。
2.2.1 SystemReserved
操作系统的基础服务,例如 systemd、journald 等,在 k8s 管理之外。 k8s 不能管理这些资源的分配,但是能管理这些资源的限额(enforcement),后面会看到。
2.2.2 KubeReserved
k8s 基础设施服务,包括 kubelet/docker/containerd 等等。 跟上面系统服务类似,k8s 不能管理这些资源的分配,但是能管理这些资源的限额(enforcement),后面会看到。
2.2.3 EvictionThreshold(驱逐门限)
当 node memory/disk 等资源即将耗尽时,kubelet 就开始按照 QoS 优先级(besteffort/burstable/guaranteed)驱逐 pod, eviction 资源就是为这个目的预留的。 更多信息。
2.2.4 Allocatable
可供 k8s 创建 pod 使用的资源。
以上就是 k8s 的基本资源模型。下面再看几个相关的配置参数。
2.3 kubelet 相关配置参数
资源预留(切分)相关的 kubelet 命令参数:
--system-reserved=""
--kube-reserved=""
--qos-reserved=""
--reserved-cpus=""
也可以通过 kubelet 配置文件,例如,
$ cat /etc/kubernetes/kubelet/config
...
systemReserved:
cpu: "2" # 上面 describe node 输出中, Allocatable 比 Capacity 少 2 个 CPU
memory: "4Gi" # 以及少 4GB 内存
是否需要对这些 reserved 资源用专门的 cgroup 来做资源限额,以确保彼此互不影响:
--system-reserved-cgroup=""
--kube-reserved-cgroup=""
默认都是不启用。实际上也很难做到完全隔离。导致的后果就是系统进程和 pod 进程有可能相互影响, 例如,截至 v1.26,k8s 还不支持 IO 隔离,所以宿主机进程(例如 logrotate)IO 飙高, 或者某个 pod 进程执行 java dump 时,会影响这台 node 上所有 pod。
关于 k8s 资源模型就先介绍到这里,接下来进入本文重点,k8s 是如何用 cgroup 来限制 container、pod、基础服务等 workload 的资源使用量的(enforcement)。
三、k8s cgroup 层次设计
3.1 Linux cgroup 概要
cgroup 是 Linux 内核基础设施,可以限制、记录和隔离进程组(process groups) 使用的资源量(CPU、内存、IO 等)。
cgroup 有两个版本,v1 和 v2,二者的区别可参考 Control Group v2 (cgroupv2 权威指南)(KernelDoc, 2021)。 目前 k8s 默认使用的是 cgroup v1,因此本文以 v1 为主。
cgroup v1 能管理很多种类的资源,
$ mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls)
k8s/kubelet 中只用到了 cpu/memory/pid/hugetlb 等几种类型。
3.2 两种 cgroup runtime driver
k8s 通过配置 cgroup 来限制 container/pod 能使用的最大资源量。这个配置有两种实现方式, 在 k8s 中称为 cgroup runtime driver:
-
cgroupfs这种比较简单直接,kubelet 往 cgroup 文件系统中写 limit 就行了。这也是目前 k8s 的默认方式。
-
systemd所有 cgroup-writing 操作都必须通过 systemd 的接口,