揭秘 ChatGPT 背后的技术栈:将Kubernetes扩展到2500个节点
- etcd
- Kube masters
- Docker image 推送
- Networking
- ARP cache
在本文中,OpenAI 的工程师团队分享了他们在 Kubernetes 集群扩展过程中遇到的各种挑战和解决方案,以及他们取得的性能和效果。
我们已经运行Kubernetes进行深度学习研究七年多了。虽然我们最大规模的工作负载直接管理裸云虚拟机,但Kubernetes提供了快速迭代周期、合理的可伸缩性和缺乏样板,这使它成为我们大多数实验的理想选择。我们现在运营着几个Kubernetes集群(一些在云中,一些在物理硬件上),其中最大的一个已经扩展到超过2500个节点。该集群运行在Azure中D15v2和NC24虚拟机的组合上。
在达到这种规模的过程中,许多系统组件都造成了破坏,包括etcd、Kube master、Docker映像提取、网络、KubeDNS,甚至是我们机器的ARP缓存。我们觉得分享一下我们遇到的具体问题,以及我们是如何解决它们的,会很有帮助。
etcd
在我们的集群中超过500个节点后,我们的研究人员开始报告kubectl命令行工具的定期超时。我们尝试添加更多的Kube主(运行Kube -apiserver的虚拟机)。这似乎暂时解决了问题,但当我们通过10个副本时,我们知道我们只是在治疗症状,而不是原因(相比之下,GKE使用单个32核VM用于500个节点)。
这让我们强烈怀疑我们的etcd集群,这是Kube大师的中央状态存储。在Datadog中,我们看到在运行ou的DS15v2机器上,写入延迟激增至数百毫秒
在对fio的性能进行基准测试时,我们看到etcd只能使用大约10%的可用IOPS,因为写延迟为2ms,而etcd执行顺序I/O,这使得它受到延迟的限制。
然后,我们将每个节点的etcd目录移动到本地临时磁盘,这是一个直接连接到实例的SSD,而不是网络连接的SSD。切换到本地磁盘将写入延迟提高到200us,并且etcd恢复正常!
我们的集群运行良好,直到超过1000个节点,此时我们再次看到etcd的高提交延迟。这一次,我们注意到了kube-apiservers
另一个有用的调整是将Kubernetes事件存储在一个单独的etcd集群中,这样事件创建的峰值就不会影响主etcd实例的性能。要做到这一点,我们只需要设置——etcd-servers-overrides标志,就像这样:——etcd-servers-overrides=/events#https://0.example.com:2381; - https://1.example.com:2381; - https://2.example.com:2381
另一个超过1,000个节点的失败是达到etcd的硬存储限制(默认为2GB),这导致它停止接受写入。这引发了一个级联故障:我们所有的Kube节点都没有通过健康检查,我们的自动缩放也失败了
Kube masters
我们将kube-apiserver、kube-controller-manager和kube-scheduler进程放在同一台机器上。对于高可用性,我们总是至少有2个主服务器,并将——apiserver-count标志设置为我们正在运行的apiserver的数量(否则Prometheus监视可能会在实例之间混淆)。
我们主要使用Kubernetes作为批调度系统,并依赖于我们的自动缩放器来动态地扩展和缩小我们的集群——这让我们大大降低了空闲节点的成本,同时在快速迭代的同时仍然提供了低延迟。默认的kube-scheduler策略是to s
{
"kind" : "Policy",
"apiVersion" : "v1",
"predicates" : [{"name" : "GeneralPredicates"},{"name" : "MatchInterPodAffinity"},{"name" : "NoDiskConflict"},{"name" : "NoVolumeZoneConflict"},{"name" : "PodToleratesNodeTaints"}],
"priorities" : [{"name" : "MostRequestedPriority", "weight" : 1},{"name" : "InterPodAffinityPriority", "weight" : 2}]
}
我们广泛地使用KubeDNS进行服务发现,但在推出新的调度策略后不久,它就开始出现可靠性问题。我们发现故障只发生在KubeDNS的某些舱上。使用新的调度策略,一些机器最终运行了10多个KubeDNS副本,创建了热点,并且我们已经超过了每个Azure虚拟机用于外部域查找的~200QPS。
我们通过在我们的KubeDNS pod中添加一个反亲和规则来解决这个问题:
affinity:podAntiAffinity:requiredDuringSchedulingIgnoredDuringExecution:- weight: 100labelSelector:matchExpressions:- key: k8s-appoperator: Invalues:- kube-dnstopologyKey: kubernetes.io/hostname
Docker image 推送
我们的Dota项目是在Kubernetes上开始的,随着规模的扩大,我们注意到新的Kubernetes节点经常长时间处于Pending状态。游戏图像大约17GB,通常需要30分钟才能拉出一个新的集群节点,所以我们理解为什么Dota容器会有一段时间处于Pending状态——但其他容器也是如此。深入研究后,我们发现kubelet有一个——serialize-image- pulled标志,默认值为true,这意味着Dota图像拖动阻塞了所有其他图像。更改为false需要切换Docker到overlay2而不是AUFS。
为了进一步加速拉取,我们还将Docker根目录移动到实例附加的SSD上,就像我们对etcd机器所做的那样。即使在优化了拉速之后,我们也看到pods无法启动,并出现了一个神秘的错误消息:rpc error: code = 2 desc = net/http: request cancelled。kubelet和Docker日志还包含了一些消息,表明由于缺乏进展,映像拉取已经被取消。我们将根跟踪到需要太长时间来提取/提取的大图像,或者当我们有很长时间的图像积压需要提取时。
为了解决这个问题,我们将kubelet的——image-pull-progress-deadline标记设置为30分钟,并将Docker守护进程的max-concurrent-downloads选项设置为10。(第二个选项并没有加快大图像的提取速度,但允许并行地提取图像队列。)我们上次的Docker拉问题是由于谷歌容器注册。默认情况下,kubelet从gcr中提取一个特殊的图像。IO(由——pod-infra-container-image标志控制),在启动任何新容器时使用。如果由于某种原因而失败,比如超出了配额,那么该节点将无法启动任何容器。因为我们的节点要经过NAT才能到达gcr。而不是拥有自己的公共IP,我们很可能会达到每个IP配额的限制。为了解决这个问题,我们使用Docker image save -o /opt/preloaded_docker_images.tar和Docker image load -i /opt/preloaded_docker_images.tar在机器镜像中为我们的Kubernetes worker预加载Docker镜像。为了提高性能,我们对常见openai内部映像(如Dota映像)的白名单进行了同样的操作。
Networking
随着实验规模的扩大,它们也变得越来越复杂,严重依赖网络进行操作。当我们第一次开始运行分布式实验时,很明显我们的网络没有配置好。直接在机器之间,我们获得了10-15Gbit/s的吞吐量,但我们使用法兰绒的Kube吊舱的最大吞吐量达到了约2Gbit/s。Machine Zone的公共基准测试显示了类似的数字,这意味着问题不可能只是配置错误,而是我们的环境固有的问题。(相比之下,Flannel不会在物理机器上增加这种开销。)
为了解决这个问题,用户可以添加两个不同的设置来禁用他们的pod: hostNetwork: true和dnpolicy: ClusterFirstWithHostNet。(在此之前请阅读Kubernetes文档中的警告。)
ARP cache
尽管进行了DNS调优,我们仍然看到DNS解析的间歇性问题。有一天,一名工程师报告说,他们的Redis服务器需要30秒以上才能打印出连接已经建立。我们追踪到内核的ARP协议栈。
对Redis pod主机的初步调查显示网络存在严重问题:任何端口上的通信都挂起了数秒,并且无法通过本地dnsmasq守护进程解析DNS名称,而dig只打印了一个神秘的失败消息:socket.c:1915: internal_send: 127.0.0.1#53: Invalid argument。dmesg日志提供了更多信息:邻居表溢出!这意味着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
在HPC集群中调优此设置是常见的,在Kubernetes集群中尤其相关,因为每个pod都有自己的IP地址,这会消耗ARP缓存中的空间。我们的Kubernetes集群已经有3个月没有发生事故了,我们计划在2018年扩展到更大的集群。我们最近升级到1.8.4版本,很高兴看到它现在正式支持5000。