本文译自 Avoiding CPU Throttling in a Containerized Environment。作者:Joakim Recht 和 Yury Vostrikov
在 Uber,所有有状态的工作负载都运行在一个跨大型主机的通用容器化平台上。有状态的工作负载包含 MySQL®、Apache Cassandra®、ElasticSearch®、Apache Kafka®、Apache HDFS™、Redis™、Docstore、Schemaless 等,在很多状况下,这些工作负载位于同一台物理主机上。
凭借 65,000 个物理主机、240 万个内核和 200,000 个容器,进步利用率以降低成本是一项重要且继续的工作。但最近,因为 CPU 限流,导致利用率晋升这件事没有那么顺利了。
事实证明,问题在于 Linux 内核如何为过程运行调配工夫。在这篇文章中,咱们将形容从 CPU 配额切换到 cpusets(也称为 CPU pinning),如何使咱们可能以 P50 提早的轻微减少换取 P99 提早的显著降落。因为资源需要的变动较小,这反过来又使咱们可能将整个集群范畴内的外围调配缩小 11%。
Cgroups、配额和 Cpusets
CPU 配额和 cpusets 是 Linux 内核的调度器性能。Linux 内核通过 cgroups 实现资源隔离,所有容器平台均以此为根底。通常,一个容器映射到一个 cgroup,它管制着在容器中运行的任何过程的资源。
有两种类型的 cgroup(Linux 术语中的控制器)用于执行 CPU 隔离:CPU 和 cpuset。它们都管制容许一组过程应用多少 CPU,但有两种不同的形式:别离通过 CPU 工夫配额和 CPU pinning。
CPU 配额
CPU 控制器应用配额来实现隔离。对于一个 CPU 集,你指定要容许的 CPU 比例(外围)。应用以下公式将其转换为给定时间段(通常为 100 毫秒)的配额:
配额 = core_count 周期(quota = core_count period)
在下面的例子中,有一个须要 2 个内核的容器,这相当于每周期须要 200 毫秒的 CPU 工夫。
CPU 配额和节流
因为容器内的多解决 / 线程,这种办法被证实是有问题的。这会使容器过快地用完配额,导致它在剩余时间段内受到限制。如下图所示:
对于提供低提早申请的容器来说,这是个问题。忽然间,因为节流,通常须要几毫秒能力实现的申请可能须要超过 100 毫秒。
简略的解决办法是为过程调配更多的 CPU 工夫。尽管这很无效,但在规模上也很低廉。另一种解决方案是基本不应用隔离。然而,这对于同一地点的工作负载来说是一个十分蹩脚的主见,因为一个过程可能会齐全耗尽其余过程。
应用 Cpusets 防止节流
cpuset 控制器应用 CPU pinning 而不是配额——它基本上限度了一个容器能够在哪些内核上运行。这意味着有可能将所有容器散布在不同的核上,以便每个核只服务于一个容器。这样就实现了齐全隔离,不再须要配额或节流,换句话说,能够用提早的一致性和更繁琐的核治理,来与解决突发和简略配置进行斗争。下面的例子看起来像这样:
两个容器在两组不同的内核上运行。它们被容许在这些外围上尽可能地应用,但不能应用未调配的外围。
这样做的后果是 P99 的提早变得更加稳固。上面是一个在启用 cpuset 时对生产数据库集群(每一行是一个容器)进行节流的例子。正如预期的那样,所有节流都隐没了:
节流景象隐没了,因为容器可能自在应用所有调配的内核。更乏味的是,因为容器可能以稳固的速率解决申请,P99 的提早也失去了改善。在这种状况下,因为打消了重大的节流,提早降落了 50% 左右。
在这一点上值得注意的是,应用 cpusets 也有负面影响。特地是,P50 提早通常会减少一点,因为它不再可能突入未调配的外围。后果 P50 和 P99 的提早变得更靠近,这通常是可取的。这点将在本文开端进行更多探讨。
调配 CPU
为了应用 cpusets,容器必须绑定到外围。正确调配内核须要一些对于古代 CPU 架构如何工作的背景常识,因为谬误的调配会导致性能显著降落。
CPU 通常围绕以下构造构建:
- 一台物理机能够有多个 CPU 插槽
- 每个插座都有独立的 L3 缓存
- 每个 CPU 有多个外围
- 每个外围都有独立的 L2/L1 缓存
- 每个外围都能够有超线程
- 超线程通常被视为外围,但调配 2 个超线程而不是 1 个可能只会将性能进步 1.3 倍
所有这些都意味着抉择正确的内核实际上很重要。最初一个问题是编号不是间断的,有时甚至不是确定性的——例如,拓扑可能如下所示:
在这种状况下,一个容器被安顿在物理套接字和不同的内核上,这会导致性能降落——咱们曾经看到因为谬误的套接字调配,P99 提早升高了多达 500%。为了解决这个问题,调度器必须从内核收集确切的硬件拓扑,并应用它来调配内核。原始信息在 /proc/cpuinfo 中找到:
利用这些信息,咱们能够调配物理上互相靠近的外围:
毛病和局限性
尽管 cpusets 解决了大部分提早的问题,但也存在一些限度和衡量:
无奈调配小数外围。 这对于数据库过程来说不是问题,因为它们往往很大,因而向上或向下舍入不是问题。然而,这的确意味着容器的数量不能大于内核的数量,这对于某些工作负载来说是有问题的。
零碎范畴的过程依然能够窃取工夫。 例如,通过 systemd、kernel workers 等在宿主机上运行的服务,依然须要在某个中央运行。实践上也能够将它们调配给一组无限的内核,但这可能很辣手,因为它们须要的工夫与零碎负载成正比。一种解决办法是在容器子集上应用实时过程调度——后文会介绍这一点。
须要进行碎片整顿。 随着工夫的推移,可用内核将变得碎片化,并且须要挪动过程以创立间断的可用内核块。这能够在线实现,然而从一个物理套接字挪动到另一个将意味着内存拜访忽然变得近程。这也能够缓解,另一篇文章会介绍。
没有突发限度。 有时你可能心愿应用主机上未调配的资源来减速正在运行的容器。在这篇文章中,咱们探讨了独占的 cpusets,但能够将同一个外围调配给多个容器(即 cgroups),也能够将 cpusets 与配额联合应用,这容许冲破限度。
论断
切换到有状态工作负载的 cpusets 是 Uber 的一项重大改良。它使咱们可能实现更稳固的数据库级别的提早,并且通过缩小适度配置以解决因为节流导致的峰值,节俭了大概 11% 的内核。因为没有突发限度,雷同大小的容器当初在主机之间的体现是一样的,这也导致了更稳固的性能。
Uber 的有状态部署平台是外部开发的,但 Kubernetes ® 也通过应用动态策略来反对 cpusets。
无关 Uber 如何测试配额和 cpusets 的细节,见附录。
云原生技术社区有 20+ 技术交换群,想进群跟技术大牛们聊天,或退出志愿者队伍,请加小助手微信:
本文由 mdnice 多平台公布