OpenAI关于Kubernetes集群近万节点的生产实践

2,573 阅读15分钟

OpenAI已经将Kubernetes集群规模扩展至7500个节点,为大型神经网络模型(如GPT-3,CLIP和DALL·E)及小型实验性研究提供了可扩展的基础架构。 很少将单个Kubernetes集群扩展到如此规模,为此进行了一些必要的改进,但好处是单一的基础架构使我们的机器学习研究团队可以在不修改代码的前提下,快速扩展以缩短实验时间、加速研发进度。

自上一篇有关扩展到2500个节点的文章以来,我们一直在不断扩展基础架构以满足研究人员的需求,并在此过程中学习了许多其他相关知识。 该篇文章总结了相关经验,以便Kubernetes社区中的其他人可以从中受益,接下来介绍,需要解决的问题。

一、工作负载

首先需要说明的是,针对工作负载,我们在Kubernetes集群上运行的应用程序和硬件与其他公司中的场景完全不同。我们面临的问题和相应的解决方案可能与读者所处的实际场景不是太一致。

大型的机器学习作业可以访问多个节点,及每个节点上的所有硬件资源,因此运行效率最高。允许GPU使用NVLink进行交叉通信,或者GPU使用GPUDirect与NIC通信。因此,对于我们的许多工作负载,单个pod占据了整个节点,因此调度不涉及任何NUMA,CPU或PCIE资源抢占。当前的集群具有完整的双向带宽互通,因此无需考虑任何网络拓扑。因此,调度程序的压力相对较低。

因为一个新的任务可能包含数百个Pod调度的需求,kube-scheduler存在毛刺现象。

最大的job是运行MPI(并行计算),job中的所有Pod都工作在同一个MPI通信器中。任何Pod的消亡,都会导致整个job暂停,并重新启动。job定期备份相关信息(即checkpoint),在重新启动时从最近的备份信息处恢复。

我们不完全依赖Kubernetes进行负载平衡。我们的七层流量很少,因为不需要进行A / B测试,蓝绿升级或金丝雀发布等。 Pod通过SSH与其他Pod的MPI直接通信(这部分貌似有点疑问),而不是service endpoint。服务发现功能相对有限,因为我们只执行一次查找,即在工作启动时(pod刚参与MPI时)。

大多数job都与Blob类型存储进行交互,通常直接向Blob传输一些数据集的分片,或将其缓存到本地盘。我们也使用了一些PersistentVolumes,但是blob类型存储具有更好的伸缩性,并且不需要挂载、卸载操作。

超级计算团队努力致力于提供生产级别的计算基础架构,当前在该集群上运行的应用寿命较短,开发人员正在快速迭代中。任何时候都有可能出现新的应用场景,这需要我们对趋势进行预判,并做出适当折衷的设想。


二、网络

随着集群中节点和Pod数量的增加,我们发现Flannel难以满足需求。转而使用主机pod网络技术进行Azure VMSSes和相关CNI插件的IP配置。这使我们能够在Pod上获得主机级别的网络吞吐量。

我们改用基于别名的IP寻址的另一个原因是,在我们最大的集群上,我们可能随时有大约200,000个IP地址正在使用。在测试基于路由的Pod网络时,我们发现路由数量存在明显的限制。

改造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 -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 */

我们使用基于Prometheus的iptables-exporter的方案,然后将其接入到我们的监控系统。

我们网络模型的一个特别的地方是,我们向研究人员公开了节点,容器和服务网络CIDR范围。 我们有一个辐射状网络模型,并使用本机节点和Pod CIDR范围来路由该流量。 研究人员连接到中枢节点,从那里可以访问任何单个集群。 但是集群本身无法相互通信。 这样可以确保集群间相互隔离,且没有跨集群的依存关系以破坏隔离(译者表示...)。

我们使用主机 NAT来转换服务网络CIDR,以处理来自集群外部的流量。 这种设置使我们的研究人员在选择实验方式和选择哪种网络配置上具有极大的灵活性。


三、API Server

Kubernetes的API Server和etcd集群是集群健康运行的关键组件,因此我们特别注意这些系统上的压力。 我们使用kube-prometheus项目提供的Grafana以及其他内部仪表板。 我们发现针对API Server的HTTP(如429、5xx等状态)告警还是很有效的。

尽管大多数人在k8s集群内运行API Server,但我们选择在集群外运行。 etcd和API Server服务都在它们自己的专用节点上运行。 我们最大的集群运行了5个API Server和5个etcd节点,以分散负载并最大程度地降低影响(如果其中一台发生故障)。 自从我们在上一篇文章中将Kubernetes Events写入到其他etcd集群以来,我们在etcd方面没有遇到任何麻烦。 API Server是无状态的,通常很容易在自愈实例组或规模集中运行。 我们尚未尝试建立etcd集群的任何自愈等自动化功能。

API Server会占用相当大的内存,并且会随着集群中节点的数量线性上升。 对于具有7500个节点的集群,我们观察到每个API Server最多使用了70GB。

API Server上的另一大压力是API上的WATCH能力,例如kubelet node-exporter。 当从集群中添加或删除节点时,将触发此WATCH。 并且由于通常每个节点本身都通过kube-proxy监视kubelet服务(译者:可通过本地LB优化,并分配固定几个Master),因此这些响应所需的带宽为节点的二次方,有时甚至达到1GB / s或更高。 在Kubernetes 1.17中的EndpointSlices特性带来巨大的优化,使此负载降低了1000倍。

通常,我们密切关注任何随集群大小扩展的API Server请求。 我们尝试避免让任何DaemonSet与API Server进行交互。 在确实需求更改所有节点的监控组件时,引入中间缓存服务(例如Datadog Cluster Agent)似乎成了一种避免集群范围瓶颈的最佳实践。

随着集群数量的增长,我们对集群的自动伸缩操作逐步减少。 有时自动伸缩超标时,我们就会遇到麻烦。 当新节点加入集群时,就会产生许多请求,并且一次添加数百个节点可能会使API Server服务过载。


四、监控

我们使用Prometheus收集指标,并使用Grafana配置图形界面,管理仪表板和警报。我们从部署kube-prometheus项目开始,该项目收集各种指标,并提供良好的仪表板以完成可视化。随着时间的推移,我们添加了许多自己特有的仪表板,指标和警报。

随着节点日益增多,我们发现Prometheus收集的大量指标毫无用处。尽管kube-prometheus公开了许多有用的数据,但其中有部分我们从未使用过。我们使用Prometheus接口删除其中的某些指标。

一段时间以来,我们一直在努力解决一个问题,即Prometheus会消耗越来越多的内存,直到最终OOM。即使在设置了超大内存容量之后,这种情况似乎仍会发生(译者:该问题应该是发生在旧版本)。更糟糕的是,当它崩溃时,启动后需要花费很多时间进行恢复。

最终,我们找到了这些OOM的来源,是Grafana和Prometheus之间的交互,其中Grafana调用Prometheus接口/api/v1/series查询。 /api/v1/series接口获取所有监控指标,这将带来内存的持续增长。我们改进了Prometheus,使其在Context中包含此超时控制。

虽然Prometheus崩溃的频率降低了很多,但在确实需要重新启动它的时候,WAL恢复仍然是一个问题。在Prometheus收集新指标和为查询提供服务之前,通常需要花费很长时间来恢复所有WAL日志。在Robust Perception的帮助下,我们发现通过配置GOMAXPROCS = 24进行优化。 Prometheus会在WAL重放期间尝试使用所有内核,而对于具有大量内核的服务器来说,抢占会削减性能。


五、健康检查

对于规模如此大的集群,当然需要依靠自动化来检测和删除集群中行为异常的节点。 随之逐步深入,我们已经建立了一套完善的健康检查系统。

a. 被动检查

(译者:可以将之称为性能监控)某些运行状况检查是被动的,始终在所有节点上运行。它们监视基本的系统资源,例如网络可达性,磁盘损坏或磁盘已满或GPU错误等。 GPU会出现多种不同的问题,但一个比较常见的错误是无法纠正的ECC错误。 Nvidia的数据中心GPU管理器(DCGM)工具使查询此错误和许多其他Xid错误变得容易了许多。我们跟踪这些错误的一种方法是通过dcgm-exporter将指标抓取到我们的监控系统Prometheus中。其为DCGM_FI_DEV_XID_ERRORS指标。此外,NVML设备查询API公开了有关GPU的运行状况和操作的详细信息。

一旦我们检测到错误,通常可以通过重置GPU或系统来修复它们。

健康检查的另一种形式是跟踪来自上游云提供商的维护事件。大多数云提供商都提供了一种方法来了解当前虚拟机是否由于即将发生的维护事件而导致的中断。如安装升级补丁、替换硬件等。

这些被动运行的监控运行在所有节点上。如果健康检查开始失败,该节点将自动建立报警,对于更严重的健康检查故障,我们还将尝试驱逐容器,该操组由Pod本身决定,可以通过Pod Disruption Budget进行配置,以决定是否允许这种驱逐。

b. GPU动态测试

不幸的是,并非所有GPU问题都表现为通过DCGM可见的错误代码。我们已经建立了自己的测试库,这些测试库可以利用GPU来捕获其他问题,并确保硬件和驱动程序的运行情况符合预期。这些测试无法在后台运行,它们需要在几秒钟或几分钟内独占GPU。

所有节点都以preflight污点和标签加入集群。此污点会阻止在节点上调度常规Pod。将DaemonSet配置为在带有此标签的节点上运行预检测试Pod。成功完成测试后,测试本身将去除preflight污点和标签,然后该节点即可用于常规用途。

随后,我们将在节点的生命周期内定期运行这些测试。我们以CronJob方式运行,使其可以在群集中的任何可用节点上运行。


五、资源配额及用量

随着我们集群规模的不断扩大,然而研究人员开始发现自己难以获得分配的所有容量。 传统的调度系统具有许多不同的能力以确保团队之间公平地运行任务,而Kubernetes则没有。我们从这些调度系统中获得了灵感,并以Kubernetes原生的方式构建了一些功能。

污点

我们在每个集群中都有一个服务,即team-resource-manager,它具有多种功能。 它的数据源是ConfigMap,它为在给定集群中具有容量的所有研究团队指定元组(节点选择器,要应用的团队标签,分配数量)。 它使用openai.com/team=teamname:NoSchedule调整适当数量的节点。

team-resource-manager还配置一个admission webhook(译者:即准入服务插件)服务,以便在提交每个作业时,根据提交者的团队成员身份应用相应的容忍度。 通过使用污点,我们可以灵活地约束Kubernetes Pod Scheduler,例如允许对优先级较低的Pod允许任意容忍,这允许团队在无需强力协调的情况下资源共享。

CPU & GPU balloons

除了使用cluster-autoscaler动态扩展虚拟机集群外,我们还使用它来管理(删除和重新添加)集群中不正常的节点。为此,我们将激情的最小设置为零,并将集群的最大设置为可用容量。但是,如果cluster-autoscaler看到空闲节点,则将尝试缩小到仅所需的容量。由于多种原因(VM启动延迟,预分配的成本,上述API Server的影响),这种空闲扩展并不理想。

因此,我们为CPU和GPU主机引入了balloons Deployment。该Deployment包含一个具有最大值数量的低优先级容器配置。这些Pod占用了节点内的资源,因此自cluster-autoscaler不会将其视为空闲。但是,由于它们的优先级较低,因此调度程序可以立即将其逐出,以便为实际工作腾出空间。 (我们选择使用Deployment而不是DaemonSet,以避免将DaemonSet视为节点上的空闲工作负载。)

需要注意的一件事是,我们使用容器抗亲和力来确保容器在节点上均匀分布。自Kubernetes 1.18起已更正了该算法的性能问题。


六、成组调度(Gang scheduling)

我们的实验通常涉及一个或多个StatefulSet,每个StatefulSet都在训练工作的不同部分进行。对于优化器,研究人员需要在进行任何训练之前调度完StatefulSet的所有pod(因为我们经常在优化器成员之间使用MPI进行协作,并且MPI对组成员身份更改很敏感)。

但是,默认情况下,Kubernetes并不一定要优先执行一个StatefulSet的请求。例如,如果两个实验作业各自请求集群容量的100%,但Kubernetes可能只调度每个实验Pod的一半,从而导致调度僵局,这两个实验作业都无法完成。

我们尝试了实现自定义调度程序,但是遇到了一些极端情况,这些情况导致与常规Pod的调度方式发生冲突。 Kubernetes 1.18引入了Kubernetes framwork plugin架构,这使得在本地添加此类功能变得更加容易。我们最近引入Coscheduling插件解决此问题。


七、结论

在扩展Kubernetes集群时,仍有许多问题需要解决。 其中一些包括:

a. 监控指标

就我们的规模而言,Prometheus的内置TSDB存储引擎的压缩速度很慢,并且每次重新启动时都需要花费很长的时间来恢复WAL(Write-Ahead-Log),这给我们带来了很大的麻烦。 我们正在迁移到其他与Prometheus兼容的存储和查询引擎。 期待将来有关它如何发展的博客文章!

b. Pod网络流量整形

当我们扩展群集时,每个Pod都会被计算为具有一定数量的Internet带宽,那么所有Pod总体流量将非常惊人,因而需要引入流量整形技术,防止网络风暴、流量泛滥等问题。

我们发现Kubernetes是满足我们研究需求的异常灵活的平台。 它具有扩展能力,可以满足我们要求的最苛刻的工作负载。 尽管还有很多地方需要改进,但OpenAI的超级计算团队将继续探索Kubernetes如何扩展。

后续相关内容,请查看公众号:DCOS

作者:Benjamin Chess、Eric Sigler
译者:zouyee
原文:openai.com/blog/scalin…


参考资料