原创分享:OpenAi
翻译+注解:suntiger
1.前言
在上一篇文章Kubernetes达到2500个节点会发生什么-来自OpenAI团队的分享中,OpenAi的工程师从Kubernetes的主从节点、Docker镜像、网络、APR缓存几个维度进行优化,最终使Kubernetes集群稳定运行超过3个月,在本篇文章中,OpenAi的工程师针对Kubernetes扩充到7500个节点遇到的问题和尝试的解决方案过程进行了非常详细的记录,在做这篇文章内容翻译的同时,我对其中涉及的一些技术栈也做了注解,尽量方便大家能够一次看完有所斩获,节约二次查找资料的时间,以后翻译的技术文章也同样会采取翻译+注解的方式,感觉大家的关注,下面进入正题。
2.摘要
我们已经将Kubernetes集群扩展到了7500个节点,为大型模型如GPT-3、CLIP和DALL·E提供了可扩展的基础设施,同时也支持了像神经语言模型的规模定律这样的快速小规模迭代研究。
将单个Kubernetes集群扩展到如此规模是非常罕见的,需要一些特殊的关注,但好处是可以提供一个简单的基础设施,使我们的机器学习研究团队能够更快地进行工作并在不更改代码的情况下进行扩展。
自从我们上一次发布关于扩展到2500个节点的文章以来,我们继续扩大基础设施,以满足研究人员的需求,在这个过程中我们学到了许多额外的经验。本文总结了这些经验,以便Kubernetes社区的其他人可以从中受益,并以我们仍然面临的问题作为结尾,我们接下来将解决这些问题。
3.工作负载
在深入了解之前,描述我们的工作负载非常重要。我们在Kubernetes中运行的应用程序和硬件与普通公司可能遇到的情况相差甚远。我们遇到的问题及相应的解决方案可能适用于您的环境,也可能不适用!
一个大型机器学习任务跨越多个节点,当它可以访问每个节点上的所有硬件资源时,运行效率最高。这使得GPU可以通过NVLink进行直接通信,或者GPU可以通过GPUDirect与NIC进行直接通信。因此,对于我们的许多工作负载,单个pod占据整个节点。 NUMA、CPU或PCIE资源争用不是调度的影响因素。碎片化或装箱问题并不常见。我们当前的集群具有完全的切分带宽,因此我们也不考虑机架或网络拓扑。所有这些意味着,虽然我们有很多节点,但调度器的负担相对较轻。
(1).注解1:关于GPU通过NVLink进行直接通讯
NVLink是一种高速硬件连接技术,由NVIDIA公司开发,用于实现不同GPU(图形处理单元)之间的高速数据传输和通信。
GPU可以通过NVLink进行直接通信,意味着多个GPU之间可以通过这种连接技术快速共享数据,从而提高并行计算的性能。
在某些计算密集型任务,如深度学习、科学计算和高性能计算中,多个GPU往往需要协同工作以提高性能。传统上,
这些GPU之间的通信需要通过主机(CPU)进行,但这会导致通信延迟和带宽限制。NVLink技术可以让GPU之间直接通信,
避免了这些问题,提高了多GPU系统的性能和可扩展性。
(2).注解2:关于GPU通过GPUDirect与NIC通信
DPUDirect是一种由NVIDIA开发的高速数据传输技术,用于实现GPU与其他硬件设备(如网络接口卡,NIC)之间的直接通信。
GPU可以通过DPUDirect与NIC进行直接通信,意味着在数据中心、高性能计算和AI应用等场景中,
GPU可以在不经过CPU的情况下,直接与网络设备进行高速数据传输。
在许多计算密集型任务中,GPU需要快速地接收和发送数据。传统上,这些数据需要经过CPU和系统内存进行处理和传输,
这会导致通信延迟和性能瓶颈。通过使用DPUDirect技术,GPU可以绕过CPU,直接与NIC进行通信,从而实现更低的延迟和更高的吞吐量。
话虽如此,kube-scheduler的压力是突发的。一个新的任务可能包括一次性创建的数百个pod,然后回到相对较低的波动率。以下是目前的负载情况:
我们最大的工作任务使用MPI运行,作业内的所有pod都在一个单独的MPI通信器中。如果其中任意一个pod挂掉,整个作业都会停止,需要重新启动。作业定期进行检查点,重新启动时会从上一个检查点恢复。因此,我们认为pod是半状态的——被杀死的pod可以被替换,工作可以继续,但这样做会造成干扰,应尽量减少。
MPI(Message Passing Interface)是一种通用的并行计算标准,用于在分布式内存系统中实现高性能计算。
MPI提供了一组通信原语,如发送和接收消息,以便在多个进程之间传递数据和协调任务。
在Kubernetes中,可以利用Pods来部署基于MPI的并行应用程序。在这种设置中,
每个Pod可以承载一个或多个MPI进程,通常称为MPI任务。通过这种方式,可以在Pod之间建立MPI通信器,
实现跨Pod的数据传输和任务协调。
我们并不太依赖Kubernetes的负载均衡。我们的HTTPS流量非常少,不需要A/B测试、蓝绿部署或金丝雀部署。Pod之间通过其pod IP地址直接使用MPI进行SSH通信,而不是服务端点。服务“发现”功能有限;我们只在作业启动时执行一次查找,以确定哪些pod正在参与MPI。
(1).注解1:关于A/B测试
A/B测试是一种统计方法,用于比较两个或多个版本的应用程序或网站,以确定哪个版本的性能更好。
在A/B测试中,流量被分为两部分,一部分访问版本A,另一部分访问版本B。
通过比较两个版本的关键性能指标如转化率、点击率等,可以确定哪个版本的表现更优。
(2).注解2:关于蓝绿部署
蓝绿部署是一种软件发布策略,通过在生产环境中创建两个完全相同的环境(“蓝”和“绿”)来降低发布风险。
在这种策略中,新版本的应用程序部署在一个环境中(例如“绿”环境),而旧版本继续在另一个环境(例如“蓝”环境)运行。
一旦验证新版本的应用程序正常运行,流量将从旧环境切换到新环境。这有助于避免因部署引起的停机时间和问题。
(3).注解3:关于金丝雀部署
金丝雀部署是另一种渐进式的软件发布策略,通过将新版本的应用程序逐步推出给部分用户来降低风险。在金丝雀部署中,
新版本的应用程序首先部署给一小部分用户,然后根据其性能和稳定性评估结果决定是否继续向更多用户推出。
这种方法允许开发团队在问题扩大之前发现并修复问题,从而降低发布风险。
大多数作业都需要与某种形式的blob存储进行交互。它们通常要么直接从blob存储中流式传输数据集的一部分分片或检查点,要么将其缓存到相对快速的本地临时磁盘上。对于需要POSIX语义的情况,我们有一些PersistentVolumes,但blob存储的可扩展性更高,而且不需要执行缓慢的分离/附加操作。
最后,我们的工作本质上是研究,这意味着工作负载本身会不断变化。虽然超级计算团队努力提供我们认为的“生产”质量级别的计算基础设施,但在集群上运行的应用程序寿命短暂,开发人员迅速迭代。新的使用模式可能随时出现,挑战我们对趋势和适当权衡的假设。我们需要一个可持续的系统,同时能在事情发生变化时迅速做出反应。
4.网络
随着集群中节点和pod数量的快速增加,我们发现Flannel在扩展所需吞吐量方面遇到了困难。我们改为使用本机pod网络技术来进行Azure VMSS的IP配置以及相关的CNI插件。这使我们能够在pod上获得主机级别的网络吞吐量。
注解:关于Azure CNI插件
Azure CNI(Container Network Interface)插件是一个用于Azure Kubernetes Service (AKS)的网络插件,
它使得容器可以直接访问虚拟网络(VNet)子网。CNI插件是一个遵循CNI规范的程序,它负责配置容器网络,
并使容器能够与其他容器、虚拟网络中的其他资源以及互联网进行通信。
使用Azure CNI插件的优势包括:
1.容器的IP地址直接分配给虚拟网络子网,无需额外的NAT(网络地址转换)操作。
2.支持高级网络功能,如虚拟网络服务终结点(VNet service endpoints)和网络安全组(NSG)。
3.提供更好的网络性能,因为容器之间的通信不需要经过其他网络组件。
尽管如此,使用Azure CNI插件可能会面临IP地址管理方面的挑战,因为容器直接从子网分配IP地址。在设计和规划AKS集群时,
需要确保子网有足够的可用IP地址。
我们切换到使用基于别名的IP寻址的另一个原因是,在我们最大的集群中,我们可能在任何时候都有大约200,000个IP地址在使用。当我们测试基于路由的pod网络时,我们发现我们可以有效使用的路由数量存在显著限制。
避免封装增加了底层SDN或路由引擎的需求,但使我们的网络设置保持简单。可以在不使用任何额外适配器的情况下添加VPN或隧道。我们不需要担心由于网络某部分具有较低MTU而导致的数据包碎片化。网络策略和流量监控很简单;数据包的来源和目的地没有任何歧义。
我们在主机上使用iptables标签跟踪每个命名空间和pod的网络资源使用情况。这让研究人员可以查看他们的网络使用模式。特别是,由于我们的许多实验具有不同的互联网和pod内通信模式,因此通常需要能够调查可能出现的任何瓶颈。
可以使用iptables mangle规则根据特定标准对数据包进行任意标记。以下是我们用于检测流量是否为内部或互联网传输的规则。FORWARD规则涵盖了来自pod的流量,与主机的INPUT和OUTPUT流量相比:
一旦标记,iptables将开始计数器来跟踪与此规则匹配的字节数和数据包数量。您可以使用iptables本身来查看这些计数器:
我们使用一个名为iptables-exporter的开源Prometheus导出器将这些数据纳入我们的监控系统。这是一种简单的方法,可以跟踪匹配各种不同类型条件的数据包。
我们网络模型中一个有些独特的方面是,我们将节点、pod和服务网络CIDR范围完全公开给我们的研究人员。我们使用中心-辐射网络模型,并使用本地节点和pod的CIDR范围来路由流量。研究人员连接到中心,从那里可以访问任何独立的集群(辐射)。但集群本身不能相互通信。这确保了集群保持隔离,没有跨集群依赖关系,这可能会破坏故障隔离。
注解:关于中心-辐射网络模型和CIDR路由
中心-辐射网络模型是一种网络拓扑结构,它包括一个中心节点(通常是一个路由器或交换机),
以及与中心节点直接相连的多个辐射节点。辐射节点可以是其他路由器、交换机或主机。
在这种模型中,数据通常通过中心节点在辐射节点之间进行传输。这种拓扑结构的优点是易于管理和扩展,
但缺点是中心节点可能成为瓶颈和单点故障。
CIDR(无类别域间路由,Classless Inter-Domain Routing)是一种用于将IPv4地址划分为可变长度的子网的方法,
它在互联网中取代了旧的划分子网的方法(如A类、B类、C类地址)。CIDR允许更加有效地分配IP地址,
并减少路由表的大小。CIDR表示法使用斜杠(/)加上子网掩码位数,例如192.168.0.0/16。
在中心-辐射网络模型中,CIDR可以用来将IP地址分配给辐射节点。例如,
如果中心节点的IP地址是192.168.0.0/16,那么可以将192.168.1.0/24、192.168.2.0/24
等地址段分配给辐射节点。这样一来,每个辐射节点都可以管理自己的子网,并与其他辐射节点通信。
对于CIDR路由流量,路由器需要根据目标IP地址和子网掩码来确定数据包的目的地。
路由器会查看其路由表,找到与目标IP地址匹配的最长前缀(最具体的子网),
然后将数据包发送到与该子网关联的下一跳路由器或主机。在中心-辐射网络模型中,
这通常意味着数据包会首先到达中心节点,然后中心节点会将数据包转发到相应的辐射节点。
我们使用一个“NAT”主机将来自集群外部的流量转换为服务网络CIDR范围。这种设置使我们的研究人员在选择实验的网络配置方式和类型方面具有很大的灵活性。
5.API服务
Kubernetes API服务器和etcd是正常稳定工作集群的关键组件,因此我们特别关注这些系统上的压力。我们使用kube-prometheus提供的Grafana仪表板,以及其他内部仪表板。我们发现通过API服务器上HTTP状态429(请求过多)和5xx(服务器错误)的告警频率来发现问题非常有用。
虽然有些人在kube内部运行API服务器,但我们一直在集群本身之外运行它们。etcd和API服务器都在它们自己的专用节点上运行。我们最大的集群运行5个API服务器和5个etcd节点,以分散负载并在其中一个出现故障时最小化影响。自从在上一篇博客文章中将Kubernetes事件分离到它们自己的etcd集群后,我们没有遇到任何值得注意的etcd问题。API服务器是无状态的,通常很容易在自我修复的实例组或扩展集中运行。由于事件极为罕见,我们尚未尝试构建任何etcd集群的自我修复自动化。
API服务器可能会占用相当多的内存,这与集群中节点的数量呈线性关系。对于我们拥有7500个节点的集群,我们观察到每个API服务器最多使用70GB堆内存,所以幸运的是,这在未来仍应在硬件能力范围内。
API服务器上的一个巨大压力是对端点的WATCH操作。有一些服务,如“kubelet”和“node-exporter”,集群中的每个节点都是其成员。当节点被添加到集群或从集群中移除时,这个WATCH操作就会触发。而且,因为通常每个节点本身都通过kube-proxy观察kubelet服务,所以这些响应所需的数量和带宽将达到N^2,偶尔会达到1GB/s或更高。EndpointSlices 在Kubernetes 1.17中发布,它们极大地降低了这个负载,达到了1000倍的优化。
注解:关于Kubernetes的端点对象和EndpointSlices
在Kubernetes中,端点(Endpoints)和端点切片(EndpointSlices)是管理服务与集群中运行的Pod之间网络连接的关键资源。
端点对象(Endpoints)将Kubernetes服务与后端Pod的IP地址关联起来。当服务选择器与Pod标签匹配时,端点控制器(Endpoint Controller)会自动创建端点对象。
然而,随着集群规模的扩大,单个端点对象可能会变得非常庞大,导致性能和可扩展性问题。
为了解决这些问题,Kubernetes引入了EndpointSlices资源。EndpointSlices将较大的端点对象拆分成多个较小的碎片,
每个碎片包含一部分后端Pod的IP地址。这种方法可以提高性能和可扩展性,因为控制平面和数据平面需要处理的对象数量减少了。
对于Kubernetes的WATCH操作,API服务器提供了实时更新,允许客户端(如kube-proxy)监视资源变化。这在Kubernetes中很常见,
用于实时跟踪集群状态的变化。在EndpointSlices的情况下,kube-proxy等组件可以监视EndpointSlices资源的变化,
以便在服务的后端Pod发生变化时,实时更新网络连接。
当Kubernetes组件(如kube-proxy)对EndpointSlices执行WATCH操作时,它们将接收到有关端点切片更改的通知。
这包括新的EndpointSlice的创建、现有EndpointSlice的更新以及EndpointSlice的删除。这使得kube-proxy能够根据最新的EndpointSlices信息实时更新其服务和负载平衡配置,
从而确保网络流量正确地路由到运行中的Pod。
总的来说,我们非常关注与集群规模成比例的API服务器请求。我们尽量避免让任何DaemonSets与API服务器互动。在需要让每个节点监视变化的情况下,引入中间缓存服务,如Datadog Cluster Agent,似乎是避免整个集群瓶颈的好方法。
注解:关于Datadog Cluster Agent中间缓存服务
Datadog Cluster Agent是Datadog监控平台的一个组件,用于在Kubernetes集群中收集和聚合度量数据、事件和元数据。
通过引入Datadog Cluster Agent,可以将与API服务器的请求和响应进一步优化,提高性能和可扩展性。
Datadog Cluster Agent是一个中间缓存服务,位于Datadog Node Agents(部署为DaemonSet)和Kubernetes API服务器之间。
Datadog Cluster Agent的主要职责是:
1.减少API服务器的负载:Datadog Cluster Agent会与API服务器进行通信,获取集群中的度量和元数据信息。
这减轻了每个节点上的Datadog Node Agents直接与API服务器进行通信的负担。
2.缓存数据:Datadog Cluster Agent会缓存从API服务器获取的数据,以便在不再次访问API服务器的情况下提供给Datadog Node Agents。
3.聚合数据:Datadog Cluster Agent负责从所有节点上的Datadog Node Agents收集度量数据,
并将其聚合为一个集中的数据源。然后,它将这些数据发送到Datadog平台,以便进行可视化、分析和告警。
随着我们的集群不断扩大,我们实际上减少了集群的自动缩放。但是,我们在一次性自动扩展过多时偶尔会遇到问题。当新节点加入集群时,会产生许多请求,一次性添加数百个节点可能会导致API服务器负载过重。即使仅仅平滑几秒钟,也有助于避免中断。
6.使用Prometheus和Grafana进行时间序列度量
我们使用Prometheus收集时间序列度量数据,并使用Grafana进行图表、仪表板和告警。我们从部署kube-prometheus开始,它收集了各种各样的度量数据,并提供了用于可视化的优秀仪表板。随着时间的推移,我们添加了许多自己的仪表板、度量和告警。
注解:关于Prometheus和Grafana
Prometheus是一个开源的监控和告警工具,它可以收集和存储来自各种数据源的时间序列数据。
Grafana是一个开源的度量分析和可视化平台,它可以从多种数据源获取数据并生成实时的可视化图表。
Prometheus和Grafana通常一起使用,以提供一个强大且灵活的监控解决方案。
使用Prometheus和Grafana进行时间序列度量的主要优点如下:
1.高度可扩展:Prometheus和Grafana都是为大型分布式系统设计的,因此它们可以轻松地扩展以满足您的监控需求。
2.实时数据分析:Grafana提供了实时数据分析,能够快速识别和解决问题。
3.灵活的数据源:Prometheus和Grafana支持从各种数据源收集和展示数据,包括Kubernetes、InfluxDB、Elasticsearch等。
4.强大的告警功能:Prometheus可以根据自定义的规则触发告警,并通过多种通道(如电子邮件、Slack等)发送通知。
5.丰富的可视化选项:Grafana提供了各种可视化图表类型,如图表、仪表板、地图等,以及丰富的自定义选项,可以轻松创建满足特定需求的仪表板。
6.社区支持:Prometheus和Grafana都有庞大的开源社区,提供了丰富的文档、插件和集成,以帮助我们充分利用这些工具。
通过使用Prometheus和Grafana,可以轻松地监控Kubernetes集群的性能、资源使用情况和其他重要指标。这将帮助我们确保系统的稳定性、可靠性和性能,以及及时发现和解决潜在的问题。
随着我们添加越来越多的节点,我们在处理由Prometheus收集的大量度量数据方面遇到了困难。虽然kube-prometheus公开了很多有用的数据,但其中有些我们实际上从未查看过,有些数据太过细致,以至于无法有效地收集、存储和查询。我们使用Prometheus规则来“丢弃”一些不需要的度量数据。
有一段时间,我们一直在努力解决一个问题,那就是Prometheus会不断消耗越来越多的内存,直到最终因内存耗尽错误(OOM)导致容器崩溃。即使为应用程序提供了大量的内存容量,这种情况似乎仍然会发生。更糟糕的是,当它崩溃时,它在重新启动过程中需要花费数小时来重放写入前日志文件,才能再次变得可用。
为了解决这个问题,我们需要调整Prometheus的配置和资源限制,确保它在收集和存储大量度量数据时仍能正常运行。这可能包括降低数据的粒度、丢弃不需要的度量数据、优化查询性能以及根据需要调整内存和CPU资源。通过对Prometheus进行适当的调优和管理,我们可以确保它能够在大型Kubernetes集群中有效地执行其监控任务。
最终,我们找到了这些OOM的根源,即Grafana和Prometheus之间的相互作用,Grafana会在Prometheus上使用/api/v1/series API并使用{le!=””}查询(基本上是“给我所有的直方图度量数据”)。/api/v1/series的实现在时间和空间上都没有限制——对于有很多结果的查询,它会继续消耗越来越多的内存和时间。即使在请求者放弃并关闭连接之后,它仍会继续增长。对我们来说,内存永远不够用,Prometheus最终会崩溃。我们对Prometheus进行了修补,将此API限制在一个上下文中以强制执行超时,从而完全解决了这个问题。
尽管Prometheus崩溃的次数大大减少,但在需要重启它的时候,WAL回放仍然是一个问题。它通常需要数小时才能回放完所有WAL日志,然后Prometheus才能开始收集新的度量数据并处理查询。在Robust Perception的帮助下,我们发现将GOMAXPROCS设置为24可以大大改善这个问题。在WAL回放期间,Prometheus试图使用所有核心,而对于具有大量核心的服务器,争用会破坏所有性能。
我们正在探索新的选择以增加我们的监控能力,在下面的“未解决的问题”部分进行了描述。
7.健康检查
对于如此庞大的集群,我们当然依赖自动化工具来检测和移除集群中的故障节点。随着时间的推移,我们建立了一系列的健康检查系统。
7.1 被动健康检查
有些健康检查是被动的,始终在所有节点上运行。这些健康检查监控基本的系统资源,例如网络可达性、磁盘损坏或已满,或者 GPU 错误。GPU 以多种不同的方式出现问题,但一个简单的常见问题是“不可纠正的ECC错误”。英伟达的数据中心GPU管理器 (DCGM) 工具可以轻松查询到这些以及其他一些“Xid”错误。我们跟踪这些错误的一种方法是通过 dcgm-exporter 将指标导入到 Prometheus(我们的监控系统)中。这将显示为 DCGM_FI_DEV_XID_ERRORS 指标,并设置为最近发生的错误代码。此外,NVML设备查询API提供了关于GPU健康状况和操作的更详细信息。
注解:关于NVML设备查询API
NVIDIA Management Library(NVML)是一个C语言编写的硬件管理库,用于直接查询、监控和管理NVIDIA GPU设备。
NVML提供了一组设备查询API,使开发人员能够获取关于GPU设备的详细信息,如温度、功耗、内存使用情况、时钟频率和驱动版本等。这些API主要用于开发GPU监控和管理工具。
以下是一些常用的NVML设备查询API:
1.`nvmlDeviceGetCount`:获取系统中GPU设备的数量。
2.`nvmlDeviceGetHandleByIndex`:通过设备索引获取设备句柄,用于后续的设备查询操作。
3.`nvmlDeviceGetName`:获取指定设备的名称(如:NVIDIA Tesla V100)。
4.`nvmlDeviceGetTemperature`:获取指定设备的温度(以摄氏度为单位)。
5.`nvmlDeviceGetPowerUsage`:获取指定设备的实时功耗(以毫瓦为单位)。
6.`nvmlDeviceGetMemoryInfo`:获取指定设备的内存信息,包括总内存、已用内存和剩余内存。
7.`nvmlDeviceGetUtilizationRates`:获取指定设备的GPU和内存利用率(以百分比表示)。
8.`nvmlDeviceGetClockInfo`:获取指定设备的时钟频率信息(如:核心时钟和内存时钟)。
9.`nvmlDeviceGetDriverModel`:获取指定设备的驱动模式(如:WDDM或TCC)。
10.`nvmlDeviceGetPciInfo`:获取指定设备的PCI信息(如:总线ID、设备ID和域ID)。
一旦我们检测到错误,通常可以通过重置 GPU 或系统来解决问题,尽管在某些情况下,可能确实需要物理更换底层 GPU。
另一种健康检查跟踪来自上游云提供商的维护事件。主要云提供商中的每一个都提供了一种了解当前 VM 是否需要进行即将到来的维护事件的方法,这些事件最终会导致中断。VM 可能需要重新启动以便应用底层的虚拟机管理器补丁,或者将物理节点更换为其他硬件。
这些被动健康检查始终在所有节点上的后台运行。如果健康检查开始失败,节点会自动被隔离,这样就不会在该节点上调度新的 pod。对于更严重的健康检查失败,我们还会尝试进行 pod 驱逐,要求所有当前运行的 pod 立即退出。但是,是否允许驱逐发生仍取决于 pod 本身,这可以通过 Pod 中断预算进行配置。最终,在所有 pod 已终止或者过去 7 天(我们的 SLA 的一部分)之后,我们将强制终止 VM。
7.2 主动GPU测试
不幸的是,并非所有GPU问题都会通过DCGM显示为可见的错误代码。我们已经建立了自己的测试库,以执行 GPU 测试来捕捉其他问题,并确保硬件和驱动程序的行为符合预期。这些测试无法在后台运行——它们需要独占GPU几秒钟或几分钟才能运行。
我们首先在节点启动时运行这些测试,这是一个我们称之为“预检”的系统。所有节点都带有一个“预检”污点和标签。这个污点防止在节点上调度普通 pod。一个 DaemonSet 被配置为在所有具有此标签的节点上运行预检测试 pod。测试成功完成后,测试本身会移除污点和标签,节点随后可用于一般用途。
我们还在节点的生命周期中定期运行这些测试。我们将其作为CronJob运行,允许它在集群中的任何可用节点上运行。诚然,这有点随机且无法控制哪些节点会被测试,但我们发现随着时间的推移,这种方法在最小的协调和干扰下提供了足够的覆盖范围。
8.配额与资源使用
随着我们的集群规模扩大,研究人员开始发现自己很难获得分配给他们的所有计算能力。传统的作业调度系统具有许多不同的功能,可在竞争团队之间公平地运行工作,而Kubernetes则没有这些功能。随着时间的推移,我们从这些作业调度系统中汲取灵感,并以Kubernetes原生的方式构建了一些功能。
8.1 团队污点
我们在每个集群中都有一个名为“team-resource-manager”的服务,它具有多种功能。它的数据来源是一个 ConfigMap,为所有具有给定集群容量的研究团队指定(节点选择器,要应用的团队标签,分配数量)的元组。它将这些数据与集群中的当前节点进行对比,为相应数量的节点添加 openai.com/team=teamname:NoSchedule 污点。
“team-resource-manager”还具有一个准入 webhook 服务,这样在每个作业提交时,都会根据提交者的团队成员关系应用相应的宽容度。使用污点允许我们灵活地约束 Kubernetes pod 调度器,例如为优先级较低的 pod 提供一个“任何”宽容度,这样团队可以在不需要繁重协调的情况下借用彼此的容量。
8.2 CPU和GPU气球
除了使用集群自动扩缩器(cluster-autoscaler)动态调整我们的VM支持的集群大小之外,我们还使用它来修复(删除并重新添加)集群中不健康的成员。我们通过将集群的“最小规模”设置为零,将集群的“最大规模”设置为可用容量来实现这一点。然而,如果集群自动扩缩器发现空闲节点,它会尝试缩减到仅所需的容量。由于多种原因(VM启动延迟、预分配成本以及上述提到的API服务器影响),这种空闲缩放并不理想。
因此,我们为我们的仅CPU和GPU主机引入了气球部署(balloon Deployment)。这个部署包含一个具有“最大规模”数量的低优先级Pod的副本集。这些Pod在节点中占用资源,因此自动扩缩器不会将它们视为空闲。然而,由于它们的优先级较低,调度器可以立即驱逐它们以腾出空间供实际工作使用。(我们选择使用部署(Deployment)而不是守护程序集(DaemonSet),以避免在节点上将守护程序集视为空闲工作负载。)
值得注意的是,我们使用Pod反亲和性(pod anti-affinity)来确保Pod在节点间均匀分布。早期版本的Kubernetes调度器在处理Pod反亲和性时存在O(N^2)的性能问题。自Kubernetes 1.18版起,这一问题已得到解决。
9.群调度
我们的实验通常涉及一个或多个StatefulSets,每个StatefulSet操作训练工作的不同部分。对于优化器,研究人员需要在进行任何训练之前调度StatefulSet的所有成员(因为我们经常使用MPI来协调优化器成员之间的通信,而MPI对群组成员资格的变化非常敏感)。
然而,Kubernetes默认不一定会优先满足一个StatefulSet的所有请求而不是另一个。例如,如果两个实验都请求了集群100%的容量,Kubernetes可能只调度每个实验Pod的一半,而不是调度一个实验的全部Pod,导致死锁,使得两个实验都无法取得进展。
我们尝试了一些需要自定义调度器的方法,但遇到了一些边缘情况,这些情况会导致普通Pod调度出现冲突。Kubernetes 1.18引入了核心Kubernetes调度器的插件架构,使得以本地方式添加此类功能变得更容易。我们最近发现Coscheduling插件是解决这个问题的好方法。这个插件可以确保在同一个PodGroup内的所有Pod都被同时调度,从而避免了上述死锁问题。使用Coscheduling插件,我们可以确保实验中涉及的StatefulSets按照预期运行,并防止实验之间的资源争用导致的阻塞。
注解:关于Coscheduling插件
Kubernetes 1.18版本并没有直接引入Coscheduling插件,但在1.18版本中,Kubernetes调度器(kube-scheduler)引入了一个新的调度框架(Scheduling Framework),
允许开发者使用插件(plugins)扩展和自定义调度策略。这为实现Coscheduling功能提供了基础。
借助新的调度框架,社区开发了名为`kube-scheduler-plugins`的项目,其中包含了多个调度插件,
包括Coscheduling插件。Coscheduling插件确保一组相关的Pods在同一时间窗口内被调度和启动。
Coscheduling插件实现了一个名为`PodGroup`的CRD(Custom Resource Definition),它可以将一组相互关联的Pods组合在一起。
当调度器处理PodGroup中的Pods时,它会尝试确保这些Pods在同一时间窗口内被调度。如果在指定的时间窗口内无法调度所有Pods,
则会将PodGroup中的所有Pods从集群中撤销,以便在稍后重试调度。
10.未解决的问题
随着我们扩展Kubernetes集群,仍有许多问题需要解决。其中一些包括:
10.1 指标
在我们的规模上,我们在Prometheus的内置TSDB存储引擎中遇到了很多问题,如缩减速度慢,以及在每次重启时需要很长时间来回放WAL(预写日志)。查询也倾向于导致“查询处理将加载过多的样本”错误。我们正在将Prometheus迁移到一个兼容的存储和查询引擎。
10.2 Pod网络流量
随着我们扩大集群规模,每个Pod都被计算为具有一定量的可用互联网带宽。每个人的总互联网带宽需求变得很大,我们的研究人员现在经常在无意中给互联网上的其他地方带来显著的资源压力,例如用于下载的数据集和要安装的软件包。
11.结论
我们发现Kubernetes是一个非常灵活的平台,能满足我们的研究需求。它具有扩展性,可以满足我们施加的最具挑战性的工作负载。尽管如此,仍有很多领域需要改进,OpenAI的超级计算团队将继续探索Kubernetes的扩展能力。如果您觉得这类工作很有趣,您应该考虑在OpenAI申请!