本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

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的可伸缩性进行基准测试

  1. API响应速度 1 99%的所有API调用在1秒内返回。
  2. Pod启动时间:99%的Pod及其容器(预拉取镜像)在5秒内启动。我们只有在满足这两个SLO的情况下才认为Kubernetes可以扩展到一定数量的节点。我们作为项目测试框架的一部分,持续收集和报告上述测量结果。这套测试分为两部分:API响应速度和Pod启动时间。

用户级抽象的API响应速度2

Kubernetes为用户提供高级抽象来表示他们的应用程序。例如,ReplicationController是表示Pod集合的抽象。列出所有ReplicationController或列出给定ReplicationController中的所有Pod是一个非常常见的用例。另一方面,几乎没有人会想要列出系统中的所有Pod——例如,30,000个Pod(1000个节点,每个节点30个Pod)代表大约150MB的数据(约5kB/Pod * 30k Pod)。因此,此测试使用ReplicationController。

对于此测试(假设N为集群中的节点数),我们

  1. 创建大约3xN个不同大小的ReplicationController(5、30和250个副本),它们总共有30xN个副本。我们随着时间推移分散它们的创建(即我们不会同时启动所有它们),并等待所有它们都运行起来。

  2. 对每个ReplicationController执行一些操作(扩展它,列出它的所有实例等),随着时间推移分散这些操作,并测量每个操作的延迟。这类似于真实用户在正常集群操作过程中可能做的事情。

  3. 停止并删除系统中的所有ReplicationController。有关此测试的结果,请参阅下面的“Kubernetes 1.2的度量”部分。

对于v1.3版本,我们计划通过创建Service、Deployment、DaemonSet和其他API对象来扩展此测试。

Pod启动端到端延迟3

用户也非常关心Kubernetes调度和启动Pod所需的时间。这不仅在初始创建时如此,而且当ReplicationController需要创建替代Pod以接替节点发生故障的Pod时也如此。

我们(假设N为集群中的节点数)

  1. 创建一个包含30xN个副本的单个ReplicationController,并等待所有副本运行。我们还在运行高密度测试,包含100xN个副本,但集群中的节点较少。

  2. 启动一系列单Pod ReplicationController——每200毫秒一个。对于每个,我们测量“总端到端启动时间”(定义见下文)。

  3. 停止并删除系统中的所有Pod和ReplicationController。我们将“总端到端启动时间”定义为从客户端向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集群的大小设置主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以来,客户已经报告在1000多个节点集群上运行。

由于LIST操作明显更大,我们再次单独展示它们:在两种集群大小下,所有延迟都远在我们1秒的SLO范围内。

Pod启动端到端延迟

“Pod启动延迟”(定义在“Pod启动端到端延迟”部分)的结果显示在下图中。作为参考,我们还在图的第一部分展示了v1.0在100节点集群上的结果。

正如您所看到的,我们大大降低了100节点集群的尾部延迟,现在在最大规模的集群中也能提供低Pod启动延迟。值得注意的是,1000节点集群的API延迟和Pod启动延迟的度量通常都优于六个月前100节点集群报告的度量!

我们是如何实现这些改进的?

为了在过去六个月中在规模和性能上取得这些显著进展,我们对整个系统进行了一系列改进。以下列出了一些最重要的改进。

由于大多数Kubernetes控制逻辑在由etcd watch(通过API服务器)保持最新状态的有序、一致快照上运行,因此数据到达的轻微延迟对集群的正确操作没有影响。这些独立控制器循环,通过设计分散以实现系统的可扩展性,乐于牺牲一点延迟来提高整体吞吐量。

在Kubernetes 1.2中,我们利用这一事实,通过添加API服务器读取缓存来提高性能和可伸缩性。通过这一更改,API服务器的客户端可以从API服务器的内存缓存中读取数据,而不是从etcd中读取。缓存通过后台的etcd watch直接更新。那些可以容忍检索数据延迟的客户端(通常缓存的滞后时间在几十毫秒的量级)可以完全从缓存中提供服务,从而减少etcd的负载并提高服务器的吞吐量。这是v1.1中开始的一项优化工作的延续,我们在v1.1中添加了直接从API服务器而不是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的可伸缩性和性能。

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时间成本,尤其是读取操作。

resync.png

在这两种情况下,问题都由Kubernetes社区成员调试和/或修复,包括Red Hat的Andy Goldstein和Jordan Liggitt,以及网易的Liang Mingqiang。

Kubernetes 1.3及未来

当然,我们的工作尚未完成。我们将继续投入精力改进Kubernetes的性能,因为我们希望它能扩展到数千个节点,就像Google的Borg一样。得益于我们在测试基础设施方面的投入以及对团队如何在生产环境中使用容器的关注,我们已经确定了改进规模的下一步措施。

Kubernetes 1.3的计划包括:

  1. 我们的主要瓶颈仍然是API服务器,它大部分时间都花在JSON对象的编组和解组上。我们计划为API添加协议缓冲区支持,作为组件间通信和在etcd中存储对象的可选路径。用户仍然可以使用JSON与API服务器通信,但由于大多数Kubernetes通信是集群内部的(API服务器到节点,调度器到API服务器等),我们预计主节点上的CPU和内存使用量将显著减少。

  2. Kubernetes使用标签来标识对象集;例如,识别哪些Pod属于给定的ReplicationController需要遍历命名空间中的所有Pod并选择与控制器标签选择器匹配的Pod。添加一个高效的标签索引器,可以利用现有的API对象缓存,将使快速查找匹配标签选择器的对象成为可能,从而使这种常见操作更快。

  3. 调度决策基于许多不同的因素,包括根据请求资源分散Pod,分散具有相同选择器的Pod(例如,来自同一服务、ReplicationController、Job等),节点上是否存在所需的容器镜像等。这些计算,特别是选择器分散,有许多改进的机会——有关其中一个建议的更改,请参阅https://github.com/kubernetes/kubernetes/issues/22262

  4. 我们还对即将发布的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的未来!有很多方式可以参与。如果您特别对可伸缩性感兴趣,您会对以下内容感兴趣:


1我们排除了对“事件”的操作,因为这些更像是系统日志,并且不是系统正常运行所必需的。
2这是Kubernetes GitHub存储库中的test/e2e/load.go。
3这是Kubernetes GitHub存储库中的test/e2e/density.go测试。
4我们正在考虑在下一版本中优化此问题,但目前使用较小的主节点可能会导致显著(数量级)的性能下降。我们鼓励任何对Kubernetes进行基准测试或尝试重现这些发现的人使用类似大小的主节点,否则性能会受到影响。