原文地址:https://kubernetes.io/blog/2024/12/17/kube-apiserver-api-streaming/
作者:Stefan Schimanski(Upbound)、Wojciech Tyczynski(Google)、Lukasz Szaszkiewicz(Red Hat)
2024年12月17日,星期二
高效地管理 Kubernetes 集群至关重要,尤其是随着集群规模的增长。大规模集群的一个显著挑战是,由于列表请求带来的内存开销。在现有的实现中,kube-apiserver 通过在将数据发送到客户端之前,先在内存中组装整个响应来处理列表请求。但是,如果响应体非常大,比如达到数百兆字节,该怎么办呢?另外,假设有多个列表请求在同一时间涌入,可能是在网络短暂中断后。虽然 API 优先级和公平性(API Priority and Fairness)已经证明可以合理地保护 kube-apiserver 防止 CPU 过载,但它对内存保护的效果相对较小。这可以通过单个 API 请求的资源消耗性质来解释——CPU 使用量在任何时候都有一个固定的上限,而内存则不可压缩,随着处理的对象数量增加,内存的使用量会成比例增长,因此没有上限。这种情况构成了真正的风险,可能会在几秒钟内通过内存溢出(OOM)导致 kube-apiserver 崩溃。为了更直观地了解这个问题,我们来看下面的图表。
图表展示了在一个合成测试期间,kube-apiserver 的内存使用情况(详细信息见合成测试部分)。结果清楚地表明,增加信息器数量显著增加了服务器的内存消耗。特别地,在约16:40时,服务器在仅处理16个信息器时就崩溃了。
为什么 kube-apiserver 会为列表请求分配如此多的内存?
我们的调查发现,这种大量内存分配发生的原因在于,服务器在向客户端发送第一个字节之前,必须执行以下操作:
1. 从数据库中获取数据;
2. 将数据从存储格式反序列化;
3. 最终通过转换和序列化数据为客户端请求的格式来构造最终响应。
这个过程会导致显著的临时内存消耗。实际的内存使用情况依赖于许多因素,如页面大小、应用的过滤器(例如标签选择器)、查询参数以及单个对象的大小。
不幸的是,无论是 API 优先级和公平性,还是 Golang 的垃圾回收或内存限制,都无法在这种情况下防止系统内存耗尽。内存是快速且突发地分配的,几次请求就可能迅速耗尽可用内存,从而导致资源耗尽。
根据 API 服务器在节点上的运行方式,它可能在超过配置的内存限制时被操作系统通过 OOM(内存溢出)杀死,或者如果没有配置内存限制,可能对控制平面节点造成更严重的影响。最糟糕的是,在第一个 API 服务器失败后,类似的请求可能会影响到 HA 设置中的另一个控制平面节点,并且很可能会产生相同的影响。这种情况通常很难诊断,并且恢复起来也非常困难。
流式传输列表请求
今天,我们很高兴宣布一个重大的改进。随着 Kubernetes 1.32 中 Watch List 特性毕业为 Beta 版本,client-go 用户可以选择启用流式传输列表(在显式启用 WatchListClient 功能门控后),通过将列表请求切换为(一种特殊类型的)观察请求来进行流式传输。
观察请求通过观察缓存提供服务,观察缓存是一个内存中的缓存,旨在提高读取操作的可扩展性。通过逐个流式传输每个项,而不是返回整个集合,新的方法维持了恒定的内存开销。API 服务器的内存消耗受限于 etcd 中单个对象的最大大小,再加上一些额外的分配。与传统的列表请求相比,这种方法显著减少了临时内存使用,从而确保了系统的更高效率和稳定性,尤其是在拥有大量某种类型对象或较大平均对象大小的集群中,尽管分页后内存消耗通常仍然很高。
基于合成测试(见合成测试部分)中获得的洞察,我们开发了一个自动化的性能测试来系统性地评估 Watch List 功能的影响。该测试复制了相同的场景,生成大量带有大负载的 Secrets,并通过扩展信息器的数量来模拟大量列表请求模式。这个自动化测试会定期执行,以监控启用和禁用该功能时服务器的内存使用情况。
测试结果表明,启用 Watch List 功能后,内存消耗明显得到改善。启用该功能时,kube-apiserver 的内存消耗稳定在大约 2 GB。而禁用该功能时,内存使用量增加到大约 20 GB,增加了 10 倍!这些结果验证了新的流式 API 的有效性,显著减少了临时内存占用。
启用 API 流式传输
升级到 Kubernetes 1.32,确保你的集群使用 etcd 版本 3.4.31+ 或 3.5.13+。 2. 将你的客户端软件更改为使用 Watch List。如果你的客户端代码是用 Golang 编写的,您需要启用 WatchListClient 特性,适用于 client-go。有关启用该功能的详细信息,请阅读 Introducing Feature Gates to Client-Go: Enhancing Flexibility and Control,链接如下:Introducing Feature Gates to Client-Go: Enhancing Flexibility and Control | Kubernetes。
下一步?
在 Kubernetes 1.32 中,尽管该功能仍处于 Beta 阶段,但 kube-controller-manager 默认启用该功能。未来,这个功能将扩展到其他核心组件,如 kube-scheduler 或 kubelet;一旦该功能全面发布(或更早),其他第三方组件也鼓励在 Beta 阶段选择加入此功能,尤其是当它们面临访问大量资源或大对象时。
目前,API 优先级和公平性给列表请求分配了一个合理的小成本。这是为了在列表请求足够便宜的情况下,允许足够的并行性处理平均情况。但是,这并不适应大量和大对象的特殊情况。一旦 Kubernetes 生态系统的大部分组件都切换到 Watch List,列表请求的成本估算可以调整为较大的值,而不必担心平均情况下性能下降,从而增强对这种类型请求的保护,以防未来仍然会影响到 API 服务器。
合成测试
为了重现该问题,我们进行了手动测试,以了解列表请求对 kube-apiserver 内存使用的影响。在测试中,我们创建了 400 个 Secrets,每个 Secret 包含 1MB 的数据,并使用信息器检索所有 Secrets。
结果令人震惊,仅仅 16 个信息器就足以让测试服务器内存耗尽并崩溃,展示了在这种条件下内存消耗如何迅速增长。
特别感谢 @deads2k 对该功能的帮助和支持。