6

Kubernetes Node规模突破7500

 3 years ago
source link: http://dockone.io/article/1721164
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【编者的话】2018年1月OpenAI 官方博客称,他们已将 Kubernetes 集群扩展到 2500 个节点。时隔三年,在2021年1月,OpenAI 官方博客再度宣布 Kubernetes 集群扩展到 7500 个节点,目前不仅可以满足 GPT-3、CLIP 和DALL·E 等大型模型的需求,而且也可以服务于快速的小规模迭代研究。下面文章来自于OpenAI 官方博客,描述了走向这个7500节点规模过程中遇到的问题和解决办法,以及对于未来走向的畅想。

yUzUF33.png!mobile

我们的Kubernetes集群规模已经上升到7,500个节点,主要为诸如 GPT-3CLIPDALL·E 等大型训练模型提供可扩展的基础架构,而且还可用于小规模快速迭代研究,例如 神经语言模型的标度律 等。将单个Kubernetes集群扩展到如此规模很难完成,同时在这个过程中需要格外小心。但好处是借助这种简单的基础架构使得我们的机器学习研究团队无需更改其代码就可以快速扩容。

自上一篇有关 [扩展到2,500个节点] 的文章发表以来,我们一直在不断扩展基础架构以满足研究人员的需求,在此过程中我们还学到了很多经验。这篇文章对此作了总结,以便Kubernetes社区共同受益,最后介绍我们仍然要面对的问题以及解决办法探讨。

工作负载

在我们深入讨论之前,介绍一下我们的工作负载是很重要的。我们运行Kubernetes软硬件和您在公司的情况可能不太一样。我们的问题和相应的解决方案可能是,也可能不是,也请您视情况而应用!

大型机器学习作业跨越许多节点,并且只有当可以访问每个节点上的所有硬件资源时,才能最大化运行效率。如此一来,GPU 就可以通过 NVLink 直接进行交叉通信,或者 GPU 也可以通过 GPUDirect 直接与NIC通信。因此,对于我们的许多工作负载,一个节点上只放置一个 Pod。任何NUMA、CPU或PCIE资源争用都不是调度的因素,因此装箱调度或碎片化不是一个常见的问题。我们现有的集群拥有完整的对分带宽,因此也无需考虑任何机架或网络拓扑。所有这些都表明,我们的 Kubernetes 拥有许多节点,但是调度的压力相对较低。

不过,kube-scheduler 上经常会出现峰值压力。一个新的Job可能包含数百个一次性创建的Pod,但具有较低的使用率。

nQJFneU.png!mobile

我们最大的Job上运行着 MPI 协议(消息传递接口协议),该Job内的所有Pod都加入了同一个 MPI 通信器。如果某个 Pod 宕机,则整个Job都将暂停,需要重新启动。我们会定期保存检查点,Job重启时会从上一个检查点恢复。因此,可以认为 Pod 是半状态化的,终止的 Pod 可以被替换掉,而且Job还可以继续,但是这种做法会干扰正常的Job,应尽量减少。

由于 HTTPS 通道流量很少,也不需要进行A / B测试、蓝色/绿色或金丝雀部署,我们没有完全依赖 Kubernetes 进行负载均衡。Pod 之间通过 SSH(而不是服务端点),利用 IP 地址直接通过 MPI 相互通信。我们的服务“发现”功能很有限,一般只需要在Job启动的时候执行一次查找去找到 MPI 中的 Pod。

我们的大多数Job都使用了某种形式的 Blob 存储。通常,它们会直接从 Blob 存储,以流的形式读取数据及或检查点的某些分片,或将其缓存到临时的本地磁盘。在需要 POSIX 语义的时候,我们也使用了一些持久卷,但是Blob存储更容易扩展,而且不需要缓慢的分离/附加操作。

最后要提醒,我们的工作大多是基于研究性质的,这意味着负载本身在不断变化。尽管超算团队努力提供了生产级别的计算基础架构,但集群上运行的应用程序的生命周期很短,而且开发人员的迭代非常快。新的使用模式随时可能出现,因此我们很难预料发展趋势,并做出适当的折中。我们需要一个可持续发展的系统,以便在事情发生变化时迅速做出响应。

网络

由于集群内的Node数和 Pod 数不断增长,我们发现 Flannel 难以扩展到所需的吞吐量。于是,我们转而使用原生 Pod 网络技术来管理 Azure VMSSes 的 IP配置和相关的 CNI 插件。这样我们的 Pod 就能够获得宿主级别的网络吞吐。

我们最大的集群上大约有20万个IP地址正在使用中,在测试基于路由的 Pod 网络时,我们发现可以有效利用的路由数量受到了严重限制。因此我们改用基于别名的 IP 寻址。

避免封装增加了对底层SDN或路由引擎的要求,但它使我们的网络设置保持简单。无需任何额外的适配器就可以添加VPN或隧道。我们不需要担心数据包分片,因为网络的某些部分MTU较低。网络策略和流量监控也很简单;数据包的源和目的地不存在歧义。

我们在宿主上使用iptables来跟踪每个命名空间和Pod上网络资源的使用情况。这样研究人员就可以可视化网络的使用情况。具体来说,因为许多实验的互联网和Pod间通信都有独特的模式,所以能够调查何处可能出现瓶颈是非常必要的。

iptables 的 mangle 规则可以给任何符合特定规则的数据包做标记。我们采用了以下规则来检测流量属于内部还是发向外网。FORWARD 规则负责 Pod 间的流量,而 INPUT 和 OUTPUT 负责来自宿主的流量:

iptables -t mangle -A INPUT ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-in"

iptables -t mangle -A FORWARD ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-in"

iptables -t mangle -A OUTPUT ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-out"

iptables -t mangle -A FORWARD ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-out"

做好标记后,iptables 就会统计符合该规则的数据包的字节数。使用 iptables命令就可以看到这些统计结果:

% iptables -t mangle -L -v

Chain FORWARD (policy ACCEPT 50M packets, 334G bytes)

pkts bytes target     prot opt in     out     source               destination

....

1253K  555M            all  --  any    any     anywhere            !10.0.0.0/8           /* iptables-exporter openai traffic=internet-out */

1161K 7937M            all  --  any    any    !10.0.0.0/8           anywhere             /* iptables-exporter openai traffic=internet-in */

我们使用了一个名为 iptables-exporter 的开源 Prometheus 导出程序,将这些跟踪信息导出到监控系统中。这样就可以直接跟踪符合各种条件的数据包了。

B7fAVf7.png!mobile

我们的网络模型的独特之处在于,Node、Pod 和服务网络的 CIDR 范围是完全暴露给研究者的。网络采用了轮辐模型,使用原生节点和 Pod 的 CIDR 范围进行路由。研究者连接到中央枢纽,从那里可以访问到任何集群。但是两个集群之间不能互相通信。这样可以保证每个集群都是隔离的,不会出现跨集群依赖(否则会破坏故障隔离原则)。

我们使用一个“NAT”宿主对来自集群外部的流量进行 CIDR 范围转译。这种结构可以让研究人员自由地选择使用何种网络配置以及怎样使用,以满足实验的需要。

API Servers

对于健康工作的集群来讲, API Servers和 etcd 是Kubernetes 的关键组件,所以我们特别关注这些组件。我们采用了 kube-prometheus 提供的 Grafana 仪表板,以及自己设计的仪表板。我们发现,针对API Servers上发生的 HTTP 429(Too Many Requests)和5xx(Server Error)发送高级别报警非常有效。

b6BRZjN.png!mobile

虽然许多人在 Kubernetes 内部运行API Servers,但我们选择了在集群外部运行。etcd 和 API Servers都运行在独立的节点上。最大的集群运行了 5 个 API Servers和5个 etcd 节点,并以分散负载减小宕机造成的影响。自从将 Kubernetes Events 分离到单独的 etcd 集群上以后,就再也没有出现过因 etcd问题导致的故障。API Servers是无状态的,因此只需要运行一个自我修复的实例组或 scaleset 就可以。我们没有尝试过针对 etcd 集群构建自我修复自动化,因为它极少出故障。

API Servers占用的内存相当多,而且内存占用会随着集群中的节点数量增加而呈线性增长。对于我们拥有 7500 节点的集群,每个 API Servers上的堆空间占用最多为 70GB,还好这依然在硬件能够承受的范围内。

vqMJZfJ.png!mobile

API Servers上比较大的压力之一就是端点上的 WATCH。有几个服务的服务对象是集群中的所有成员,如 kubelet、node-exporter 等。每当集群中添加或删除节点时,就会触发WATCH。而且由于每个节点自身都会通过 kube-proxy 监视 kubelet 服务,这些服务的响应数量和所需带宽就会呈 N^2 增长,大约每秒增加 1GB。Kubernetes1.17 中发布的 EndpointSlices 极大地缓解了这个压力,它将负载降低了 1000 倍。

2Yj6jeJ.png!mobile

一般而言,我们会注意任何API Servers请求数量随着集群大小而变化的情况。我们会尽量避免让任何 DaemonSet 与 API Servers交流。如果需要让每个节点监控变化,那么引入中间缓存服务(如DatadogCluster Agent)或许是避免集群范围瓶颈的好办法。

随着集群的增长,我们的自动伸缩越来越少了。但偶尔也会出现大幅自动伸缩的情况。新的节点加入集群会产生许多请求,而一次性增加几百个节点会超过 API Servers能够承受的容量。平滑请求速度,甚至仅仅增加几秒钟,就可以有效地避免这个问题。

使用Prometheus和Grafana测量时序列度量

我们使用 Prometheus 收集时序列度量,利用 Grafana 绘制成图表、显示仪表板并生成警告。首先我们部署了 kube-prometheus 来收集各种度量和可视化的仪表板。随着时间的推移,我们已经添加了许多我们自己的仪表板、指标和警报。

随着节点越来越多,我们逐渐难以理解 Prometheus 收集到的度量。尽管kube-prometheus 公开了许多非常有用的数据,但有些数据我们并不需要,而有些数据过于细致,很难收集、存储和有效地查询。因此我们使用 Prometheus 规则 “放弃”了一些度量。

长期以来,有一个问题一直困扰我们:Prometheus 消耗的内存越来越多,最终由于内存耗尽而崩溃。即使给 Prometheus 提供大量的内存也无济于事。更糟糕的是,每当出现崩溃,它就需要花费好几个小时重新执行预写式日志(write-ahead log)文件,之后才能正常使用。

最后我们 研究了 Prometheus 的源代码 ,发现内存耗尽是由于Grafana和Prometheus之间的交互导致的,Grafana会使用Prometheus上的 /api/v1/series 这个 API,进行 {le!=""} 的查询(含义是“获取所有直方图的度量”)。而 /api/v1/series 的实现在运行时间和空间上都没有任何限制,如果查询结果过多,就会消耗越来越多的内存和时间。即使请求者放弃请求并关闭连接,查询也会继续执行。对于我们的情况而言,无论多少内存都不够,Prometheus 最终总会崩溃。于是,我们给 Prometheus 打了补丁,将这个 API 包裹在一个 Context 中以实现超时,终于修复了该问题。

虽然 Prometheus 的崩溃次数大大减少了,但我们依然需要经常重启,因此预写式日志(简称WAL)的重新执行依然是一个问题。重新执行所有 WAL 通常需要花费好几个小时,之后 Prometheus 才能启动,并开始收集度量和查询请求。在 Robust Perception 的帮助下,我们发现设置 GOMAXPROCS=24 可以极大地改善这个问题。因为 Prometheus 会在执行WAL期间尝试使用所有 CPU 核心,对于核心数量极多的服务器而言,核心之间的竞争会导致性能大幅度下降。

我们正在探索新的选项,以增加我们的监测能力,如下面的“ 未解决的问题 ”一节所述。

健康检查

面对如此庞大的集群,我们必须依赖自动化来检测并移除任何有问题的节点。慢慢地,我们建立起了一系列健康检查系统。

被动健康检查

一些健康检查是被动的,永远在节点上运行。这些健康检查会监视基本的系统资源,如网络不通畅、磁盘失败、磁盘写满或GPU错误等。GPU会呈现多种错误,但最常见的就是“Uncorrectable ECC error”(无法修复的ECC错误)。Nvidia的Data Center GPU Manager (DCGM)工具可以帮助查询该错误,以及许多其他的“Xid”错误。跟踪错误的方法之一就是使用 dcgm-exporter 工具将度量导出到Prometheus监视系统中。这样就可以创建DCGM_FI_DEV_XID_ERRORS度量,其内容为最近发生过的错误代码。此外, NVMLDevice Query API 还可以提供有关 GPU 的健康情况和操作的更详细信息。

检测到错误之后,通常重启就能修复 GPU 或系统,尽管有些情况下需要更换显卡。

另一种健康检查会跟踪来自上游云服务提供商的维护事件。每个主流云服务提供商都会提供一种方法,获知当前使用的VM是否即将维护,从而导致服务中断。VM 可能需要重启,因为需要给监视程序打补丁,或者给物理服务器更换硬件。

这些被动健康检查在所有节点的后台不断运行。如果运行状况检查开始失败,将自动隔离该节点,这样就不会在该节点上调度新的pods。对于更严重的健康检查失败,我们还将尝试终止pod,请求所有当前运行的pod立即退出。它仍然取决于pod本身,通过pod中断预算进行配置,以决定它是否希望允许这种终止发生。最终,在所有pod终止或7天过去(我们SLA的一部分)之后,我们将强制终止VM。

主动 GPU 测试

不幸的是,并非所有的 GPU 问题都能从 DCGM 中看到错误码。我们自己构建了GPU测试库,能够捕获额外的错误,确保硬件和驱动程序按照预期运行。这些测试无法在后台运行,因为运行测试需要独占 GPU 几秒钟或几分钟。

首先,我们会在节点启动时运行测试,称为“预运行”。所有加入集群的节点都会加上 “preflight” 污染并打标签。该污染可以防止普通 Pod 被调度到节点上。然后配置一个 DaemonSet,在所有带有该标签的 Pod 上运行预运行测试。测试成功后,测试程序会移除污染,节点就可以正常使用了。

我们还会在节点的生命周期内定期执行测试。测试通过 CronJob 运行,因此可以在集群中的任何可用节点上执行。虽然这样无法控制测试在哪个节点上运行,但我们发现,只要时间足够长,它就能提供足够的测试覆盖,同时不会对服务造成太多干扰。

配额和资源利用

当我们扩大集群时,研究人员开始发现他们很难获得分配给他们的所有能力。传统的Job调度系统有很多不同的特性,可以在竞争团队之间公平地运行工作,而Kubernetes没有这些特性。随着时间的推移,我们从这些Job调度系统中获得了灵感,给 Kubernetes 添加了几个原生功能。

Team taints

我们在每个集群都有一个服务 “team-resource-manager”,它具有多种功能。它的数据源是一个ConfigMap,它为在给定集群中有能力的所有研究团队指定元组(节点选择器、要应用的团队标签、分配数量)。它与集群中的当前节点保持一致,从而设置适当数量的节点

openai.com/team=teamname:NoSchedule.

“team-resource-manager”还具有一个admission webhook服务,例如,当提交每个作业时,会根据提交者的团队成员申请相应的容忍度。使用taints允许我们灵活地约束Kubernetes pod调度程序,例如允许对较低优先级的pod有“any”容忍度,这允许团队在不需要重量级协调的情况下借用彼此的能力。

CPU & GPU balloons

除了使用 cluster-autoscaler 来动态伸缩集群之外,我们还会删除并重新添加集群内的不健康节点。实现方法是将集群的最小尺寸设置为零,最大尺寸设置为可用的容量。但是,如果 cluster-autoscaler 看到空闲节点,就会尝试将集群收缩至必要限度大小。从许多角度来看(VM 的启动延迟、预分配的成本、对API服务器的影响)来看,这种空闲状态的伸缩并不理想。

所以,我们同时为仅支持 CPU 的宿主和支持 GPU 的宿主引入了气球部署。该部署包含一个 ReplicaSet,其中设置了低优先级 Pod 的最大数量。这些 Pod 会占用一个节点内的资源,所以自动缩放器就不会认为该节点闲置。但是由于这些 Pod 优先级很低,因此调度器可以随时将其驱逐,给真正的作业腾出空间。(我们选择了使用部署而不是 DaemonSet,避免 DaemonSet 在节点上被认为是闲置负载。)

需要注意的一点是,我们使用了 Pod 反亲和性来保证 Pod 最终会均匀地分布到节点上。Kubernetes 早期版本的调度器在处理 Pod 反亲和性时的性能为O(N^2),不过这一点在1.8版本后就修正了。

有问题的调度

我们的实验经常涉及到一个或多个StatefulSet,每个负责训练作业的一部分。至于优化器,研究人员要求所有的StatefulSet都被调度,训练作业才能完成(因为我们经常使用MPI来协调优化器的各个成员,而MPI对于组内成员数量的变化非常敏感)。

但是,Kubernetes 默认并不一定会优先满足某个 StatefulSet 的所有请求。例如,如果两个实验都要求100%的集群容量,那么 Kubernetes 不一定会调度某个实验的所有 Pod,而是可能会为每个实验调度一半的 Pod,导致死锁状态,每个实验都无法完成。

我们尝试了几种方案,但都遇到了一些极端情况,会与正常 Pod 的调度产生冲突。Kubernetes 1.18 为核心调度器引入了一个插件架构,因此添加功能变得非常容易了。我们最近刚刚发布了Coscheduling plugin,以解决这个问题。

未解决的问题

随着我们的 Kubernetes 集群不断扩大,还有许多问题有待解决。一些问题包括:

度量

在目前的规模下,Prometheus 自带的 TSDB 存储引擎有许多问题,例如速度很慢、重启时需要很长时间重新执行 WAL(预写入日志)等。查询也很容易导致“查询可能会加载过多数据”的错误。我们正在迁移到与 Prometheus 兼容的另一个存储和查询引擎上。

Pod 网络流量

随着集群的扩大,每个 Pod 都会占用一定的互联网带宽。因此,每个人的互联网带宽加起来就无法忽略不计了,我们的研究人员有可能无意间给互联网的其他部分带来不可忽略的资源压力,例如下载数

总结

我们发现Kubernetes对于我们的研究需求来说是一个非常灵活的平台。它有能力扩大规模,以满足最苛刻的工作负载。不过,Kubernetes还有很多需要改进的地方,OpenAI的超级计算团队将继续探索Kubernetes如何扩展。

原文链接: Scaling Kubernetes to 7,500 Nodes 翻译:张亚龙


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK