本文我们会回顾一下 Kubernetes 中 CPU request 和 limit 的含义,以及背后实现的原理。
为 Pod 和容器管理资源
官方文档《为 Pod 和容器管理资源》定义如下:
当我们定义 Pod 时可以选择性地为每个 容器设定所需要的资源数量。最常见的可设定资源是 CPU 和内存(RAM)大小;此外还有其他类型的资源。
当我们为 Pod 中的 Container 指定了资源 request(请求) 时, kube-scheduler 就利用该信息决定将 Pod 调度到哪个节点上。当我们为 Container 指定了资源 limit(限制) 时,kubelet 就可以确保运行的容器不会使用超出所设限制的资源。kubelet 还会为容器预留所 request(请求) 数量的系统资源,供其使用。
请求和限制
如果 Pod 运行所在的节点具有足够的可用资源,容器可能(且可以)使用超出对应资源 request 属性所设置的资源量。不过,容器不可以使用超出其资源 limit 属性所设置的资源量。
例如,如果我们将容器的 memory 的请求量设置为 256 MiB,而该容器所处的 Pod 被调度到一个具有 8 GiB 内存的节点上,并且该节点上没有其他 Pod 运行,那么该容器就可以尝试使用更多的内存。
如果我们将某容器的 memory 限制设置为 4 GiB,kubelet (和容器运行时)就会确保该限制生效。容器运行时会禁止容器使用超出所设置资源限制的资源。例如:当容器中进程尝试使用超出所允许内存量的资源时,系统内核会将尝试申请内存的进程终止, 并引发内存不足(OOM)错误。
限制可以以被动方式来实现(系统会在发现违例时进行干预),或者通过强制生效的方式实现 (系统会避免容器用量超出限制)。不同的容器运行时采用不同方式来实现相同的限制。
说明:如果你为某个资源指定了限制,但不指定请求, 并且没有应用准入时机制为该资源设置默认请求, 然后 Kubernetes 复制你所指定的限制值,将其用作资源的请求值。
Kubernetes 应用资源请求与限制的方式
当 kubelet 将容器作为 Pod 的一部分启动时,它会将容器的 CPU 和内存请求与限制信息传递给容器运行时。
在 Linux 系统上,容器运行时通常会配置内核 CGroups,负责应用并实施所定义的请求。
CPU 限制定义的是容器可使用的 CPU 时间的硬性上限。在每个调度周期(时间片)期间,Linux 内核检查是否已经超出该限制;内核会在允许该 cgroup 恢复执行之前会等待 (这就是我们常说的 CPU Throttling,节流)。
CPU 请求值定义的是一个权重值。如果若干不同的容器(CGroups)需要在一个共享的系统上竞争运行, CPU 请求值大的负载会获得比请求值小的负载更多的 CPU 时间。
内存请求值主要用于(Kubernetes)Pod 调度期间。在一个启用了 CGroup v2 的节点上, 容器运行时可能会使用内存请求值作为设置 memory.min 和 memory.low 的提示值。
内存限制定义的是 cgroup 的内存限制。如果容器尝试分配的内存量超出限制, 则 Linux 内核的内存不足处理子系统会被激活,并停止尝试分配内存的容器中的某个进程。如果该进程在容器中 PID 为 1,而容器被标记为可重新启动,则 Kubernetes 会重新启动该容器。
Pod 或容器的内存限制也适用于通过内存供应的卷,例如 emptyDir 卷。kubelet 会跟踪 tmpfs 形式的 emptyDir 卷用量,将其作为容器的内存用量, 而不是临时存储用量。
如果某容器内存用量超过其内存请求值并且所在节点内存不足时,容器所处的 Pod 可能被逐出。
每个容器可能被允许也可能不被允许使用超过其 CPU 限制的处理时间。但是,容器运行时不会由于 CPU 使用率过高而杀死 Pod 或容器。
CPU limit 和 Throttling
《k8s CPU limit和throttling的迷思》一文,原因中指出:
其中,request是给调度看的,调度会确保节点上所有负载的CPU request合计与内存request合计分别都不大于节点本身能够提供的CPU和内存,limit是给节点(kubelet)看的,节点会保证负载在节点上只使用这么多CPU和内存。例如,下面配置意味着单个负载会调度到一个剩余CPU request大于0.1核,剩余request内存大于200MB的节点,并且负载运行时的CPU使用率不能高于0.4核(超过将被限流),内存使用不多余300MB(超过将被OOM Kill并重启)。
resources: requests: memory: 200Mi cpu: "0.1" limits: memory: 300Mi cpu: "0.4"
另外流传很广的文章:Kubernetes OOM and CPU Throttling,有如下内容:
CPU process in Kubernetes
CPU is handled in Kubernetes with shares. Each CPU core is divided into 1024 shares, then divided between all processes running by using the cgroups (control groups) feature of the Linux kernel.
If the CPU can handle all current processes, then no action is needed. If processes are using more than 100% of the CPU, then shares come into place. As any Linux Kernel, Kubernetes uses the CFS (Completely Fair Scheduler) mechanism, so the processes with more shares will get more CPU time.
这里又提到了 1024,感觉有点乱!
这里要补充一下 CPU request/limit 和 CGroup 的关系
一个例子
以下是 Kubernetes 中比较典型的资源配置
resources: requests: memory: 50Mi cpu: 50m limits: memory: 100Mi cpu: 100m
单位后缀m代表“千分之一核心”,因此该资源对象指定容器进程需要 50/1000 个核心 (5%),并且最多允许使用 100/1000 个核心 (10%)。同样2000m是两个完整核心,也可以指定为2或2.0。
让我们创建一个仅请求 cpu 的 pod,看看如何在 docker 和 cgroup 级别进行配置:
kubectl run limit-test --image=busybox --requests "cpu=50m" --command -- /bin/sh -c "while true; do sleep 2; done"deployment.apps "limit-test" created
我们可以看到 kubernetes 配置了50m cpu request:
kubectl get pods limit-test-5b4c495556-p2xkr -o=jsonpath='{.spec.containers[0].resources}'map[requests:map[cpu:50m]]
我们还可以看到 docker 是如何配置容器的:
$ docker ps | grep busy | cut -d' ' -f1f2321226620e$ docker inspect f2321226620e --format '{{.HostConfig.CpuShares}}'51
为什么是 51,而不是 50?
cpu control group 和 docker 都将一个核心划分为 1024 份,而 Kubernetes 将其划分为 1000 份。
docker 如何将这个 request 应用到容器进程上?这和 docker 配置进程的 memory cgroup 一样,设置 cpu request / limit 也会导致它配置 cpu,cpuacct cgroup:
ps ax | grep /bin/sh 60554 ? Ss 0:00 /bin/sh -c while true; do sleep 2; done sudo cat /proc/60554/cgroup...4:cpu,cpuacct:/kubepods/burstable/pode12b33b1-db07-11e8-b1e1-42010a800070/3be263e7a8372b12d2f8f8f9b4251f110b79c2a3bb9e6857b2f1473e640e8e75 ls -l /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pode12b33b1-db07-11e8-b1e1-42010a800070/3be263e7a8372b12d2f8f8f9b4251f110b79c2a3bb9e6857b2f1473e640e8e75total 0drwxr-xr-x 2 root root 0 Oct 28 23:19 .drwxr-xr-x 4 root root 0 Oct 28 23:19 .....-rw-r--r-- 1 root root 0 Oct 28 23:19 cpu.shares
Docker 的 HostConfig.CpuShares 属性映射到 cpu.shares 这个 cgroup 的属性:
sudo cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/podb5c03ddf-db10-11e8-b1e1-42010a800070/64b5f1b636dafe6635ddd321c5b36854a8add51931c7117025a694281fb11444/cpu.shares51
让我们也看一下 limit 对应的 cgroup 属性:
kubectl run limit-test --image=busybox --requests "cpu=50m" --limits "cpu=100m" --command -- /bin/sh -c "while true; dosleep 2; done"deployment.apps "limit-test" created kubectl get pods limit-test-5b4fb64549-qpd4n -o=jsonpath='{.spec.containers[0].resources}'map[limits:map[cpu:100m] requests:map[cpu:50m]]
容器相关的配置:
docker ps | grep busy | cut -d' ' -f1f2321226620e docker inspect 472abbce32a5 --format '{{.HostConfig.CpuShares}} {{.HostConfig.CpuQuota}} {{.HostConfig.CpuPeriod}}'51 10000 100000
正如我们在上面看到的,CPU request 存储在 HostConfig.CpuShares 属性中。CPU limit 由两个值表示:HostConfig.CpuPeriod 和 HostConfig.CpuQuota。这些 docker 容器配置属性映射到进程 cpu,cpuacct cgroup 的两个附加属性:cpu.cfs_period_us 和 cpu.cfs_quota_us。
sudo cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod2f1b50b6-db13-11e8-b1e1-42010a800070/f0845c65c3073e0b7b0b95ce0c1eb27f69d12b1fe2382b50096c4b59e78cdf71/cpu.cfs_period_us100000 sudo cat /sys/fs/cgroup/cpu,cpuacct/kubepods/burstable/pod2f1b50b6-db13-11e8-b1e1-42010a800070/f0845c65c3073e0b7b0b95ce0c1eb27f69d12b1fe2382b50096c4b59e78cdf71/cpu.cfs_quota_us10000
原理说明
CPU Request 和 CPU limit 是使用两个独立的控制系统实现的。request 使用了 CPU 共享系统,它将每个核心划分为 1024 个片,并保证每个进程将获得这些片的对应份额。如果有 1024 个切片,并且两个进程中的每一个都设置 cpu.shares 为 512,那么它们将各自获得大约一半的可用时间。然而,CPU 共享系统无法强制执行上限。如果一个进程不使用其共享,则另一个进程可以自由使用。
request 值不仅仅影响 Kubernetes 的调度,也会影响进程在 CPU 上的调度。
2010 年左右,Google 和其他公司注意到这可能会导致问题。作为回应,添加了第二个功能更强大的系统:CPU 带宽控制。
带宽控制系统定义了一个周期(通常为 1/10 秒,即 100 毫秒或者 100000 微秒)和一个配额,该配额表示该周期内允许进程在 CPU 上运行的最大切片数。在此示例中,我们要求 100m pod 的 CPU 限制。即 100/1000 个核心,或 100000 微秒的 CPU 时间中的 10000。因此,我们的 limit 转化为 cgroup 子控制组(cpu,cpuacct)中 cpu.cfs_period_us=100000 以及 cpu.cfs_quota_us=10000 。顺便说一句,这里的 cfs 中的代表 Linux Completely Fair Scheduler,它是默认的 Linux CPU 调度程序。如果使用另外的实时调度器,则需要设置相对应的 quota 值。
官方定义
cpu.shares
包含一个整数值,指定 cgroup 中任务可用的 CPU 时间的相对份额。例如,两个 cgroup 中 cpu.shares 设置为 100 的任务将获得相同的 CPU 时间,但 cgroup 中 cpu.shares 设置为 200 的任务获得的 CPU 时间是 cpu.shares 设置为 100 的 cgroup 中任务的两倍。cpu.shares 文件中指定的值必须为 2 或更高。
请注意,CPU 时间份额分布在多核系统上的所有 CPU 核心上。即使 cgroup 在多核系统上限制为低于 100% 的 CPU,它也可以使用每个单独 CPU 核心的 100%。请考虑以下示例:如果 cgroup A 配置为使用 25% 的 CPU,而 cgroup B 配置为 75%,则在具有四个核心的系统上启动四个 CPU 密集型进程(A 中的一个和 B 中的三个)会导致以下 CPU 份额划分:
PID | cgroup | CPU | CPU share |
---|---|---|---|
100 | A | 0 | 100% of CPU0 |
101 | B | 1 | 100% of CPU1 |
102 | B | 2 | 100% of CPU2 |
103 | B | 3 | 100% of CPU3 |
cpu.cfs_period_us
指定一个以微秒(μs,此处表示为“us”)为单位的时间段,用于指示应重新分配 cgroup 对 CPU 资源的访问的频率。如果 cgroup 中的任务每 1 秒能够访问单个CPU的时间为 0.2 秒,则将 cpu.cfs_quota_us 设置为200000,cpu.cfs_period_us 设置为 1000000。cpu.cfs_quota_us 参数的上限为1秒,下限为1秒 限制为 1000 微秒。默认值为 100000 微秒(即 100 毫秒)。
cpu.cfs_quota_us
指定 cgroup 中的所有任务在一个周期(由 cpu.cfs_period_us 定义)内可以运行的总时间,以微秒(μs,此处表示为“us”)为单位。一旦 cgroup 中的任务用完配额指定的所有时间,它们就会在该周期指定的剩余时间内受到限制,直到下一个周期才允许运行(这个就是 CPU 节流)。如果 cgroup 中的任务应该能够每 1 秒中有 0.2 秒访问单个 CPU,请将 cpu.cfs_quota_us 设置为 200000,将 cpu.cfs_period_us 设置为 1000000。请注意,配额和周期参数以 CPU 为基础进行操作。例如,要允许进程充分利用两个 CPU,请将 cpu.cfs_quota_us 设置为 200000,将 cpu.cfs_period_us 设置为 100000。
将 cpu.cfs_quota_us 中的值设置为 -1 表示 cgroup 不遵守任何 CPU 时间限制。这也是每个 cgroup(根 cgroup 除外)的默认值。
参考:
为 Pod 和容器管理资源
https://kubernetes.io/zh-cn/docs/concepts/configuration/manage-resources-containers/
Kubernetes OOM and CPU Throttling
https://sysdig.com/blog/troubleshoot-kubernetes-oom/
k8s CPU limit和throttling的迷思
https://nanmu.me/zh-cn/posts/2021/myth-of-k8s-cpu-limit-and-throttle/
3.2. cpu Red Hat Enterprise Linux 6 | Red Hat Customer Portal
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-cpu
https://Understanding resource limits in kubernetes: cpu time
https://medium.com/@betz.mark/understanding-resource-limits-in-kubernetes-cpu-time-9eff74d3161b
https://research.google/pubs/cpu-bandwidth-control-for-cfs/