当前位置: 首页 > 后端技术 > Node.js

当K8s集群达到10000规模时,阿里巴巴如何解决系统各组件的性能问题?

时间:2023-04-03 13:14:36 Node.js

本文主要介绍阿里巴巴在大规模生产环境中实施Kubernetes时,在集群规模上遇到的典型问题以及相应的解决方案。这些关键的增强是阿里巴巴内部拥有数万个节点的Kubernetes集群顺利支持2019天猫618大促的关键。背景从阿里巴巴最早的AI系统(2013年)开始,集群管理系统经历了多轮架构演进,2018年Kubernetes全面应用,这期间的故事非常精彩。有机会我单独给大家做一个。分享。这里忽略系统演进的过程,不讨论为什么Kubernetes能在社区和公司内部取胜,而是关注Kubernetes在应用过程中会遇到哪些问题,我们做了哪些重点优化。在阿里巴巴的生产环境中,容器化应用超过10k,全网有数百万个容器运行在数十万台主机上。支撑阿里巴巴核心电商业务的集群有十几个,最大的集群有数万个节点。在实施Kubernetes的过程中,我们在规模上遇到了很大的挑战,比如如何将Kubernetes应用到超大规模的生产层面。罗马不是一天建成的。为了了解Kubernetes的性能瓶颈,我们结合阿里生产集群的现状,在一个10k节点的集群中预估了预期规模:20w个pods100w个对象我们搭建了一个基于Kubemark的大规模集群模拟平台,使用容器启动多个(50)个Kubemark进程,并使用200个4c容器来模拟具有10k节点的kubelet。在模拟集群中运行常见的工作负载时,我们发现Pod调度等一些基础操作延迟非常高,达到了惊人的10s水平,集群处于非常不稳定的状态。当Kubernetes集群规模达到10k节点时,系统的各个组件都会出现相应的性能问题。比如etcd有大量的读写延迟,出现拒绝服务的情况。同时,由于空间限制,无法承载大量的KubernetesStore对象;APIServer查询pod/nodes有很高的延迟,并发查询请求可能会导致后端etcdoom;Controller无法及时感知APIServer的最新变化,处理延迟高;当出现异常重启时,服务恢复时间有时需要几分钟;Scheduler延迟高,吞吐量低,无法满足阿里业务的日常运维需求,更不能支持大促的极端场景。etcd的改进为了解决这些问题,阿里云容器平台在多方面下了功夫,提升了Kubernetes在大规模场景下的性能。首先是etcd层面。作为Kubernetes中存储对象的数据库,对Kubernetes集群的性能有着至关重要的影响。第一版的改进,我们通过将etcd数据dump到tail集群来增加etcd存储的数据总量。但是,这种方法有一个明显的缺点,那就是额外的Tair簇。运维复杂度的增加对集群中的数据安全提出了极大的挑战。同时其数据一致性模型不基于Raft复制组,牺牲了数据安全性。对于第二个版本的改进,我们将APIServer中不同类型的对象存储在不同的etcd集群中。从etcd的角度来说,对应的是不同的数据目录。通过将不同目录的数据路由到不同的后端etcd,减少单个etcd集群存储的数据总量,提高可扩展性。对于第三个版本的改进,我们深入研究了etcd的内部实现原理,发现影响etcd扩展性的一个关键问题在于底层bboltdb的页面分配算法:随着etcd存储数据量的增长,bbolt在db中线性查找“连续长度为n的页内存页”的性能明显下降。为了解决这个问题,我们设计了一种基于隔离hashmap的免费页面管理算法。hashmap以连续页面大小为key,以连续页面的起始页面id为value。通过检查这个隔离的hashmap,实现了O(1)的自由页面搜索,大大提高了性能。释放块时,新算法会尝试合并具有相邻地址的页面并更新隔离的哈希图。更详细的算法分析可以参考CNCF博客发表的博文:https://www.cncf.io/blog/2019/05/09/performance-optimization-of-etcd-in-web-scale-data-scenario/通过本次算法改进,我们可以将etcd的存储空间从推荐的2GB扩展到100GB,大大提高了etcd存储数据的规模,并且读写没有明显的延迟。此外,我们还与谷歌工程师合作开发了etcdraftlearner(如zookeeperobserver)/全并发读等特性,以增强数据安全性和读写性能。这些改进已贡献开源,并将在社区etcd3.4版本中发布。APIServerimprovementsEfficientnodeheartbeats在Kubernetes集群中,影响其向更大规模扩展的核心问题之一是如何高效地处理节点的心跳。在典型的生产环境(non-trival)中,kubelet每10s上报一次心跳,每次心跳请求的内容达到15kb(包括节点上的几十张图片,以及数条volume信息),这会带来两个大问题:心跳请求触发etcd中节点对象的更新。在一个10k节点的集群中,这些更新会产生近1GB/min的事务日志(etcd会记录变更历史);APIServerCPU消耗大,node节点非常Huge,序列化/反序列化开销非常大,处理心跳请求的CPU开销超过APIServerCPU时间的80%。为了解决这个问题,Kubernetes引入了一个新的内置LeaseAPI,从节点对象中剥离出与心跳密切相关的信息,也就是上图中的Lease。原本kubelet每10s更新一次节点对象,升级为:每10s更新一次Lease对象,表示节点的存活状态,NodeController根据Lease对象的状态判断节点是否存活;出于兼容性原因,减少为每60s更新一次节点对象,这样Eviction_Manager就可以继续按照原来的逻辑工作。因为Lease对象很小,所以更新它的代价要比更新节点对象低很多。通过这种机制,kubernetes显着降低了APIServer的CPU开销,同时大大减少了etcd中的大量事务日志,并成功地将其规模从1000个扩展到数千个节点。该功能在社区发布,Kubernetes-1.14默认开启。APIServer负载均衡在生产集群中,出于性能和可用性的考虑,通常会部署多个节点组成一个高可用的Kubernetes集群。但是在高可用集群的实际运行中,可能会出现多个API服务器之间负载不均衡的情况,尤其是在集群升级或者部分节点重启失败的情况下。这对集群的稳定性造成了很大的压力。本来打算通过高可用来分散APIServer的压力。折叠节点然后导致雪崩。下图是压力测试集群中的一个模拟案例。在三节点集群中,APIServer升级后,所有压力都集中在其中一个APIServer上,其CPU开销远高于另外两个节点。解决负载均衡问题,一个很自然的想法就是加一个负载均衡器。前面的描述中提到,集群中的主要负载是处理节点的心跳,所以我们在APIServer和kubelet之间添加了lb。典型的思路有两种:APIServer措施增加lb,所有kubelet都连接到lb。典型的云提供商交付的Kubernetes集群就是这种模式;kubelettest增加lb,lb选择API服务器。通过压测环境的验证,发现增加lb并不能很好的解决上述问题。我们必须深刻理解Kubernetes内部的通信机制。深入研究Kubernetes发现,为了解决tls连接认证的开销,Kubernetes客户端做了很多努力来保证“尽可能复用同一个tls连接”。大多数情况下,clientwatcher工作在下层的同一个tls连接上,而只有当这个连接发生异常时,才可能触发重连,然后切换APIServer。结果就是我们看到的,当kubelet连接到其中一个API服务器时,基本上没有发生负载切换。为了解决这个问题,我们从三个方面进行了优化:APIServer:认为客户端是不可信任的,需要保护自己不被过载的请求压垮。当自身负载超过阈值时,发送409——请求过多,提醒客户端退避;当自身负载超过较高阈值时,通过关闭客户端连接来拒绝请求;客户端:一段时间内频繁收到409同时尝试重建连接,切换APIServer;周期性重建连接并切换APIServer完成shuffle;在运维层面,我们通过设置maxSurge=3来升级APIServer,避免升级过程带来的性能抖动。如上图左下角监控图所示,增强版可以基本平衡APIServer的负载,同时可以在两个节点出现故障时快速自动恢复到平衡状态。重启(图中抖动)。List-Watch&CacherList-Watch是Kubernetes中Server和Client之间的核心通信机制。etcd中的所有对象及其更新的信息,APIServer内部使用Reflector观察etcd的数据变化并存储在内存中,而在controller/kubelets客户端也通过类似的机制订阅数据变化。List-Watch机制面临的一个核心问题是,当Client和Server的通信断开时,如何保证重连时数据不丢失。这是在Kubernetes中通过全局递增的版本号resourceVersion实现的。如下图所示,当前同步的数据版本保存在Reflector中。重连时,Reflector将自己的当前版本通知给Server(5),Server根据内存中记录的最近更改历史计算出客户端所需数据的起始位置(7)。这一切看似非常简单可靠,但是……在APIServer内部,每一种类型的对象都会存储在一个叫做storage的对象中,比如会有:PodStorageNodeStorageConfigmapStorage……每一种类型的存储都会有存储对象最新变化的有限队列,以支持watcher的一定滞后(重试等场景)。一般来说,所有类型的类型共享一个递增的版本号空间(1,2,3,...,n),即如上图所示,pod对象的版本号只保证递增而不是连续的。客户端在使用List-Watch机制同步数据时,可能只关注部分pod。最典型的kubelet只关注与自己节点相关的pod。如上图所示,某kubelet只关注绿色的pod(2,5)。因为存储队列是有限的(FIFO),所以当pod更新时,旧的更改将从队列中逐出。如上图所示,当队列中的更新与某个Client无关时,Client的进度保持在rv=5。如果Client在5被淘汰后重连,APIServer无法判断5的值和当前队列的最小值(7)之间是否有客户端需要感知的变化,所以返回Clienttoooldversionerr触发客户端重新列出所有数据。为了解决这个问题,Kubernetes引入了watch书签机制:书签的核心思想是在客户端和服务端之间保持一个“心跳”。即使队列中没有客户端需要感知的更新,反射器内部的版本号也需要及时更新。如上图所示,Server会在合适的时候推送当前最新的rv=12版本号给Client,让Client版本号跟上Server的进度。bookmark可以将APIserver重启时需要重新同步的事件数减少到原来的3%(性能提升了数十倍)。该功能由阿里云容器平台开发,已发布至社区Kubernetes-1.15版本。Cacher&Indexing除了List-Watch,另一种客户端访问方式是直接查询APIServer,如下图所示。为了保证客户端在多个API服务器节点之间读取到一致的数据,API服务器会通过获取etcd中的数据来支持客户端的查询请求。从性能的角度来看,这会带来几个问题:无法支持索引,需要先获取集群中的所有pod,才能查询节点的pod。这个开销很大;因为etcd的request-response模型,单个请求查询过大的数据会消耗大量的内存。通常APIServer和etcd之间的查询会限制请求的数据量,通过分页的方式完成大量的数据查询。显着减少了寻呼带来的多次往返。表现;为了保证一致性,APIServer查询etcd都是使用Quorumread,这个查询开销是集群级别的,无法扩展。为了解决这个问题,我们设计了APIServer和etcd之间的数据协调机制,保证Client可以通过APIServer的缓存获取一致的数据。原理如下图所示,整体流程如下:t0时刻,Client查询APIServer;APIServer请求etcd获取当前数据版本rv@t0;APIServer请求进度更新,等待Reflector数据版本达到rv@t0;通过缓存响应用户请求。这种方式并没有破坏Client的一致性模型(有兴趣的可以自己演示),同时我们可以通过缓存灵活的增强响应用户请求时的查询能力,比如支持namespace节点名/标签索引。本次增强大大提升了APIServer的读请求处理能力。在10000个单元的集群中,典型的describenode时间从5s减少到0.3s(触发节点名称索引),其他查询操作如getnodes的效率也翻倍。控制器故障转移在一个10k节点的生产集群中,控制器存储了将近一百万个对象。从APIServer获取和反序列化这些对象的开销不容忽视。当Controller重新启动并恢复时,可能需要几分钟才能完成此任务。工作,这对于阿里巴巴这样规模的企业来说是不能接受的。为了减少组件升级对系统可用性的影响,我们需要尽量减少单个控制器升级对系统的中断时间。这里采用下图所示的方案来解决这个问题:预启动backupcontrollerinformer,提前加载controller。数据;当Master控制器升级时,会主动释放LeaderLease,触发Standby立即接管工作。通过该方案,我们将控制器的中断时间降低到秒级(升级时<2s)。即使在异常宕机的情况下,备份也只需要等待leaderlease(默认15s)到期,不需要花费数分钟重新同步数据。通过此次增强,controller的MTTR显着降低,同时controller恢复时对APIServer的性能影响也降低了。该方案也适用于调度程序。定制调度器由于历史原因,阿里巴巴的调度器采用自研架构。由于时间关系,本次分享没有展开调度器的增强。这里只分享两个基本思路,如下图所示:等价类:一个典型的用户扩容请求是一次扩容多个容器,所以我们通过将待处理队列中的请求划分为等价类来实现批处理,显着减少谓词/优先级的数量;Relaxedrandomization:对于单个调度请求,当集群中有很多候选节点时,我们不需要评估集群中的所有节点。选择足够多的节点后,我们就可以进入后续的调度Processing(通过牺牲求解精度来提高调度性能)。总结通过一系列的增强和优化,阿里巴巴已经成功地将Kubernetes应用到生产环境中,并达到了单集群10000节点的超大规模,包括:通过索引与数据分离和数据分片来增加etcd的存储容量,以及最后通过改进etcd底层bboltdb存储引擎的block分配算法,大幅提升etcd在存储大量数据场景下的性能,单个etcd集群支持大规模Kubernetes集群,大大提高简化了整个系统架构的复杂性;通过实现KubernetesLightweightheartbeat,改进了HA集群下多个APIServer节点的负载均衡,在ListWatch机制中增加了书签,通过索引和缓存改善了大规模Kubernetes集群中最头疼的List性能瓶颈,使得数十个稳定运行数千个节点的集群成为可能;通过双机热备,大大缩短了主备切换时控制器/调度器的服务中断时间,提高了整个集群的可用性;阿里巴巴自研调度器性能优化最有效的两个思路是:等价类处理和随机松弛算法。通过这一系列的功能提升,阿里巴巴内部核心业务成功运行在数万节点的Kubernetes集群上,并经受住了2019年天猫618大促的考验。作者简介:曾繁松(花名:朱玲),阿里云云原生应用平台资深技术专家。丰富的分布式系统设计和开发经验。在集群资源调度领域,其负责的自研调度系统已管理数十万节点,在集群资源调度、容器资源隔离、不同工作负载混合等方面具有丰富的实践经验。目前主要负责Kubernetes在阿里巴巴内部规模化落地,将Kubernetes应用于阿里巴巴内部核心电商业务,提升应用发布效率和集群资源利用效率,稳定支撑2018双十一和2019618促销。本文作者:曾繁松阅读原文本文为云栖社区原创内容,未经允许不得转载。