我们应该摆脱平均 CPU 利用率

2026-05-22 1 阅读 JeremyTheo
为什么我们应该摆脱平均 CPU 利用率 2026-05-14 · 基于 2026 年 4 月亚琛云原生聚会上的演讲 我们应用程序中的 Go 函数在生产中不断被取消。该函数的超时时间很紧。相同的代码在我们的开发设置、CI 和 CD 管道以及我们进行的每个集成测试中都运行良好。在生产中,它有时会超过超时并因超过上下文截止日期而死亡。更糟糕的是我们使用的状态机库 1。当它的上下文被取消时,它不会自行恢复。它会崩溃并挂起。我们无法重现它。当我们与用户交谈时,他们报告 CPU 利用率看起来不错。我们花了几周时间才找到原因。为什么平均 CPU 不够用 当您使用计算机时,您学到的第一件事就是:当计算机变慢时,您打开任务管理器(或类似的工具)并检查 CPU。如果它很高,你就会看看这个过程是什么,然后停止它或者继续处理它,然后继续。在 Linux 服务器上,你可以看到 top、htop 等等,屏幕上还有很多其他数字。至少对我来说,当我使用它时,我总是只关注平均 CPU 值。然后,当您继续您的职业生涯时,您可以配置虚拟机。也许在 VMware 或本地 Hyper-V 上。也许在 AWS、Azure 或 Hetzner 上。您选择多个 vCPU。如果你仔细观察,它会说“性能”或“专用”之类的东西是更昂贵的选择。但至少我自己从来没有真正问过为什么。只是没有的话更便宜。每个工具、每个供应商、每个仪表板教给你的本能在这里失败了。每个工具都会显示平均 CPU 利用率,但没有一个工具可以帮助您解释它。它们都没有告诉您 CPU 利用率与您拥有的容量不是线性关系。他们只是向您显示一个百分比。 CPU 利用率从 80% 跃升至 81% 的等待时间比从 10% 跃升至 11% 的等待时间增加了大约 20 倍 2 。因此,即使在 80% 的利用率下有 20% 的“余量”,延迟也已经开始攀升 3:CPU 利用率 等待 10 毫秒的请求 10% ~11 毫秒 80% ~50 毫秒 95% ~200 毫秒 M/M/1 排队模型基线。 3 利用率低:新请求大约需要等待一个槽才能完成当前作业。更高的利用率:同一个请求等待三个槽。当然,现实更为复杂(随机到达、可变的服务时间)。 M/M/1 模型捕捉细节。平均 CPU 是解决一个问题的正确指标:我们的 CPU 是否得到利用?这是一个成本问题,IT 部门提出这个问题是正确的。仅当您的工作负载可以等待时它才有效。对于延迟敏感的系统,更高的利用率只意味着更长的等待时间。但在我们的例子中,CPU 利用率并不高。我们遇到的是 Docker 和 Kubernetes 用于强制执行每个容器限制的 Linux 内核功能,称为 cgroup ,以及它的副产品之一:限制。容器有资源限制,设置为 2000m 。我们将其解读为“两个 CPU”。内核将其读取为时间预算。当预算用完时,容器将受到限制,直到下一个周期开始。我们或我们的客户面前的任何工具都无法显示这一点。这就是为什么我们花了几周的时间才找到超过上下文截止日期的原因。每张图表都表明一切都很好。每个用户都说一切都很好。这是非常令人惊讶的。 CFS 限制实际上是如何工作的 让我们假设您正在处理 HTTP 消息的容器中运行一个服务,并且您已经设置了资源限制,因为这是每个指南所建议的 4 。保证 QoS pod 配置:请求 = 限制 = 2000m kubectl top pod 显示 800 毫核。您的 Horizo​​ntal Pod Autoscaler (HPA) 配置为以 80% 的利用率进行扩展。 2000m 限制中的 800m 为 40%,距离 80% 的目标还很远。一切看起来都很好。正确的?不,让我们仔细看看。有三个数字决定这里发生的情况: 您的资源限制:2000m 内核的 CFS 调度周期:默认为 100 毫秒。 5 主机CPU:4核。 100 毫秒的 CFS 调度周期。在 2000m 限制下,容器每个周期获得 200 毫秒的 CPU 时间。现在,为什么主机有多少个 CPU 核心很重要呢?因为这就是抽象泄漏的地方。容器可以在节点上的每个可用 CPU 核心上花费 200 毫秒。现在想象一个 HTTP 服务。收到的请求可能是资源密集型的,并且可能会在 50 毫秒内耗尽所有 4 个核心的全部可用预算。 4 个核心的 50 毫秒突发耗尽了 200 毫秒的预算。当第二个请求到达时,它会受到限制,并且必须再等待 50 毫秒,直到下一个调度周期。接下来的 50 毫秒挂钟将受到限制。现在想象一下这种情况重复发生。如果您的负载模式是突发、空闲、空闲、空闲、突发,则您的 p99 延迟可能会达到极限。然而每张 CPU 图表仍然表明一切都很好。突发-空闲-空闲-空闲-突发交替模式:平均 CPU 看起来很健康,而 p99 则在攀升。这就是我们的 Go 函数所发生的情况。该函数运行超时时间很短,需要在