本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
1000 个节点及以上:Kubernetes 1.2 中的性能和可扩展性更新
编者按:这是关于 Kubernetes 1.2 新功能的深入系列文章中的第一篇
我们很自豪地宣布,随着 1.2 版本的发布,Kubernetes 现在支持 1000 节点的集群,并且大多数 API 操作的 99 分位尾延迟降低了 80%。这意味着在短短六个月内,我们将整体规模扩大了 10 倍,同时保持了出色的用户体验 — 99 分位的 Pod 启动时间小于 3 秒,大多数 API 操作的 99 分位延迟为几十毫秒(LIST 操作除外,在非常大的集群中需要数百毫秒)。
空谈无益,事实胜于雄辩。看看这个!
在上面的视频中,您看到集群在 1000 个节点上扩展到每秒 1000 万次查询 (QPS),包括滚动更新,零停机时间且对尾延迟没有影响。这足以成为互联网上排名前 100 的网站之一!
在这篇博文中,我们将介绍我们为实现这一结果所做的工作,并讨论我们未来扩展更高规模的一些计划。
方法论
我们根据以下服务级别目标 (SLO) 对 Kubernetes 的可扩展性进行基准测试
- API 响应速度 1 99% 的所有 API 调用在 1 秒内返回
- Pod 启动时间:99% 的 Pod 及其容器(带有预先拉取的镜像)在 5 秒内启动。我们说 Kubernetes 可以扩展到一定数量的节点,只有在满足这两个 SLO 的情况下。我们不断收集并报告上述测量结果,作为项目测试框架的一部分。这组测试分为两部分:API 响应速度和 Pod 启动时间。
用户级抽象的 API 响应速度2
Kubernetes 为用户提供了高级抽象来表示他们的应用程序。例如,ReplicationController 是一种表示 Pod 集合的抽象。列出所有 ReplicationController 或列出给定 ReplicationController 中的所有 Pod 是非常常见的用例。另一方面,很少有人会想要列出系统中的所有 Pod — 例如,30,000 个 Pod(1000 个节点,每个节点 30 个 Pod)表示大约 150MB 的数据(大约 5kB/Pod * 30k 个 Pod)。因此,此测试使用 ReplicationController。
对于此测试(假设 N 为集群中的节点数),我们
创建大约 3xN 个大小不同的 ReplicationController(5、30 和 250 个副本),总共有 30xN 个副本。我们随着时间的推移分散它们的创建(即我们不会同时启动所有副本),并等待它们全部运行。
对每个 ReplicationController 执行一些操作(缩放它,列出它的所有实例等),随着时间的推移分散这些操作,并测量每个操作的延迟。这类似于真实用户在正常集群操作过程中可能做的事情。
停止并删除系统中的所有 ReplicationController。有关此测试的结果,请参见下面的“Kubernetes 1.2 的指标”部分。
对于 v1.3 版本,我们计划通过创建服务、部署、守护程序集和其他 API 对象来扩展此测试。
Pod 启动端到端延迟3
用户也非常关心 Kubernetes 调度和启动 Pod 所需的时间。这不仅在初始创建时是这样,而且当 ReplicationController 需要创建替换 Pod 来接管其节点失败的 Pod 时也是如此。
我们(假设 N 为集群中的节点数)
创建一个具有 30xN 个副本的单个 ReplicationController,并等待它们全部运行。我们还运行高密度测试,使用 100xN 个副本,但集群中的节点较少。
启动一系列单 Pod ReplicationController - 每 200 毫秒一个。对于每个,我们测量“总端到端启动时间”(定义如下)。
停止并删除系统中的所有 Pod 和复制控制器。我们将“总端到端启动时间”定义为客户端向 API 服务器发送创建 ReplicationController 的请求的时间,到通过 watch 向客户端返回“运行中且准备就绪”Pod 状态的时间。这意味着“Pod 启动时间”包括创建 ReplicationController 并转而创建 Pod、调度程序调度该 Pod、Kubernetes 设置 Pod 内网络、启动容器、等待 Pod 成功响应运行状况检查,然后最终等待 Pod 将其状态报告回 API 服务器,然后 API 服务器通过 watch 将其报告给客户端。
虽然我们可以通过例如排除等待通过 watch 报告,或者直接创建 Pod 而不是通过 ReplicationController 来大幅减少“Pod 启动时间”,但我们认为,最符合实际用例的广泛定义最适合真实用户了解他们可以从系统中获得的预期性能。
Kubernetes 1.2 的指标
结果如何?我们在 Google Compute Engine 上运行我们的测试,根据 Kubernetes 集群的大小设置 master VM 的大小。特别是对于 1000 节点集群,我们使用 n1-standard-32 VM 作为 master(32 核,120GB RAM)。
API 响应速度
以下两个图表显示了 Kubernetes 1.2 版本和 100 节点集群上的 1.0 版本的 99 分位 API 调用延迟的比较。(较小的条形更好)
我们分别展示 LIST 操作的结果,因为这些延迟明显更高。请注意,我们在此期间稍微修改了我们的测试,因此对 v1.0 运行当前测试会导致比以前更高的延迟。
我们还对 1000 节点集群运行了这些测试。注意:我们不支持 GKE 上大于 100 个节点的集群,因此我们没有指标来比较这些结果。但是,自 Kubernetes 1.0 以来,客户报告说已经在 1,000 多个节点的集群上运行。
由于 LIST 操作明显更大,我们再次分别展示它们:两种集群大小的所有延迟都在我们 1 秒的 SLO 范围内。
Pod 启动端到端延迟
“Pod 启动延迟”(如“Pod 启动端到端延迟”部分中所定义)的结果在下图所示。作为参考,我们还在图表的第一部分中展示了 v1.0 中 100 节点集群的结果。
如您所见,我们大幅降低了 100 节点集群中的尾延迟,现在在我们测量的最大集群规模上提供了低 Pod 启动延迟。值得注意的是,对于 1000 节点集群,API 延迟和 Pod 启动延迟的指标通常都优于仅六个月前报告的 100 节点集群的指标!
我们是如何进行这些改进的?
为了在过去六个月中在规模和性能方面取得这些重大进展,我们在整个系统中进行了许多改进。下面列出了一些最重要的改进。
- _ 在 API 服务器级别创建“读取缓存” _
(https://github.com/kubernetes/kubernetes/issues/15945 )
由于大多数 Kubernetes 控制逻辑都在由 etcd 监视(通过 API 服务器)保持更新的有序、一致的快照上运行,因此该数据的到达稍微延迟不会影响集群的正确操作。 这些独立的控制器循环,为了系统的可扩展性而设计为分布式,很乐意为了提高整体吞吐量而牺牲一点延迟。
在 Kubernetes 1.2 中,我们利用这一事实,通过添加 API 服务器读取缓存来提高性能和可扩展性。 通过此更改,API 服务器的客户端可以从 API 服务器的内存缓存中读取数据,而不是从 etcd 中读取。 缓存通过后台的监视直接从 etcd 更新。 那些可以容忍数据检索延迟的客户端(通常缓存的滞后时间在几十毫秒左右)可以完全由缓存提供服务,从而减少 etcd 的负载并提高服务器的吞吐量。 这是 v1.1 中开始的优化的延续,在 v1.1 中,我们添加了直接从 API 服务器而不是 etcd 提供监视的支持:https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/apiserver-watch.md.
感谢 Google 的 Wojciech Tyczynski 以及 Red Hat 的 Clayton Coleman 和 Timothy St. Clair 的贡献,我们能够将仔细的系统设计与 etcd 的独特优势相结合,从而提高 Kubernetes 的可扩展性和性能。
- 在 Kubelet 中引入“Pod 生命周期事件生成器”(PLEG) (https://github.com/kubernetes/kubernetes/blob/master/docs/proposals/pod-lifecycle-event-generator.md)
Kubernetes 1.2 还从每个节点的 Pod 角度提高了密度 — 对于 v1.2,我们测试并宣布在单个节点上最多支持 100 个 Pod(而在 1.1 版本中为 30 个 Pod)。 此改进之所以成为可能,归功于 Kubernetes 社区通过实现 Pod 生命周期事件生成器 (PLEG) 而进行的辛勤工作。
Kubelet(Kubernetes 节点代理)为每个 Pod 都有一个工作线程,负责管理 Pod 的生命周期。 在早期版本中,每个工作线程都会定期轮询底层容器运行时 (Docker) 以检测状态更改,并执行任何必要的操作以确保节点的状态与所需的状态匹配(例如,通过启动和停止容器)。 随着 Pod 密度的增加,来自每个工作线程的并发轮询会使 Docker 运行时不堪重负,从而导致严重的可靠性和性能问题(包括额外的 CPU 利用率,这是扩大规模的限制因素之一)。
为了解决这个问题,我们引入了一个新的 Kubelet 子组件 — PLEG — 来集中状态更改检测并为工作线程生成生命周期事件。 随着并发轮询的消除,我们能够将 Kubelet 和容器运行时的稳态 CPU 使用率降低 4 倍。 这也使我们能够采用更短的轮询周期,以便更快地检测和响应更改。
改进的调度器吞吐量 来自 CoreOS 的 Kubernetes 社区成员(Hongchao Deng 和 Xiang Li)帮助深入研究了 Kubernetes 调度器,并在不牺牲准确性或灵活性的情况下显着提高了吞吐量。 他们将调度 30,000 个 Pod 的总时间缩短了近 1400%! 您可以在以下链接中阅读一篇关于他们如何解决此问题的精彩博客文章:https://coreos.com/blog/improving-kubernetes-scheduler-performance.html
更高效的 JSON 解析器 Go 的标准库包含一个灵活且易于使用的 JSON 解析器,该解析器可以使用反射 API 对任何 Go 结构进行编码和解码。 但这种灵活性是有代价的 — 反射会分配许多必须由运行时跟踪和垃圾收集的小对象。 我们的分析证实了这一点,表明客户端和服务器的大部分时间都花在了序列化上。 鉴于我们的类型不会频繁更改,我们怀疑可以通过代码生成绕过大量的反射。
在调查了 Go JSON 的前景并进行了一些初步测试后,我们发现 ugorji 编解码器库提供了最显着的加速 — 在使用生成的序列化程序时,JSON 的编码和解码速度提高了 200%,并且对象分配显着减少。 在向上游库贡献修复程序以处理我们的一些复杂结构之后,我们切换了 Kubernetes 和 go-etcd 客户端库。 除了 JSON 上下层的一些其他重要优化之外,我们还能够大幅降低几乎所有 API 操作(尤其是读取)的 CPU 时间成本。
其他值得注意的更改也带来了重大胜利,包括:
- 减少了导致不必要的新 TLS 会话的断开的 TCP 连接数量:https://github.com/kubernetes/kubernetes/issues/15664
- 提高了大型集群中 ReplicationController 的性能:https://github.com/kubernetes/kubernetes/issues/21672
在这两种情况下,问题都是由 Kubernetes 社区成员(包括 Red Hat 的 Andy Goldstein 和 Jordan Liggitt 以及网易的 Liang Mingqiang)调试和/或修复的。
Kubernetes 1.3 及更高版本
当然,我们的工作尚未完成。 我们将继续投资改进 Kubernetes 的性能,因为我们希望它可以像 Google 的 Borg 一样扩展到数千个节点。 感谢我们在测试基础设施方面的投资以及我们对团队如何在生产中使用容器的关注,我们已经确定了改进规模的下一步工作。
Kubernetes 1.3 的重点:
我们的主要瓶颈仍然是 API 服务器,它将大部分时间都花在了编组和解组 JSON 对象上。 我们计划添加对协议缓冲区的支持作为 API 的可选路径,用于组件间的通信以及将对象存储在 etcd 中。 用户仍然可以使用 JSON 与 API 服务器进行通信,但由于大多数 Kubernetes 通信都是集群内的(API 服务器到节点、调度器到 API 服务器等),我们预计主节点的 CPU 和内存使用率将大幅降低。
Kubernetes 使用标签来标识对象集; 例如,要识别哪些 Pod 属于给定的 ReplicationController,需要在命名空间中迭代所有 Pod,并选择那些与控制器的标签选择器匹配的 Pod。 添加一个可以利用现有 API 对象缓存的标签的高效索引器,将可以快速找到与标签选择器匹配的对象,从而使此常用操作更快。
调度决策基于许多不同的因素,包括基于请求的资源分散 Pod、分散具有相同选择器的 Pod(例如,来自同一 Service、ReplicationController、Job 等)、节点上是否存在所需的容器镜像等。 这些计算,特别是选择器分散,有很多改进的机会 — 请参阅 https://github.com/kubernetes/kubernetes/issues/22262 以了解一个建议的更改。
我们也对即将发布的 etcd v3.0 版本感到兴奋,该版本的设计考虑了 Kubernetes 的用例 — 它将提高性能并引入新功能。 来自 CoreOS 的贡献者已经开始为 Kubernetes 迁移到 etcd v3.0 奠定基础(请参阅 https://github.com/kubernetes/kubernetes/pull/22604)。 虽然此列表并未涵盖所有与性能相关的努力,但我们乐观地认为,我们将获得与从 Kubernetes 1.0 到 1.2 的性能提升一样大的性能提升。
结论
在过去的六个月中,我们显着提高了 Kubernetes 的可扩展性,使 v1.2 能够运行 1000 节点集群,其响应速度(以我们的 SLO 衡量)与我们之前仅在小得多的集群上实现的相同。 但这还不够 — 我们希望更快地进一步推动 Kubernetes。 Kubernetes v1.3 将进一步提高系统的可扩展性和响应速度,同时继续添加使构建和运行要求最高的基于容器的应用程序更容易的功能。
请加入我们的社区,帮助我们构建 Kubernetes 的未来! 有很多参与方式。 如果您对可扩展性特别感兴趣,您会对以下内容感兴趣:
- 我们的 可扩展性 slack 频道
- 可扩展性“特别兴趣小组”,每周四太平洋时间上午 9 点在 SIG-Scale hangout 开会 当然,有关该项目的一般信息的更多信息,请访问 www.kubernetes.io
1我们排除对“事件”的操作,因为这些更像系统日志,并且不是系统正常运行所必需的。
2这是来自 Kubernetes github 存储库的 test/e2e/load.go。
3这是来自 Kubernetes github 存储库的 test/e2e/density.go 测试
4我们正在研究在下一个版本中对此进行优化,但就目前而言,使用较小的主节点可能会导致显着的(数量级)性能下降。 我们鼓励任何针对 Kubernetes 运行基准测试或尝试复制这些发现的人员使用大小相似的主节点,否则性能将受到影响。