本文已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已不正确。
1000 节点及以上:Kubernetes 1.2 中的性能和可伸缩性更新
编者注:这是关于 Kubernetes 1.2 新特性系列深度文章的第一篇
我们很荣幸地宣布,随着 1.2 版本的发布,Kubernetes 现已支持 1000 节点集群,并且大多数 API 操作的 99% 尾延迟降低了 80%。这意味着在短短六个月内,我们将总体规模扩大了 10 倍,同时保持了出色的用户体验——99% 的 Pod 启动时间少于 3 秒,大多数 API 操作的 99% 延迟为几十毫秒(大型集群中的 LIST 操作是例外,需要几百毫秒)。
文字描述不错,但没有比演示更能说明问题了。请看!
在上面的视频中,您看到集群在 1000 多个节点上将每秒查询数 (QPS) 扩展到 1000 万,包括滚动更新,实现了零停机且对尾延迟没有影响。这个规模足以跻身互联网前 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 * 3 万个 Pod)。因此,此测试使用 ReplicationController。
对于此测试(假设 N 为集群中的节点数),我们
创建约 3xN 个不同大小(5、30 和 250 个副本)的 ReplicationController,它们总共有 30xN 个副本。我们将它们的创建分散在一段时间内(即我们不同时启动所有 ReplicationController),并等待直到它们全部运行起来。
对每个 ReplicationController 执行一些操作(扩缩容、列出其所有实例等),并将这些操作分散在一段时间内,同时测量每个操作的延迟。这类似于真实用户在正常集群操作过程中可能执行的操作。
停止并删除系统中的所有 ReplicationController。 有关此测试的结果,请参阅下面的“Kubernetes 1.2 的指标”部分。
对于 v1.3 版本,我们计划通过创建 Services、Deployments、DaemonSets 和其他 API 对象来扩展此测试。
Pod 启动端到端延迟3
用户也非常关注 Kubernetes 调度和启动 Pod 所需的时间。这不仅适用于初始创建,也适用于当 ReplicationController 需要创建替代 Pod 来接管其节点发生故障的 Pod 时。
我们(假设 N 为集群中的节点数)
创建一个包含 30xN 个副本的 ReplicationController,并等待直到它们全部运行起来。我们也在运行高密度测试,使用 100xN 个副本,但在集群中节点数较少。
启动一系列单 Pod ReplicationController - 每 200 毫秒一个。对于每个,我们测量“总端到端启动时间”(定义如下)。
停止并删除系统中的所有 Pod 和 ReplicationController。 我们将“总端到端启动时间”定义为从客户端向 API server 发送创建 ReplicationController 的请求时刻,到 Pod“运行中且就绪”状态通过 watch 返回给客户端的时刻。这意味着“Pod 启动时间”包括 ReplicationController 的创建以及随后创建 Pod,调度器调度该 Pod,Kubernetes 设置 Pod 内部网络,启动容器,等待直到 Pod 成功响应健康检查,最后等待直到 Pod 将其状态报告回 API server,然后 API server 通过 watch 将其报告给客户端。
虽然我们可以通过排除(例如)等待通过 watch 报告,或者直接创建 Pod 而不是通过 ReplicationController 来大幅缩短“Pod 启动时间”,但我们认为,采用一个映射到最现实用例的广泛定义,对于真实用户理解他们可以期望从系统获得的性能来说是最好的。
Kubernetes 1.2 的指标
结果如何?我们在 Google Compute Engine 上运行测试,根据 Kubernetes 集群的大小设置主节点 VM 的规格。特别是对于 1000 节点集群,我们使用 n1-standard-32 VM 作为主节点(32 核,120GB RAM)。
API 响应能力
以下两张图表展示了 Kubernetes 1.2 版本和 1.0 版本在 100 节点集群上的 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 server 层面创建了“读缓存” _
(https://github.com/kubernetes/kubernetes/issues/15945 )
由于大多数 Kubernetes 控制逻辑都是基于由 etcd watch(通过 API server)保持更新的有序、一致的快照进行操作的,因此数据到达的轻微延迟不会影响集群的正确运行。这些独立控制器循环按设计是分布式的,以提高系统的可扩展性,它们乐于牺牲一点延迟来换取整体吞吐量的提升。
在 Kubernetes 1.2 中,我们利用这一事实,通过添加 API server 读缓存来提高性能和可伸缩性。通过这一改变,API server 的客户端可以从 API server 的内存缓存中读取数据,而不是从 etcd 读取。缓存通过 watch 在后台直接从 etcd 更新。那些能够容忍数据检索延迟的客户端(通常缓存延迟在几十毫秒级别)可以完全从缓存提供服务,从而减轻 etcd 的负载并增加 server 的吞吐量。这是 v1.1 中开始进行的优化工作的延续,我们在 v1.1 中添加了直接从 API server 而不是 etcd 提供 watch 服务支持: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 设置一个 worker 线程,负责管理 Pod 的生命周期。在早期版本中,每个 worker 会定期轮询底层容器运行时(Docker)以检测状态变化,并执行任何必要的操作以确保节点状态与所需状态匹配(例如,通过启动和停止容器)。随着 Pod 密度的增加,每个 worker 的并发轮询会使 Docker 运行时不堪重负,导致严重的可靠性和性能问题(包括额外的 CPU 利用率,这是扩展的限制因素之一)。
为了解决这个问题,我们引入了一个新的 Kubelet 子组件——PLEG——来集中检测状态变化并为 worker 生成生命周期事件。消除了并发轮询后,我们将 Kubelet 和容器运行时的稳态 CPU 使用率降低了 4 倍。这还使我们能够采用更短的轮询周期,以便更快地检测和响应变化。
改进的调度器吞吐量 来自 CoreOS 的 Kubernetes 社区成员(邓宏超和李翔)帮助深入研究了 Kubernetes 调度器,并在不牺牲准确性或灵活性的情况下显著提高了吞吐量。他们将调度 30,000 个 Pod 的总时间缩短了近 1400%!您可以在此处阅读一篇关于他们如何解决这个问题的精彩博文:https://coreos.com/blog/improving-kubernetes-scheduler-performance.html
更高效的 JSON 解析器 Go 的标准库包含一个灵活易用的 JSON 解析器,可以使用反射 API 对任何 Go 结构体进行编码和解码。但这种灵活性伴随着代价——反射会分配大量小对象,这些对象需要由运行时跟踪和垃圾回收。我们的性能分析证实了这一点,显示客户端和服务器的大量时间都花在了序列化上。考虑到我们的类型不常改变,我们怀疑可以通过代码生成绕过大量的反射。
在调查了 Go JSON 生态并进行了一些初步测试后,我们发现 ugorji codec 库提供了最显著的加速——使用生成的序列化器时,JSON 编码和解码性能提高了 200%,对象分配也大幅减少。在向上游库贡献修复程序以处理我们的一些复杂结构后,我们切换了 Kubernetes 和 go-etcd 客户端库。加上 JSON 上下层的一些其他重要优化,我们成功地大幅降低了几乎所有 API 操作(尤其是读取操作)的 CPU 时间成本。
其他显著的改变带来了重要的收益,包括:
- 减少中断的 TCP 连接数,这些连接导致不必要的新的 TLS 会话: https://github.com/kubernetes/kubernetes/issues/15664
- 提高了 ReplicationController 在大型集群中的性能:https://github.com/kubernetes/kubernetes/issues/21672
在这两种情况下,问题都由 Kubernetes 社区成员进行了调试和/或修复,包括 Red Hat 的 Andy Goldstein 和 Jordan Liggitt,以及网易的梁明强。
Kubernetes 1.3 及以后
当然,我们的工作还没有完成。我们将继续投入精力改进 Kubernetes 的性能,希望它能像 Google 的 Borg 一样扩展到数千个节点。感谢我们在测试基础设施上的投入以及对团队如何在生产环境中使用容器的关注,我们已经确定了进一步提高可伸缩性的下一步措施。
Kubernetes 1.3 计划:
我们主要的瓶颈仍然是 API 服务器,它将大部分时间花费在序列化和反序列化 JSON 对象上。我们计划为 API 添加对 protocol buffers 的支持,作为组件间通信和在 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 能够以与之前仅在小得多集群上实现的同样出色的响应速度(根据我们的 SLO 衡量)运行 1000 节点集群。但这还不够 — 我们希望将 Kubernetes 推向更远更快。Kubernetes v1.3 将进一步改进系统的可扩展性和响应速度,同时继续添加新特性,使其更易于构建和运行要求最高的基于容器的应用。
请加入我们的社区,帮助我们构建 Kubernetes 的未来!有很多参与方式。如果您对可扩展性特别感兴趣,您可能会对以下内容感兴趣:
- 我们的可扩展性 Slack 频道
- 可扩展性“特别兴趣小组”(Special Interest Group),每周四太平洋时间上午 9 点在 SIG-Scale 环聊举行会议 当然,关于项目的更多一般信息,请访问 www.kubernetes.io
1我们排除了对“事件”(events)的操作,因为这些更像是系统日志,并非系统正常运行所必需的。
2这是来自 Kubernetes github 仓库的 test/e2e/load.go 文件。
3这是来自 Kubernetes github 仓库的 test/e2e/density.go 测试。
4我们正在研究在下个版本中优化这一点,但目前使用较小的主节点可能会导致显著(数量级)的性能下降。我们建议任何对 Kubernetes 进行基准测试或试图重现这些结果的人使用类似大小的主节点,否则性能会受到影响。