最近无意间看到一篇两年前的文章《Scaling Kubernetes to 7,500 nodes》原文如下:https://openai.com/research/scaling-kubernetes-to-7500-nodes#unsolvedproblems。 虽然有点倔坟,但好在里面的东西并不算太落后,至少从OpenAI团队之前的文章来看,也确实记录了整个在Kubernetes集群规模的成长与经验的分享,非常的值得学习。正好最近我们 KubeGems PAI(机器学习平台)也遇到一些调度方面的问题,便再三阅读并简单做个笔记与大家分享读后感。
资源调度
[图片上传失败...(image-10b29d-1689260468845)]
**解释:**因为Kubernetes中的每个Node节点的GPU均采用NVLink和GPUDirect直通网卡,所以在一个Node上仅调度一个Pod独占全部资源来达到算力最大化利用。 要实现这个效果,采用NodeSelector和DaemoSet可以最简单满足需求,对K8S的调度压力也最小(后面说到OpenAI并不是使用DaemonSet方式,且也无法做到更高级的调度策略,这里仅仅只是举例)。在独占Node场景下确实不需要调度支持Bin-Pack(尽可能将pod填充满node)和Fragmentation(碎片化)算法,因为此时整个集群的资源最小粒度是Node而不是Pod,也自然不用考虑CPU NUMA拓扑结构。也不存在Node资源争强的问题。
**新知识:**full bisection bandwidth(全双工切分带宽)指一个集群中任何一半的节点都可以与另一半的节点进行最大带宽的通信,而不会受到带宽限制的影响。例如,假设一个系统有16个节点,每个节点都有一个10 Gb/s的网络连接。如果系统设计得很好,那么任何8个节点都应该能够同时与其他8个节点进行10 Gb/s的通信。全双工切分带宽的主要优点是它可以大大提高系统的并行处理能力,因为它可以让所有的节点都能够最大化地利用他们的网络带宽。
[图片上传失败...(image-a8bfb9-1689260468845)]
解释:我们根据团队名字设计了一个污点openai.com/team=teamname:NoSchedule
并把他标记到服务器上,这样不同团队在使用资源时就必须要添加污点容忍才能协调到资源。同时我们还自己开发了个控制器,用于在准入阶段将忽略污点,优先调度低优先级的pod。这样就可以让团队直接可以彼此借用资源。这个Webhook挺有意思的,熟悉Volcano的知道,它在做资源调度时也允许不同Queue之间互相借用资源,并reclaim这个布尔值来决定是否当前Queue是否允许回收正在使用中的超额资源。这里可以说是英雄所见略同了。
[图片上传失败...(image-bfe398-1689260468845)]
解释:Gang scheduling在处理MPI作业时非常重要,原因在于MPI作业的同步通信特性。由于MPI是一种并行计算的编程模型,它允许进程间通过消息传递的方式进行通信,以完成一项共同的计算任务。在MPI中,一项常见的操作是集合通信,其中所有进程需要同时参与。如果任何一个进程滞后或者不可用,那么所有的进程都将被阻塞,等待该进程完成。这就导致了MPI作业非常依赖于所有参与进程的同步执行。OpenAI实现Gang Scheduling的方式则是通过嵌入k8s scheuler plugis的方式实现。这个插件名叫Coscheduling,当前已被合并到scheudler-plugin主线。https://github.com/kubernetes/enhancements/pull/1463. 上文也提到,Gang Scheduling也可通过Volcano来扩展Kubernetes的调度功能来实现。
并行作业处理
[图片上传失败...(image-1f450-1689260468845)]
解释: 参与到运行MPI作业任务的work节点都必须定期进行checkpoint,这是一种容错机制,可以在作业出错或者系统崩溃时恢复作业的状态,用来避免计算出错后全部重头来过。
新概念: semi-stateful pod (半状态容器),由于并行任务的Runtime载体是Pod,它的状态数据主要就是任务执行是产生的checkpoint。显然这部分数据需要被持久化到PVC中。之所以称之为半状态,主要在于即便该容器挂了,最坏的情况也是任务整体暂停并回到上一次checkpoint重新开始,并不会像有状态应用产生不可逆的灾难
网络
[图片上传失败...(image-6f568d-1689260468845)]
解释: 训练过程中几乎没有外部的网络开销(个人理解不包含存储数据集的数据访问),所以对Kubernetes的kube-proxy,ingress组建没有特别的依赖。之前调度部分说过,很多时候一个Node上就调度一个Pod独占,我甚至认为有可能Pod直接使用了Host网络来最小化网络的影响。 此外对Kubernets的服务发现应用场景也主要是为参与到MPI并行作业的进程提供网络拓扑结构,并用在各个Worker之间进行集体通信。
[图片上传失败...(image-79166a-1689260468845)]
解释: 当K8S集群扩大到7500台时,网络方案不管是基于overlay的flannel还是基于路由实现的组网,都无法在IP地址扩展性和性能方面做到同时兼顾。所以我们使用了Azure的VMSS解决了我们的问题。
**新知识:**VMSS是Azure上管理大规模虚拟机集群的网络服务和解决方案。
资料较少,看起来这里看起想表达的意思是OpenAI将Azure上管理虚拟机地址的VMSS服务通过CNI给Kuberntes Pod用了起来。
[图片上传失败...(image-a69597-1689260468845)]
解释: 我们的Pod对外访问还是基于NAT的,只不过用了Iptables来标记流量的来源以及使用量,这个主要用来评估Pod间或者说是并行作业间网络通讯是否存在瓶颈
存储
[图片上传失败...(image-b30272-1689260468845)]
解释:因为没有更多资料参考OpenAI中Blob存储的设计,按照这里意思,我们存储的用途主要来放训练时所需要的数据集以及记录训练过程中的checkout(上文有提到)。并且该存储还支持数据的预热以加速数据访问效率,同时这个存储对上还实现了操作系统标准的POSIX接口方便开发人员直接操作。
API servers
[图片上传失败...(image-b2e76d-1689260468845)]
**解释:**我们用5台独立的ETCD服务器和5台独立的api server服务器支撑了7500个节点,并当前的配置还足以应对未来的扩容的需求。这里面我们的主要优化点是将Kuebrnetes Events分离到其它Etcd集群上以减少记录大量事件的IO带来的延迟
[图片上传失败...(image-7c1f82-1689260468845)]
解释:运行大量节点场景下,每个Node上的List-Watch
带来的泛洪效应比较明显,涓流成河,当所有请求都汇聚到API Server后所带来的传输带宽高达1GB/s! 好在我们用了Kubernete 1.1之后的版本,通过EndpointSlices在服务器将压力缩小了1000倍
[图片上传失败...(image-9135d2-1689260468845)]
新知识:EndpointSlices是Kubernetes 1.16 版本引入的新Feature。它将Endpoint信息分散在多个较小的对象中,每个对象只包含一部分Endpoint信息。这样,对端点的添加、删除或修改只需要更新一个较小的 EndpointSlice 对象,而不需要更新整个 Endpoints 对象。这大大提高了 Kubernetes 在处理大规模集群时的性能和可扩展性。
监控
[图片上传失败...(image-f78d26-1689260468845)]解释: 我们也遇到了海量无效的指标,这真的很"烦人",大部分我们都不从来不关注。我们Prometheus也经常OOM,后来发现是大量的histogram指标查询堆积造成的。所以我们在后端查询时设置了执行超时时间,这样promtheus的内存就再没爆过了。
另外Prometheus重启后对WAL文件的重放事件慢得我们也无法忍受,后来在Robust Perception的帮助下知道了调大GOMAXPROCS参数来设置goroutine数来加快重放速度
啊?原来OpenAI的工程师居然不知道 - -!
总结
OpenAI 将 Kubernetes 集群扩展到 7,500 个节点,并为 GPT-3、CLIP 和 DALL·E 等大型模型提供了可扩展的基础设施,足以证明Kubernetes 是一个非常灵活的平台,随着AI行业的这波浪潮,Kubernetes也会跟着机器学习、更大规模和精细化的调度迎来一波新的高点。
[图片上传失败...(image-ce1e43-1689260468845)]
[图片上传失败...(image-39a212-1689260468845)]