eBay应用程序集群管理器TESS.IO在大规模集群下的性能优化


【编者的话】Tess.IO是eBay的应用程序集群管理器,旨在为eBay大规模开发和运行应用程序提供世界一流的保障,使程序开发人员的工作更加高效、安全和灵活。随着越来越多的应用程序部署在Tess.IO集群上,集群的可扩展性及扩展后的良好性能变得越来越重要。本篇介绍了Tess.IO团队如何对大规模集群的性能进行卓有成效的优化。

开源容器集群管理器Kubernetes(通常称为“k8s”)是Tess.IO的上游平台,理论上,Kubernetes声称可以支持集群中5000个节点的运行,但在实际操作中却很难达到。原因在于:

其一,为了使Tess.IO集群更好地运行,我们在每个节点上部署了附加组件,例如,network agent(专为Pod配置网络的网络代理),beats(节点监控信息的心跳记录),和node-problem-detector(节点问题检测器)等,这些附加组件都需要与集群的控制平台进行交互。

其二,云本地的客户端(cloud-nativecustomer pods)会使用CustomResourceDefinition,这为集群添加了更多负载量。

本文用kubemark进行模拟实验,介绍了Tess.IO是如何在以上因素的限制下,依然能够实现流畅运行5000个节点的大规模集群的目标。如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Tess.IO集群架构

在讨论集群的可扩展性之前,首先要明确Tess集群的部署结构。(如下图)
1.jpg

为了实现99.99%的可靠性目标,我们在Tess.IO集群中部署了五个主节点(Master nodes)来运行Kubernetes核心服务(apiserver,控制器管理器,调度程序,etcd和etcdsidecar等)。

除核心服务外,每个节点还有Tess插件,用于公开指标,设置网络或收集日志。以上所有插件都会监控集群控制平台的资源,这给kubernetes控制平台带来额外的负担。

测试用例

在明确了集群架构以后,我们通过Kubemark进行集群的扩展性测试。

Step1

首先,明确Tess集群中当前部署的etcd/apiserver能否承受来自控制平台和系统守护进程(daemonsets)组件的压力。因为来自守护进程和kubelet的api负载会随着节点数的增加而增加,而且这是集群控制层面自身的压力,因此这是测试的基本操作。

Step2

然后,明确Tess集群在内置基础API负载下响应客户端变更的能力。例如,同时创建/删除1000个Pod,查看错误率,QPS和延迟的百分率。
2.png

Step3

此外,还应明确Tess集群能够承受多少来自客户端Pod的压力。例如,watcher的上限是多少,apiserver可以承担多少LIST请求。
3.png

4.png

模拟测试环境

除了测试用例本身,我们还需要模拟测试环境,以接近真实运行环境。

为了模拟5000个节点,我们在集群B中启动了5000个kubemark pod并将其注册到集群A的apiserver。(如下图)
5.jpg

这样执行有四个优势:
  • 可以在eBay数据中心节省5000个虚拟机
  • 使用kubernetes集群B(私有IP)可以轻松扩展5000个节点
  • 可以保存目标集群的公共IP(公共IP在eBay数据中心全局可路由)
  • 可以隔离kubemark节点和目标集群A之间的影响


此外,节点上其他组件的运行也需模拟,但只是抽象与apiserver的交互逻辑,并将其构建为容器添加到kubemarkpod中。由此,一个kubemark pod包含了一个模拟节点,以及该节点上的守护进程集群。

测试问题陈述

在运行上述测试时,出现了以下问题:

无法从故障中恢复

当有5k个节点,150k个Pod时。

每个主节点上etcd都会占用大量记忆存储(大约100GB)。
6.jpg

etcd频繁进行leader选举。
7.jpg

in the etcd log:
2018-07-02 17:17:43.986312 W | etcdserver: apply entries tooktoo long [52.361859051s for 1 entries]
2018-07-02 17:17:43.986341 W | etcdserver: avoid queries withlarge range/delete range!

apiservers容器每隔几分钟持续重启。

调度程序在大型集群中很慢

当集群中只有5k个节点,没有Pod的时候,调度1k个Pod大约需要20分钟,平均每个Pod需要1~2秒。然而,当有5k个节点和30k个Pod时,每个Pod的平均调度时间要长达1分钟。

大量查询会破坏集群

  • 在Tess.IO集群中,Pod是代表应用程序实体的重要资源,因此集群可扩展性的指标不仅要看节点数,还要看Pod数。
  • Pod信息对集群控制平台的组件和客户端应用程序十分重要,因为两者都需要进行Pod查询。
  • 随着应用程序数量的增加,容器数量将变得非常大,对Pod资源的LIST请求将成为LargeRange请求,大量的并发LIST请求可能完全占用kube-apiserver的缓冲区窗口,并对核心探测器产生影响。


etcd周期性改变leader

Tess.IO集群每半小时都会获取一次etcd快照,而etcd会在快照期间更改leader。

解决方案

一般来说,对于大规模集群,首先要将(max mutating in flight)参数请求设置为1000。对于5k个节点,如果只考虑kubelet的PATCH请求(节点每10秒汇报一次心跳),由于部署了5个master node,那么平均每个apiserver的每秒写操作数是100。

此外,NPD(Node-Problem-Detector)和其他组件也会同时PATCH节点,如果用于patch node的tcp长链接到5个apiserver实例的分布不均匀,或者某些apiserver宕掉,亦或是patch操作受到长的READ事务的影响,那么很容易触碰到默认200的inflight-limit,请求就会被拒绝(错误码429)。这样一来,请求就会被频繁重试,给apiserver带来更大的压力。

下面针对测试出现的四大问题各个击破

问题1. 无法从故障中恢复

在每个节点上,都会有一些守护进程用于配置网络、收集指标/日志或报告硬件信息。因此,除了kubelet以外,这些守护进程也会监视节点上的所有pods。

当大规模集群无法从故障中恢复,主要有三种可能:

No.1 守护进程覆盖ListOption

根本原因:

在默认的LIST/WATCH机制中,第一个请求是LIST调用,并且只会从apiserver cache中获取对象列表并不到达etcd。在注册自定义ListFunc和WatchFunc时,假如守护进程覆盖了ListOption,那么这些请求仍将穿透apiserver到达etcd,这样一来,如果重新启动apiserver或重新部署守护进程,所有pods的LIST请求都将命中etcd,这对于kube-apiserver将是一场灾难。

改进措施:

确保所有守护进程在自定义ListFunction的时候不会覆盖ListOption,这样就从apiserver缓存而不是通过etcd获取资源列表。

No.2 podwatchcache未准备好

根本原因:

每个节点上都有5个守护进程组件对pods发起LIST/WATCH请求,这些组件通过fieldSelector向pods发起问询(第一个LIST请求是通过resourceVersion=0和nodename=<nodeName>发送的)。

由于Pod数量非常大,因此podwatchcache准备就绪需要几秒钟,如果podwatchcache没有准备好,那么apiserver就会直接将这些LIST请求发送给etcd。此外,当apiserver重启时,所有podwatcher还会并行重新发送LIST请求,且依旧发送给etcd,这对于etcd来说是一个巨大的压力。

改进措施:

在监视到缓存未准备好时及时返回错误。(如下图)
8.png

No.3 apiserver过滤压力大

问题陈述:

缓存准备就绪后,所有LIST请求将再次并行发送给apiserver。虽然etcd集群的状态正常,但是压力转移到了apiserver。通过pprof可以看到,apiserver正忙于过滤那些Pod数量超过150k的节点上的Pod。(如下图)
9.jpg

根本原因:

apiserver过滤压力大,是因为GetAttrs从目标处理对象中获取字段和标签并将其设置为Golang地图,而设置地图是一项耗时的操作。在1.10之前的版本中,这项工作通常在filterloop中被调用,如果有150k个Pod,那么在每个使用fieldSelector/ labelSelector的LIST请求中,它将被调用15万次。

改进措施:

将GetAttrs移动到统一的位置,当把处理对象从etcd移到apiserver缓存时,GetAttrs就会获取到该对象的属性,并将属性和对象一起存储在缓存中。这样一来,apiserver和etcd就可以承受突发的压力了,比如服务器重启或用户重新同步LIST/WATCH请求。

问题2. 调度程序在大型集群中速度很慢

根本原因:

首先明确Pod在以下两种状态会拖慢程序的调度效率(即使只有一个Pod也会如此)。
  • Pod处于Terminating状态
  • Pod所在的Node丢失


在实际产品环境中,上述问题十分常见,因此改善这两种情况对调度大型集群非常重要。

改进措施:

  • 改进FilteredList()函数,因为该函数是慢速路径。
    • 将Mutex更改为RWMutex
    • 删除多次调用函数所产生的字符串连接
    • 避免较大的序列增长

  • 即使存在Pod所在的节点信息被删除,也要创建元数据,集群调度就会在上述情况下绕过慢路径。


改进原理:

引入元数据可以更好地用affinity/anti-affinity规则调度Pod。

通过使用satisfiesPodsAffinityAntiAffinity()函数查找集群中与affinity/anti-affinity规则所匹配的Pod,并存储为元数据,这样FilteredList()函数运行时只需要检查每个节点上可匹配规则的pods即可,从而大大缩短运行延迟。

satisfiesPodsAffinityAntiAffinity()函数写为:
satisfiesPodsAffinityAntiAffinity()- Checks if scheduling the pod onto this node would break any rules of this pod

改进后再测试:

基于5000个节点,170k个现存Pod(包括节点已被删除的处于Terminating状态的pods),我们创建1000个使用了anti-affinity规则的pod,然后测试其调度延迟。(如下图)
10.png

测试结果:

平均每秒调度15.2个Pod(未改进之前每个Pod需要7分钟)。

问题3. 大量的查询会破坏集群

根本原因:

Pod和节点是Tess.IO集群中最大的两种资源,但在etcd中存在全局锁,Pod和节点这两种资源会相互竞争全局锁,从而导致生产力降低。

此外,当Pod的LIST请求也在并行(命中etcd)时,节点的PATCH将受到影响,如果节点PATCH长时间不能成功,节点将变为NotReady状态,这对于集群是很危险的。

改进措施a:将Pod信息存储在单独的etcd里

考虑到节点的PATCH请求是集群运行中最频繁的请求,因此将Pods资源分离出专用的etcd,一方面可以避免影响节点的PATCH,另一方面也有助于扩展集群规模。

改进措施b:将Rate-Limit增至大型LIST请求

在集群测试中,如果同时创建/删除少于1000个Pod,那么apiserver或etcd就可以承受这种压力。然而,如果所有核心组件都使用SharedInformer/ ListWatch机制,控制平台甚至可以同时重启5个apiserver。此时,所有客户端发送的请求都会命中apiserver缓存而不是旁路apiserver缓存。

因此,将Rate-Limit增至大型LIST请求,通过旁路apiserver缓存,这样可以避免给etcd过多负载。
11.jpg

如上图所示,我们基于吞吐量设定Rate-Limit。

具体而言,记录访问比较频繁且大量的LIST请求的运行模式和运行成本(这些信息可以通过etcd接收,并从apiserver发送至客户端),然后根据已有记录来预测未来LIST请求的运行成本并执行Rate-Limit。

问题4. Etcd持续改变leader

根本原因:
12.png

etcd将raftproposal记录至WAL(write-aheadlogging)日志中(以维护数据一致性)。从上方贴出的etcd日志中可以看出,WAL同步的持续时间大于1秒,IO被占用,那么etcd集群的leader可能或错过心跳汇报,导致请求超时和leader丢失,那么其他followers就会开始发起新的leader选举。
13.png

如上图所示,所有“WAL同步时间较长”的时间点都与“etcd备份”的时间点完全匹配。

改进措施:将etcd数据备份到独立磁盘

用iostat测试etcd数据盘(下图中的sdb)的读写速率以及io使用率。在大多数情况下,每秒写入字节数为20MB/ s~60MB / s,但在每半小时快照状态中,每秒写入字节数约为500MB/ s,并且IO利用率高达100%。
14.png

根据上图,我们猜测这是由于刷新etcd备份文件引起的。由于目前etcd备份与etcd数据共享同一磁盘,测试时快照大小为4GB,etcd备份占用了所有磁盘IO,并影响etcd同步WAL日志的常规操作。

Tess.IO集群使用的内核版本是3.10.0-862.3.2.el7.x86_64,默认IO调度算法是deadline,而不是cfq(为每个队列分配访问磁盘的时间片),基于deadline的IO调度算法会加剧刷新备份占用磁盘IO的情况。
15.png

为了解决这一问题,我们获取快照并将其写入另一块与etcd数据盘不同的磁盘。这样一来, WAL同步时长的峰值消失了,也没有发生频繁进行leader选举的问题。

总 结

Kubernetes声称在一个集群中可以支持5000个节点,但在实际操作中,集群中的大量现有资源(例如Pod)、不同的部署结构和不同的API负载模式都会限制其可承载量的真实大小。

如今,经过调整和修复,eBay Tess.IO已经成功实现了大规模集群的性能优化,为程序开发人员提供更好的运行体验。

原文链接:https://mp.weixin.qq.com/s/znfLbETcyof-y49Xd3sn9w

0 个评论

要回复文章请先登录注册