最近 ChatGPT 和其公司 OpenAI 特别火:ChatGPT 3, ChatGPT 3.5, New Bing, ChatGPT 4...
怀着学习的心态,这几天访问了 OpenAI 的博客, 上边关于 AI 的内容,确实隔行如隔山,完全看不明白。😂
但是翻看过程中,惊喜发现有 2 篇与 Kubernetes 使用相关的文章:
这不碰到老本行了嘛,学习下~
以下为读后笔记,也加入了自己的思考:针对 OpenAI 现状,如何进一步优化监控、镜像拉取、容器编排相关架构。
💪💪💪
Dota2 游戏镜像大约是 17GB😛
Kubernetes 在 OpenAI 主要用于深度学习,主要使用的是 Kubernetes Job.
Kubernetes 提供了
集群 500 节点后,刚开始是 kubectl 使用卡;另外通过 DataDog 监控发现磁盘写入延迟飙升至数百 ms,尽管每台机器都使用 30,5 IOPS 的 P000 SSD。
后面解决之后,加到 1000 节点,发现 etcd 延迟再次变高;发现 kube-apiservers 从 etcd 读取的速率超过 500MB/s
另一个 1,000 个节点后的故障是达到了 etcd 的硬存储限制(默认为 2GB),这导致它停止接受写入。
--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 节点。
在 7500 节点时,有 5 个 API 服务器,并且每个 API 服务器使用的堆内存高达 70GB.
Dota 容器会在一段时间内处于 pending 状态——但对于其他容器也是如此。
解决之后,发现有报错:rpc error: code = 2 desc = net/http: request canceled
, 表明由于缺乏进度,镜像拉取已被取消。
还有个问题,OpenAI 的 Kubernetes 组件镜像是默认从 gcr.io
拉取的,但是 gcr.io
可能失败或超出配额(机器用的 NAT 公网 IP 是同一个,很容易超出配额).
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.tar
和docker image load -i /opt/preloaded_docker_images.tar
,在 Kubernetes worker 的机器镜像中预装了这些 Docker 镜像。 为了提高性能,我们对常见的 OpenAI 内部镜像如 Dota 镜像的白名单做了同样的处理。
关于 OpenAI 碰到的 Docker 镜像拉取的问题,都是非常典型的大规模 Kubernetes 会碰到的问题,其实有更好的解决方案:P2P 镜像解决方案,典型就是 DragonFly.
DragonFly 提供高效、稳定、安全的基于 P2P 技术的文件分发和镜像加速系统,并且是云原生架构中镜像加速领域的标准解决方案以及最佳实践。其最大的优势就是:
Flannel 在这种超大规模场景下肯定是撑不住的,刚开始 OpenAI 采用了非常简单暴力的解决方案(也适合他们的使用场景): pod 配置使用 HostNetwork:
...
hostNetwork: true
...
dnsPolicy: ClusterFirstWithHostNet
后面是改为使用 Azure 的 VMSSes CNI 插件。
其实 OpenAI 对 Kubernetes 的刚需是:容器编排,网络功能不是刚需,OpenAI 不用 Kubernetes CNI 也可以的。后面会延伸讨论一下。
另外一个可能经常会忽略的点是 ARP 缓存问题。
有一天,一位工程师报告说,他们的 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 地址)的。
在/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 缓存的空间。
由于大量的采集和查询,Prometheus 和 Grafana OOM 的频率也不低。
GOMAXPROCS=24
配置,在 WAL replay 期间,Prometheus 试图使用所有的内核,对于拥有大量内核的服务器来说,这种争夺会扼杀所有的性能。这里详细描述一下 OpenAI Kubernetes 的使用场景:
看完 Scaling Kubernetes to 7,500 nodes (openai.com) 这篇,其实会发现 OpenAI 对 Kubernetes 的使用和普通 IT 公司差异还是比较大的。
OpenAI 最主要用的是:Kubernetes 的容器编排, 特别是对 Job 的调度能力。
其他 Kubernetes 功能,用的很少或几乎没有,如:
所以我个人认为(观点仅供参考), Kubernetes 对于 OpenAI 来说,还是有些过于复杂和功能冗余的。
OpenAI 真正需要的,是一个纯粹的容器编排解决方案,特别是对 Job 的调度能力。
所以我觉得啊,不考虑用户规模,不考虑 Kubernetes 是容器编排领域的事实上的标准的话,HashiCorp 的 Nomad 反而是更合适的解决方案。以下是具体理由:
另外,惊喜的发现 OpenAI 的博文中竟然提到了 Kubernetes 横向扩展的小技巧,OpenAI 将其称为:CPU & GPU balloons.
笔者这里详细向大家介绍一下:
首先,OpenAI 的横向扩展需求涉及 2 个层面:
但是,在流量或业务量飙升的情况下,Node (也就是云虚拟机)的扩展并不像 Pod 那么迅速,一般是需要几分钟的初始化和启动的时间,进而影响到 Pod 的横向扩展,导致无法及时响应业务飙升的需求。
为此,解决方案就是 CPU & GPU balloons, 具体如下:
在新 Node 上创建新 Pod 所需的时间由四个主要因素决定:
这里主要耗时是 Node 配置的时间,这主要取决于云提供商。
一个新的计算资源在 3 到 5 分钟内完成配置是很标准的。
在新 Node 上创建新 Pod 所需的时间预估需要 7 min 左右。
如果你需要一个新的 Node,你如何调整自动缩放以减少 7 分钟的缩放时间?
大部分时间花在了 Node 配置上,由于你不能改变云供应商提供资源的时间,所以就需要一个变通办法:
即:主动创建节点(超配),这样当你需要它们时,它们已经被配置好了。
始终确保有一个备用节点可用:
这里,可以运行具有足够 requests 的 deployment 来保留整个节点。
可以将此 pod 视为占位符 — 它旨在保留空间,而不是使用任何资源。
一旦创建了一个真正的 Pod,您就可以逐出占位符并部署 Pod。
具体怎么实现呢?
如果您的节点实例是 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 中 增加 priorityClassName
为 overprovisioning
)
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 遇到的性能问题及解决方案,同时就横向扩展小技巧(占位🎈) 进行了详细说明。
同时,结合笔者的经验,也做出一些延伸思考:
以上。
三人行, 必有我师; 知识共享, 天下为公. 本文由东风微鸣技术博客 EWhisper.cn 编写.