- CPU requests 如何工作
- CPU limits 如何工作
- 按编程语言分类的现状
- 限制不是最佳选择的情况
- 可以使用哪些替代方法来限制
Kubernetes CPU Requests
Requests 在 Kubernetes 中有两个目的:调度可用性
首先,它们告诉 Kubernetes 需要多少 CPU 可用。然后它会过滤掉任何没有足够的 Requests 资源来完成调度的节点。 如果没有足够的 Requests CPU/内存,则 Pod 将处于未调度状态,直到有满足 Requests 的资源。Guaranteed CPU 分配
一旦确定了在哪个节点上运行 Pod(在预选阶段幸存下来,然后得分最高的节点),它就会设置 Linux CPU shares 以大致与 mCPU 指标保持一致。CPU Shares 是 Linux 控制组 cgroups 功能**。 通常,cpu_shares 可以是任意数字,Linux 使用每个 cgroup 的 shares 数与所有 shares 总数之间的比率来确定在 CPU 上应该调度哪个进程的优先级。例如,如果一个进程有 1,000 个 shares,所有 shares 的总和为 3,000,则它将获得总时间的三分之一。就 Kubernetes 而言:- 它应该是在每个节点上配置 Linux CPU shares 的唯一工具,并且它总是将这些 shares 与 mCPU 指标对齐。
- 它只允许在机器上可用的实际逻辑 CPU 内核中请求最大的 mCPU(如果没有足够的未请求 CPU 可用,它将被调度器过滤掉)。
- 因此,它将 Linux cgroups CPU shares 几乎变成了系统上容器最少 CPU 内核的“保证”,而在 Linux 上不一定这样使用。
- 如果通过 LimitRange 在命名空间上设置了一个 Limit,那么这个 Limit 将会应用。
- 如果没有设置一个 request,但设置了 limit(直接或通过命名空间 LimitRange),那么它也会设置一个 request 等于 limit。
- 如果命名空间上没有 LimitRange,也没有 limit,那么如果不指定,就不会有 request。
- 没有节点会在调度中被过滤掉,所以它可以在任何节点上调度。这意味着从 CPU 的角度来看,可以过度配置集群。
- 结果得到的 Pods/容器将是节点上优先级最低的,因为它们没有 CPU shares (而那些设置了 request 的 Pod 是有的)。
Kubernetes CPU 限制
除了请求,Kubernetes 也有 CPU 限制。限制是应用程序可以使用的最大资源量,但不能保证,因为它们取决于可用资源。 虽然可以将请求视为确保容器至少具有该数量/比例的 CPU 时间,但限制却确保容器的 CPU 时间不能超过该数量。 虽然这可以保护系统上的其他工作负载免于与受限容器竞争,但它也会严重影响受限容器的性能。限制工作的方式也不直观,用户经常错误配置它们。 requests 使用 Linux cgroup CPU shares,而 limits 则使用 cgroup CPU quotas。 这些有两个主要组成部分:- Accounting Period:重置配额之前的时间量(以微秒为单位)。Kubernetes 默认将此设置为 100,000us(100 毫秒)。
- 配额周期:cgroup(本例中为容器)在该周期内可以拥有的 CPU 时间量(以微秒为单位)。Kubernetes 设置了这个 CPU == 1000m CPU == 100,000us == 100ms。
- 设置 Limit 以容纳所有线程。
- 降低线程数以便与 Limit 对齐。
编程语言的现状
node.js
Node 是单线程的(除非使用 worker_threads),所以如果没有它们,代码就不能使用一个以上的 CPU 内核。这使得它成为 Kubernetes 上扩展到更多 pod 的良好候选,而不是扩展单个 pod。- 例外的是,Node.js 在默认情况下运行四个线程来执行各种操作(文件系统 IO、DNS、crypto 和 zlib),而 UV_THREADPOOL_SIZE 环境变量可以控制这一点。
- 如果确实使用 worker_threads,那么想防止节流,那么需要确保运行的并发线程不会超过 CPU 限制。如果通过 piscina 这样的线程池包使用它们,这将更容易做到,在这种情况下,需要确保 maxThreads 被设置为与 CPU 限制相匹配,而不是与节点中的物理 CPU 相匹配(piscina 目前默认为物理 CPU 的 1.5 倍-并且不会从 cgroup/container limit 自动计算出限制)。
Python
像 Node.js 一样,解释性 Python 通常是单线程的,除了一些例外(使用多处理库,C/c++扩展等),不应该使用多个 CPU。这使得它成为一个很好的候选,就像 Node.js 一样,可以扩展到 Kubernetes 上的更多 pod,而不是扩展单个 pod。 注意,如果使用它,多处理库假定 Node 的物理内核数是默认情况下应该运行的线程数。目前似乎没有办法影响这种行为,除了在代码中设置它,而不是采用默认池大小。目前 GitHub 上有一个未解决的问题**。Java
Java 虚拟机 (JVM) 现在提供自动容器/cgroup 检测支持,这允许它确定在 Docker 容器中运行的 Java 进程可用的内存量和处理器数量。它使用此信息来分配系统资源。此支持仅适用于 Linux x64 平台。如果支持,则默认启用容器支持。如果不支持,可以使用<font style="color:rgb(58, 58, 58);">-XX:ActiveProcessorCount=X</font>
手动指定 CPU 限制的核心数。
NET/C
与 Java 一样,提供了自动容器/cgroup 检测支持**。Golang
为容器设置 GOMAXPROCS 环境变量以匹配任何 CPU 限制。或者使用 automaxprocs 包** 根据 Uber 的容器限制自动设置。
真的需要限制吗?
也许可以避免所有这些,只使用请求,因为它们讨论的是总 CPU 的比例,而不是像限制那样的 CPU 时间,因此在概念上更有意义。 限制的主要目的是确保特定的容器只能获得节点的特定份额。如果节点数量是相当固定的,可能是因为这是一个内部部署的数据中心,在接下来的几个月/几年里使用的裸金属节点数量是固定的,那么它们可能是维护整个多租户系统/环境的稳定性和公平性不得不接受的事。 然而,在云计算中,拥有的节点数量实际上是无限的,除了它们的成本。他们可以快速地提供和取消供应。这种弹性和灵活性是许多客户首先在云中运行这些工作负载的重要原因! 同样,需要多少节点直接取决于需要多少 pod 和它们的大小。需要的 pod 数量可以自动/动态地缩放,以响应在任何给定时间内要完成的工作量(请求的数量、队列中的项目数量等)。 可以做很多事情来节省成本,但是以一种损害其性能甚至可用性的方式限制工作负载应该是最后的手段。Limit 的替代品
结论
现在更好地了解了 Kubernetes CPU 限制,是时候检查工作负载并适当地配置它们了。 这通常意味着确保运行的并发线程数符合限制(尽管某些语言/运行时,如 Java 和 C# 现在可以做到这一点)。 而且,有时,这意味着根本不使用它们,而是依靠 Requests 和 Horizontal Pod Autoscaler (HPA) 的组合来确保始终有足够的容量以更动态(和更少的节流)方式运行。参考资料
Kubernetes 限制和请求的基础知识: https://sysdig.com/blog/kubernetes-limits-requests/
内存不足终止和 CPU 节流: https://sysdig.com/blog/troubleshoot-kubernetes-oom/
CPU Shares 是 Linux 控制组 cgroups 功能: https://www.batey.info/cgroup-cpu-shares-for-kubernetes.html
目前 GitHub 上有一个未解决的问题: https://github.com/python/cpython/issues/77167
手动指定 CPU 限制的核心数: https://docs.oracle.com/en/java/javase/19/docs/specs/man/java.html#advanced-runtime-options-for-java
提供了自动容器/cgroup 检测支持: https://github.com/dotnet/designs/blob/main/accepted/2019/support-for-memory-limits.md
为容器设置 GOMAXPROCS 环境变量: https://pkg.go.dev/runtime
automaxprocs 包: https://github.com/uber-go/automaxprocs