【CSDN 编者按】最近,OpenAI 官方博客称,他们已将 Kubernetes 集群扩展到 7500 个节点,如此大规模的基础架构不仅可以满足 GPT-3、CLIP 和DALL·E 等大型模型的需求,而且也可以服务于快速的小规模迭代研究。

编译 | 弯月    责编 | 张文

出品 | CSDN(ID:CSDNnews)

最近,OpenAI 的官方博客称,他们已将 Kubernetes 集群扩展到 7500 个节点,如此大规模的基础架构不仅可以满足 GPT-3、CLIP 和 DALL·E 等大型模型的需求,而且也可以服务于快速的小规模迭代研究(例如神经语言模型的缩放定律等)。

很少有单个 Kubernetes 集群能够扩展到如此规模,这个过程需要付出巨大的努力,但 OpenAI 表示,简单的基础架构可以帮助机器学习研究团队更快地向前发展,并快速扩大规模,同时又无需修改代码。

2018 年的时候,OpenAI 的 Kubernetes 集群只有 2500 个节点。然而,两年以来,OpenAI 一直在扩展基础架构以满足研究人员的需求。在本文中,我们就来看一看他们总结的经验教训,以及有待解决的难题。

 

工作负载

 

首先,我们来看一看工作负载。我们通过 Kubernetes 运行的应用程序和硬件不同于寻常公司。

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

话虽如此,kube-scheduler 上经常会出现峰值压力。一项新作业就可能需要一次性创建数百个Pod,然后返回到相对较低的使用率。

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

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

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

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

 

网络

 

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

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

不采用封装增加了对底层 SDN 或路由引擎的需求,但能让网络结构更简单。添加VPN或隧道也不需要添加任何额外的适配器。我们也不需要担心由于某部分网络的MTU较低而导致数据分片的问题。网络策略和流量都很容易监控,每个数据包的源地址和目的地址都没有任何歧义。

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

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

iptables -t mangle -AINPUT ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openaitraffic=internet-in"
iptables -t mangle -AFORWARD ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openaitraffic=internet-in"
iptables -t mangle -AOUTPUT ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openaitraffic=internet-out"
iptables -t mangle -AFORWARD ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openaitraffic=internet-out"

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

% iptables -t mangle -L -v
Chain FORWARD (policyACCEPT 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-exporteropenai traffic=internet-in */

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

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

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

 

API服务器

 

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

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

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

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

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

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

 

使用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 可能需要重启,因为需要给监视程序打补丁,或者给物理服务器更换硬件。

这些被动的健康检查在所有节点的后台时刻运行。如果健康检查失败,节点就会自动禁止访问,这样新的Pod就会被调度到其他节点。对于更严重的健康检查失败,我们还会驱逐 Pod,要求所有当前正在运行的 Pod 立即退出。是否退出依然取决于 Pod 本身,这一点可以通过 Pod 中断预算进行配置,决定是否允许驱逐发生。最终,在所有 Pod 终止或者经过7天之后(我们的 SLA 中的规定),我们会强制终止 VM。

主动 GPU 测试

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

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

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

 

配额和资源利用

 

随着集群的扩展,研究人员越来越难获得分配给他们的所有容量。传统的作业调度系统提供了各种功能,可用于处理多个团队间反复运行的作业,但 Kubernetes 没有。长期以来,我们不断地从传统作业调度系统获取灵感,给 Kubernetes 添加原生功能。

团队污染

我们的每个集群中都有一个名为 team-resource-manager 的服务,它有多种功能。它的数据源是一个 ConfigMap,以 (节点选择器, 团队标签, 分配额度) 的方式为所有研究团队指定在某个集群中的容量。该服务会协调这些配置与集群中的当前节点,并给适当数量的节点添加以下污染:openai.com/team=teamname:NoSchedule。

team-resource-manager 还有一个管理 Webhook 服务,每当提交作业时,就会根据提交者所属的团队添加一个容忍。污染可以限制 Kubernetes 的 Pod调度器的灵活性,给低优先级的 Pod 添加“any”容忍,可以让团队在不需要大量协调的前提下互相借用容量。

CPU 和 GPU 的气球部署

除了使用 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 是一个非常灵活的平台,适合我们的研究需求。它能够扩展规模,以满足工作负载最大的需求。我们还有许多需要改进的地方,而且OpenAI 的超级计算团队也在继续探索 Kubernetes 的扩展。

参考链接:

https://openai.com/blog/scaling-kubernetes-to-7500-nodes/

程序员如何避免陷入“内卷”、选择什么技术最有前景,中国开发者现状与技术趋势究竟是什么样?快来参与「2020 中国开发者大调查」,更有丰富奖品送不停!

Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐