OpenAI Kubernetes 相关博文读后笔记

2023-04-27,,

一、概述

最近 ChatGPT 和其公司 OpenAI 特别火:ChatGPT 3, ChatGPT 3.5, New Bing, ChatGPT 4...

怀着学习的心态,这几天访问了 OpenAI 的博客, 上边关于 AI 的内容,确实隔行如隔山,完全看不明白。

但是翻看过程中,惊喜发现有 2 篇与 Kubernetes 使用相关的文章:

2018 年 1 月:Scaling Kubernetes to 2,500 nodes (openai.com)
2021 年 1 月:Scaling Kubernetes to 7,500 nodes (openai.com)

这不碰到老本行了嘛,学习下~

以下为读后笔记,也加入了自己的思考:针对 OpenAI 现状,如何进一步优化监控、镜像拉取、容器编排相关架构。

二、读后笔记

2.1 Dota 2 的 OpenAI 是跑在 Kubernetes 上的

Dota2 游戏镜像大约是 17GB

2.2 OpenAI 如何使用 Kubernetes

2.2.1 用途

Kubernetes 在 OpenAI 主要用于深度学习,主要使用的是 Kubernetes Job.

2.2.2 选择 Kubernetes 原因

Kubernetes 提供了

快速的迭代周期
合理的可扩展性
标准样板

2.2.3 Kubernetes 集群规模

集群运行在 AZure 上
2018 年博文,集群 Node 规模为 2500 多
2021 年博文,集群 Node 规模为 7500 多

2.2.4 Kubernetes 超大规模使用过程中遇到的问题

etcd
Kube masters
Docker 镜像拉取
网络
KubeDNS
机器的 ARP 缓存

2.2.5 Kubernetes 配套工具

监控:

DataDog(商业监控)
Prometheus + Grafana
日志:
fluentd
网络:
刚开始是 Flannel
后面是 Azure 的 VMSSes CNI 插件

2.3 Kubernetes 超大规模发现的问题及解决

2.3.1 Etcd

2.3.1.1 问题描述

集群 500 节点后,刚开始是 kubectl 使用卡;另外通过 DataDog 监控发现磁盘写入延迟飙升至数百 ms,尽管每台机器都使用 30,5 IOPS 的 P000 SSD。

后面解决之后,加到 1000 节点,发现 etcd 延迟再次变高;发现 kube-apiservers 从 etcd 读取的速率超过 500MB/s

另一个 1,000 个节点后的故障是达到了 etcd 的硬存储限制(默认为 2GB),这导致它停止接受写入。

2.3.1.2 解决方案

将每个节点的 etcd 目录移动到本地临时磁盘,该临时磁盘是直接连接到实例的 SSD,而不是网络连接的 SSD。切换到本地磁盘使写入延迟达到 200us,etcd 变得健康!(根据我们之前的 Azure 使用经验,其网络 SSD 确实性能不太行)
1000 节点时出现的问题,在于
启用了审计日志
启用了 Prometheus 对 APIServer 的监控
这导致出现了许多缓慢的查询和对事件的 LIST API 的过度调用
根本原因:默认设置 fluentd 和 Datadog 的监控过程是从集群中的每个节点查询 API 服务器。"我们"只是简单地更改了这些进程,使其轮询不那么激进,并且 apiserver 上的负载再次变得稳定
将 Kubernetes Events 存储在一个单独的 etcd 集群中,这样事件创建中的峰值不会影响主 etcd 实例的性能。配置如后面的代码段
我们用标志 --quota-backend-bytes 增加了最大 etcd 大小。

--etcd-servers-overrides=/events#https://0.example.com:2381;­https://1.example.com:2381;­https://2.example.com:2381

最后,etcd 和 APIServer 都运行在专用节点上。避免相互影响。

在 7500 节点时,有 5 个 etcd 节点。

2.3.2 API Server

在 7500 节点时,有 5 个 API 服务器,并且每个 API 服务器使用的堆内存高达 70GB.

2.3.3 Docker 镜像拉取

2.3.3.1 问题描述

Dota 容器会在一段时间内处于 pending 状态——但对于其他容器也是如此。

解决之后,发现有报错:rpc error: code = 2 desc = net/http: request canceled, 表明由于缺乏进度,镜像拉取已被取消。

还有个问题,OpenAI 的 Kubernetes 组件镜像是默认从 gcr.io 拉取的,但是 gcr.io 可能失败或超出配额(机器用的 NAT 公网 IP 是同一个,很容易超出配额).

2.3.3.2 解决方案

kubelet 有一个 --serialize-image-pulls 默认为 true 的标志,表示 Dota 镜像拉取阻塞了所有其他镜像。

--serialize-image-pulls 改为 false; 将 Docker 根目录移动到了实例附加的 SSD(而不是网络 SSD)

针对第二个问题,大镜像需要太长时间的 pull 和提取,或者当有大量积压的镜像需要拉取时。为了解决这个问题,我们将 kubelet 的 --image-pull-progress-deadline 标志设置为 30 分钟,并将 Docker 守护进程的 max-concurrent-downloads 选项设置为 10。第二个选项没有加快大镜像的提取速度,但允许镜像队列并行拉取。

为了解决这个 gcp.io 失败的问题,"我们"通过使用 docker image save -o /opt/preloaded_docker_images.tardocker image load -i /opt/preloaded_docker_images.tar,在 Kubernetes worker 的机器镜像中预装了这些 Docker 镜像。 为了提高性能,我们对常见的 OpenAI 内部镜像如 Dota 镜像的白名单做了同样的处理。

2.3.3.3 笔者思考

关于 OpenAI 碰到的 Docker 镜像拉取的问题,都是非常典型的大规模 Kubernetes 会碰到的问题,其实有更好的解决方案:P2P 镜像解决方案,典型就是 DragonFly.

DragonFly 提供高效、稳定、安全的基于 P2P 技术的文件分发和镜像加速系统,并且是云原生架构中镜像加速领域的标准解决方案以及最佳实践。其最大的优势就是:

基于 P2P 的文件分发:通过利用 P2P 技术进行文件传输,它能最大限度地利用每个对等节点(Peer)的带宽资源,以提高下载效率,并节省大量跨机房带宽,尤其是昂贵的跨境带宽。
预热:P2P 加速可预热两种类型数据 image 和 file, 用户可以在控制台操作或者直接调用 api 进行预热。

2.3.4 网络

Flannel 在这种超大规模场景下肯定是撑不住的,刚开始 OpenAI 采用了非常简单暴力的解决方案(也适合他们的使用场景): pod 配置使用 HostNetwork:

...
hostNetwork: true
...
dnsPolicy: ClusterFirstWithHostNet

后面是改为使用 Azure 的 VMSSes CNI 插件。

2.3.4.1 笔者思考

其实 OpenAI 对 Kubernetes 的刚需是:容器编排,网络功能不是刚需,OpenAI 不用 Kubernetes CNI 也可以的。后面会延伸讨论一下。

2.3.5 ARP 缓存

另外一个可能经常会忽略的点是 ARP 缓存问题。

2.3.5.1 问题描述

有一天,一位工程师报告说,他们的 Redis 服务器的 nc -v 需要 30 多秒才能打印出连接已经建立。我们追踪到这个问题是由内核的 ARP 栈引起的。对 Redis pod 主机的初步调查显示,网络出了严重的问题:任何端口的通信都要挂上好几秒,而且无法通过本地 dnsmasq 守护进程解析 DNS 名称,dig 只是打印了一条神秘的失败信息:socket.c:1915: internal_send: 127.0.0.1#53: Invalid argument。dmesg 日志的信息量更大:neighbor table overflow! 这意味着 ARP 缓存的空间已经用完。ARP 是用来将网络地址(如 IPv4 地址)映射到物理地址(如 MAC 地址)的。

2.3.5.2 解决方案

/etc/sysctl.conf中设置选项:

net.ipv4.neigh.default.gc_thresh1 = 80000
net.ipv4.neigh.default.gc_thresh2 = 90000
net.ipv4.neigh.default.gc_thresh3 = 100000

在 Kubernetes 集群中调整这些选项尤其重要,因为每个 pod 都有自己的 IP 地址,会消耗 ARP 缓存的空间。

2.3.6 Prometheus 和 Grafana

由于大量的采集和查询,Prometheus 和 Grafana OOM 的频率也不低。

2.3.6.1 解决方案

Grafana: 本质上还是 Prometheus 的高基数问题,我之前介绍过,见这里:

Prometheus 性能调优 - 什么是高基数问题以及如何解决?- 东风微鸣技术博客 (ewhisper.cn)
Prometheus: 重启后,WAL replay 是个问题,在 Prometheus 收集新指标和服务查询之前,通常需要花费数小时才能 replay 所有 WAL 日志。
使用 GOMAXPROCS=24 配置,在 WAL replay 期间,Prometheus 试图使用所有的内核,对于拥有大量内核的服务器来说,这种争夺会扼杀所有的性能。

2.3.6.2 笔者思考
    Prometheus 近期版本性能会好很多,及时升级到最新版本会对性能问题大有帮助。比如:高基数,大内存,cpu 消耗较多等都有一定程度优化。
    Prometheus 在这么大规模集群情况下,建议创建多个 node role 为 monitoring 的高配机器(也挂本地 SSD), 供 Prometheus 专用。
    或者更近一步,可以选择兼容 Prometheus 的其他方案,如:VictoriaMetrics(适合存储为块存储场景) 和 Grafana Labs 发布的 Mimir(适合存储为对象存储场景).

2.4 OpenAI Kubernetes 的使用场景及推荐的替换方案

这里详细描述一下 OpenAI Kubernetes 的使用场景:

主要用到的资源类型是 Job
对于 OpenAI 的许多工作负载,单个 Pod 占据了整个节点
OpenAI 当前的集群具有完整的平分带宽,不会考虑任何机架或网络拓扑
OpenAI 不太依赖 Kubernetes 负载均衡,HTTPS 流量很少,不需要 A/B 测试,蓝/绿或金丝雀
Pod 通过 SSH 使用 MPI 在其 Pod IP 地址上直接相互通信 (hostNetwork),而不是通过 service endpoint 进行通信
服务发现是有限的
有一些 PersistentVolume,但 blob 存储的可伸缩性要高得多
避免使用 Overlay 网络,因为会影响网络性能

2.4.1 笔者思考

看完 Scaling Kubernetes to 7,500 nodes (openai.com) 这篇,其实会发现 OpenAI 对 Kubernetes 的使用和普通 IT 公司差异还是比较大的。

OpenAI 最主要用的是:Kubernetes 的容器编排, 特别是对 Job 的调度能力。

其他 Kubernetes 功能,用的很少或几乎没有,如:

存储 CSI 用的很少,主要使用的是 blob 存储
网络 CNI 用了,但 Pod 主要用的是 hostNetwork
DNS 用的也不多
Service 用的很少,Pod 主要用的是 hostNetwork
Ingress (即上文说的 Kubernetes 负载均衡)用的很少,因为 Kubernetes 集群主要用于实验(现在随着 ChatGPT 的大规模使用可能会用的比之前多一些)
Kubernetes 的一些高级发布策略,如 A/B 测试,蓝/绿或金丝雀也不需要

所以我个人认为(观点仅供参考), Kubernetes 对于 OpenAI 来说,还是有些过于复杂和功能冗余的。

OpenAI 真正需要的,是一个纯粹的容器编排解决方案,特别是对 Job 的调度能力。

所以我觉得啊,不考虑用户规模,不考虑 Kubernetes 是容器编排领域的事实上的标准的话,HashiCorp 的 Nomad 反而是更合适的解决方案。以下是具体理由:

Nomad 是一个易于使用、灵活和高性能的工作负载调度器,可以部署混合的微服务、批处理容器化和非容器化应用。
GPU 支持:Nomad 为 GPU 工作负载(如机器学习(ML)和人工智能(AI))提供内置支持。Nomad 使用设备插件来自动检测和利用来自硬件设备(如 GPU、FPGA 和 TPU)的资源。
经过验证的可扩展性:Nomad 乐观地并发,可提高吞吐量并减少工作负载的延迟。Nomad 已被证明可以在实际生产环境中扩展到 10K+ 节点的集群。
简单性:Nomad 作为单个进程运行,外部依赖性为零。 运维人员可以轻松配置、管理和扩展 Nomad。开发人员可以轻松 定义并运行应用程序。
Nomad 的其中 2 个 调度器: Batch 和 System Batch, 非常契合 OpenAI 的使用场景。
Nomad 并不附带服务发现及网络的相关功能,只是在 1.3 版本以后, 增加了内置的本地服务发现(SD),使得 Consul 或其他第三方工具非必要。Nomad 的服务发现并不是要取代这些工具。相反,它是一个替代品,可以更容易地测试和部署更简单的架构。

2.5 横向扩展小技巧

另外,惊喜的发现 OpenAI 的博文中竟然提到了 Kubernetes 横向扩展的小技巧,OpenAI 将其称为:CPU & GPU balloons.

笔者这里详细向大家介绍一下:

2.5.1 横向扩展的时间悖论

首先,OpenAI 的横向扩展需求涉及 2 个层面:

Kubernetes 集群层面的 Node 扩展,通过 Cluster AutoScaler 实现(这里 OpenAI 用的是自己开发的,一般各个公有云都提供对应的 Cluster AutoScaler 插件), 为的是快速横向扩展 Node
Pod 层面的 横向扩展 (HPA), 为的是快速横向 Pod 的数量。

但是,在流量或业务量飙升的情况下,Node (也就是云虚拟机)的扩展并不像 Pod 那么迅速,一般是需要几分钟的初始化和启动的时间,进而影响到 Pod 的横向扩展,导致无法及时响应业务飙升的需求。

2.5.2 解决方案概述

为此,解决方案就是 CPU & GPU balloons, 具体如下:

在新 Node 上创建新 Pod 所需的时间由四个主要因素决定:

HPA(Horizontal Pod Autoscaler) 响应时间。
Cluster Autoscaler 响应时间。
Node 配置时间
Pod 创建时间

这里主要耗时是 Node 配置的时间,这主要取决于云提供商。

一个新的计算资源在 3 到 5 分钟内完成配置是很标准的。

在新 Node 上创建新 Pod 所需的时间预估需要 7 min 左右。

如果你需要一个新的 Node,你如何调整自动缩放以减少 7 分钟的缩放时间?

大部分时间花在了 Node 配置上,由于你不能改变云供应商提供资源的时间,所以就需要一个变通办法:

即:主动创建节点(超配),这样当你需要它们时,它们已经被配置好了。

始终确保有一个备用节点可用

    创建一个节点并将其留空(其实是放置一个 balloon pod 来占用该节点)。
    如果空节点中有 Pod (非 balloon pod) 就会创建另一个空节点。

这里,可以运行具有足够 requests 的 deployment 来保留整个节点。

可以将此 pod 视为占位符 — 它旨在保留空间,而不是使用任何资源。

一旦创建了一个真正的 Pod,您就可以逐出占位符并部署 Pod。

具体怎么实现呢?

2.5.3 具体实现

具有 requests 的 Pod
Pod 部署优先级 (PodPriorityClass) 和抢占

如果您的节点实例是 2 vCPU 和 8GB 内存,那么 Pod 的可用空间应该为 1.73 vCPU 和 ~5.9GB 内存 (OS, kubelet, kubeproxy 等需要预留一定资源),以使得 Pod 独占该 Node。

具体资源需求可以如下:

apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
containers:
- name: pause
image: registry.k8s.io/pause
resources:
requests:
cpu: '1739m'
memory: '5.9G'

然后,要确保在创建真正的 Pod 后立即逐出该 Pod,需要使用 优先级和抢占。

Pod 优先级表示 Pod 相对于其他 Pod 的重要性。

当无法调度 Pod 时,调度程序会尝试抢占(逐出)优先级较低的 Pod 来调度挂起的(优先级较高的) Pod。

可以使用 PodPriorityClass 在集群中配置 Pod 优先级:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: overprovisioning
value: -1
globalDefault: false
description: 'Priority class used by overprovisioning.'

由于 Pod 的默认优先级是 0,而超配的 PriorityClass 的值是 -1,所以当集群的空间耗尽时,这个 Pod 会被首先驱逐。

之前的 Deployment 可以调整为:(spec 中 增加 priorityClassNameoverprovisioning)

apiVersion: apps/v1
kind: Deployment
metadata:
name: overprovisioning
spec:
replicas: 1
selector:
matchLabels:
run: overprovisioning
template:
metadata:
labels:
run: overprovisioning
spec:
priorityClassName: overprovisioning
containers:
- name: reserve-resources
image: registry.k8s.io/pause
resources:
requests:
cpu: '1739m'
memory: '5.9G'

当集群中没有足够的资源时,占位的 pod 会被抢占,新的 pod 会取代它们的位置。

由于占位 pod 变得不可调度,它迫使 Cluster Autoscaler 向集群添加更多节点。

三、总结

本文通过学习 OpenAI 博客中 Kubernetes 相关博文,学到了很多超大规模 Kubernetes 集群下的调优技巧。

在这里梳理了 OpenAI 遇到的性能问题及解决方案,同时就横向扩展小技巧(占位) 进行了详细说明。

同时,结合笔者的经验,也做出一些延伸思考:

在 Kubernetes 集群中,

Metrics 监控:推荐使用 VictoriaMetrics 和 Grafana Labs 发布的 Mimir 替换 Prometheus
镜像工具:推荐使用 DragonFly, 利用 P2P 和预热功能缓解镜像拉取问题
在容器/批处理编排调度解决方案中,可以尝试选择 HashiCorp 的 Nomad 替换 Kubernetes.

以上。

参考文档

Scaling Kubernetes to 2,500 nodes (openai.com)
Scaling Kubernetes to 7,500 nodes (openai.com)
Prometheus 性能调优 - 什么是高基数问题以及如何解决?- 东风微鸣技术博客 (ewhisper.cn)
大规模 IoT 边缘容器集群管理的几种架构 -2-HashiCorp 解决方案 Nomad - 东风微鸣技术博客 (ewhisper.cn)
Architecting Kubernetes clusters — choosing the best autoscaling strategy (learnk8s.io)
Grafana 系列文章(一):基于 Grafana 的全栈可观察性 Demo(包括 Mimir) - 东风微鸣技术博客 (ewhisper.cn)

三人行, 必有我师; 知识共享, 天下为公. 本文由东风微鸣技术博客 EWhisper.cn 编写.

OpenAI Kubernetes 相关博文读后笔记的相关教程结束。

《OpenAI Kubernetes 相关博文读后笔记.doc》

下载本文的Word格式文档,以方便收藏与打印。