Kubernetes达到2500个节点会发生什么-来自OpenAI团队的分享

Kubernetes OpenAI

分享者: OpenAi

翻译: suntiger

1.摘要

我们已经运行 Kubernetes 进行深度学习研究超过两年了。虽然我们最大规模的工作负载直接管理裸露的云虚拟机,但Kubernetes提供了快速的迭代周期、合理的可扩展性以及无模板代码的特性,使其成为我们大多数实验的理想选择。我们现在操作了几个Kubernetes集群(一些在云中,一些在物理硬件上),其中最大的一个已经扩展到超过 2,500个节点。这个集群在Azure上运行,采用D15v2和NC24虚拟机的组合。

注:当扩充到7500个节点时会面临新的问题,可继续关注在7500个节点下带来的新的挑战: 挑战Kubernetes扩充到7500个节点-OpenAI的技术进化之路

在达到这个规模的过程中,许多系统组件导致了故障,包括 etcd、Kube 主节点、Docker 镜像拉取、网络、KubeDNS,甚至还包括我们机器的ARP缓存。我们觉得分享我们遇到的具体问题以及如何解决它们会很有帮助。

2.关于etcd节点

在集群规模超过500个节点后,我们的研究人员开始报告来自 kubectl命令行工具的常规超时。我们尝试增加更多的 Kube主节点(运行 kube-apiserver 的虚拟机)。这似乎暂时解决了问题,但是当我们超过10个副本时,我们知道我们只是在治标而非治本(相比之下,GKE 对于500个节点使用单个32核虚拟机)。

这让我们强烈怀疑是我们的etcd集群出了问题,etcd集群是Kube主节点的中心状态存储。在Datadog中查看时,我们发现运行etcd副本的DS15v2机器上的写入延迟飙升至数百毫秒,尽管每台机器都使用了一块具有5,000IOPS性能的 P30 SSD。

使用fio对性能进行基准测试时,我们发现etcd由于写入延迟为2ms并且执行顺序I/O,因此只能利用大约10%的可用 IOPS,这使得它受到延迟的限制。

接下来,我们将每个节点的etcd目录移动到本地临时磁盘,这是一个直接连接到实例的SSD,而不是网络连接的磁盘。切换到本地磁盘将写入延迟降低到200us,etcd变得健康了!

在我们的集群规模超过1,000个节点时,一切运行良好,此时我们再次看到etcd的高提交延迟。这次,我们注意到 kube-apiservers从etcd读取的数据速率超过500MB/s。我们设置了Prometheus来监控apiservers,并设置了 --audit-log-path和--audit-log-maxbackup标志以在apiserver上启用更多日志记录。这暴露出一些缓慢的查询和对Events的LIST API的过量调用。

根本原因:Fluentd 和 Datadog 的监控进程的默认设置是从集群中的每个节点查询 apiservers(例如,这个现已修复的问题)。我们只需将这些进程的轮询设置得不那么激进,apiservers 上的负载就再次稳定下来:

另一个有利的调整是将Kubernetes事件存储在单独的etcd集群中,这样在事件创建中的峰值不会影响主etcd实例的性能。要做到这一点,我们只需将 --etcd-servers-overrides 标志设置为类似于以下内容的值:--etcd-servers-overrides=/events# 0.example.com:2381;­https://1.example.…

在规模超过1,000个节点之后,另一个失败是触及etcd的硬存储限制(默认为 2GB),导致它停止接受写入操作。这触发了级联故障:所有的 Kube 节点都未通过健康检查,而我们的自动扩展器因此认为需要终止所有工作节点。我们使用 --quota-backend-bytes标志增加了最大etcd大小,现在自动扩展器已经添加了一个完整性检查,以防止在终止超过集群 50% 的节点时采取行动。

3.关于Kube主节点

我们在同一台机器上共同定位kube-apiserver、kube-controller-manager 和 kube-scheduler进程。为了高可用性,我们始终至少拥有2个主节点,并将--apiserver-count 标志设置为我们正在运行的apiserver数量(否则Prometheus监控可能在实例之间产生混淆)。

我们主要使用Kubernetes作为批处理调度系统,并依赖我们的自动扩展器来动态地扩展和缩小集群规模——这使我们能够显著降低空闲节点的成本,同时在快速迭代时仍能提供低延迟。默认的 kube-scheduler 策略是在节点之间均匀分配负载,但我们希望实现相反的效果,以便未使用的节点可以被终止,同时大型 Pod 可以快速调度。因此,我们切换到了以下策略:

{ "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 开始出现可靠性问题。我们发现故障仅发生在KubeDNS的某些 Pod 上。由于新的调度策略,一些机器最终运行了10个或更多的KubeDNS副本,从而创建了热点,我们已经超过了每个AzureVM 允许的大约200QPS的外部域名查找。

我们通过为KubeDNS Pod添加反亲和性规则来解决这个问题:

ffinity: 
  podAntiAffinity: 
   requiredDuringSchedulingIgnoredDuringExecution: 
   - weight: 100 
   labelSelector: 
       matchExpressions: 
       - key: k8s-app 
       operator: In 
       values: 
       - kube-dns 
   topologyKey: kubernetes.io/hostname

4.关于Docker镜像拉取

我们的Dota项目最初是在Kubernetes上启动的,随着规模的扩大,我们注意到新的 Kubernetes节点上的 Pod 经常处于 Pending 状态很长时间。游戏镜像大约为17GB,通常需要在新的集群节点上花费30分钟才能完成拉取,所以我们理解为什么 Dota 容器会处于Pending状态一段时间——但其他容器也是如此。深入挖掘,我们发现 kubelet 有一个默认为 true 的 --serialize-image-pulls 标志,这意味着 Dota 镜像拉取阻塞了所有其他镜像。将其更改为 false 需要将 Docker 从 AUFS 切换到 overlay2。为了进一步加快拉取速度,我们还将 Docker 根目录移动到与实例关联的 SSD 上,就像我们为 etcd 机器所做的那样。

即使在优化了拉取速度之后,我们还是看到Pod无法启动,并显示了一个神秘的错误消息:rpc error: code = 2 desc = net/http: request canceled。kubelet 和 Docker 日志还包含表明镜像拉取已被取消的消息,原因是进度不足。我们将根源追踪到大型镜像拉取/提取时间过长,或者需要拉取的镜像队列过长。为解决这个问题,我们将kubelet的--image-pull-progress-deadline 标志设置为30分钟,并将Docker守护程序的max-concurrent-downloads选项设置为10。(第二个选项并没有加快大型镜像的提取速度,但允许并行拉取镜像队列。)

我们最后一个Docker拉取问题是由于Google容器注册表。默认情况下,kubelet从 gcr.io拉取一个特殊的镜像(由 --pod-infra-container-image 标志控制),该镜像在启动任何新容器时使用。如果由于任何原因(如超过配额)导致该拉取失败,该节点将无法启动任何容器。因为我们的节点是通过NAT访问 gcr.io,而不是拥有自己的公共 IP,所以我们很可能会触及这个每个IP配额限制。为了解决这个问题,我们简单地在我们的 Kubernetes 工作节点的机器镜像中预加载了那个Docker镜像,方法是使用docker image save -o /opt/preloaded_docker_images.tar 和 docker image load -i /opt/preloaded_docker_images.tar。为了提高性能,我们对Dota镜像等OpenAI内部常用镜像的白名单执行相同操作。

5.关于网络

随着我们的实验规模不断扩大,它们也变得越来越复杂,成为依赖网络进行操作的分布式系统。当我们开始运行分布式实验时,很明显我们的网络配置并不理想。在机器之间,我们可以获得10-15Gbit/s的吞吐量,但是使用Flannel 的 Kube Pod 最高只能达到约 2Gbit/s。Machine Zone 的公开基准测试显示了类似的数字,这意味着问题不太可能仅仅是错误的配置,而是我们环境中固有的某些因素。(相比之下,Flannel在我们的物理机器上没有增加这种开销。)

为了解决这个问题,用户可以添加两个不同的设置来禁用他们的Pod的 Flannel:hostNetwork: true 和 dnsPolicy: ClusterFirstWithHostNet。(尽管在执行此操作之前请阅读 Kubernetes 文档中的警告。)

6.关于ARP缓存

尽管我们对DNS进行了调优,但我们仍然不时遇到DNS解析问题。有一天,一位工程师报告说,对他们的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

在HPC集群中调整此设置很常见,尤其是在 Kubernetes集群中,因为每个Pod都有自己的IP地址,这会消耗ARP缓存中的空间。我们的Kubernetes集群已经运行稳定约3个月:

微信扫码立即使用「源自下载」小程序

「源自下载」小程序二维码

Copyright © 2019-2024 源自下载