OpenAI研究 将 Kubernetes 扩展到 2,500 个节点
两年多来,我们一直在 运行 Kubernetes进行深度学习研究。虽然我们最大规模的工作负载直接管理裸云虚拟机,但 Kubernetes 提供了快速的迭代周期、合理的可扩展性和缺乏样板,这使其成为我们大多数实验的理想选择。我们现在运行着几个 Kubernetes 集群(一些在云端,一些在物理硬件上),其中最大的一个已经推送到超过 2,500 个节点。此群集在 Azure 中运行,结合 D15v2 和 NC24 VM。
在达到这种规模的过程中,许多系统组件造成了损坏,包括 etcd、Kube master、Docker 镜像拉取、网络、KubeDNS,甚至我们机器的 ARP 缓存。我们觉得分享我们遇到的具体问题以及我们如何解决这些问题会很有帮助。
等等
在我们的集群中经过 500 个节点后,我们的研究人员开始使用kubectl命令行工具报告定期超时 。我们尝试添加更多 Kube master(运行 kube-apiserver 的虚拟机)。这似乎暂时解决了问题,但是一旦我们通过了 10 个副本,我们就知道我们是在治标不治本(相比之下, GKE 对 500 个节点使用单个 32 核 VM)。
这让我们强烈怀疑我们的 etcd 集群,它是 Kube 主节点的中央状态存储。在 Datadog中,我们看到在运行 etcd 副本的 DS15v2 机器上写入延迟激增至数百毫秒,尽管每台机器都使用能够达到 5,000 IOPS 的 P30 SSD。

使用fio对性能进行基准测试 ,我们看到 etcd 只能使用大约 10% 的可用 IOPS,因为写入延迟为 2 毫秒,而 etcd 执行顺序 I/O,使其受延迟限制。
然后我们将每个节点的 etcd 目录移动到本地临时磁盘,这是一个直接连接到实例的 SSD,而不是网络连接的。切换到本地磁盘使写入延迟达到 200us,etcd 变得健康了!
我们的集群运行良好,直到我们通过了大约 1,000 个节点,此时我们再次看到来自 etcd 的高提交延迟。这一次,我们注意到 kube-apiservers 从 etcd 读取超过 500MB/s。我们设置 Prometheus 来监控 apiservers,并设置 --audit-log-path
和 --audit-log-maxbackup
标志以在 apiserver 上启用更多日志记录。这暴露出许多缓慢的查询和对事件的 LIST API 的过度调用。
根本原因: Fluentd和 Datadog 的监控进程的默认设置是从集群中的每个节点查询 apiservers(例如,这个 问题 现在已经修复)。我们只是简单地改变了这些进程,使其轮询变得不那么激进,并且 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 节点都未能通过健康检查,因此我们的 自动缩放器 决定它需要终止所有工作节点。我们已经使用标志增加了最大 etcd 大小 --quota-backend-bytes
,并且自动缩放器现在有一个完整性检查,如果它会终止超过 50% 的集群,则不会采取行动。
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 的某些 pod 上。使用新的调度策略,一些机器最终运行了 10 个以上的 KubeDNS 副本,创建了热点,并且我们已经超过了每个 Azure VM 允许的 ~200QPS 以进行外部域查找。
我们通过向 KubeDNS pod添加 反亲和性规则来解决此问题:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- weight: 100
labelSelector:
matchExpressions:
- key: k8s-app
operator: In
values:
- kube-dns
topologyKey: kubernetes.io/hostname
Docker 镜像拉取
我们的 Dota 项目从 Kubernetes 开始,随着规模的扩大,我们注意到新的 Kubernetes 节点经常有 pod 长时间处于Pending状态。游戏镜像大约为 17GB,通常需要 30 分钟才能拉入一个新的集群节点,因此我们理解了为什么 Dota 容器会等待一段时间——但其他容器也是如此。深入研究,我们发现 kubelet 有一个 --serialize-image-pulls
默认为 的标志 true
,这意味着 Dota 镜像拉取阻止了所有其他镜像。更改为 false
需要将 Docker 切换到 overlay2 而不是 AUFS。为了进一步加快拉取速度,我们还将 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 pull 问题是由于 Google Container Registry。gcr.io
默认情况下,kubelet 从(由标志控制 )中拉取一个特殊图像 --pod-infra-container-image
,在启动任何新容器时使用该图像。如果该拉取由于任何原因失败,例如超过您的 配额,该节点将无法启动任何容器。因为我们的节点通过 NAT 到达 gcr.io
而不是拥有自己的公共 IP,所以我们很可能会达到每个 IP 的配额限制。docker image save -o /opt/preloaded_docker_images.tar
为了解决这个问题,我们只需使用和 为我们的 Kubernetes 工作人员在机器映像中预加载 Docker 映像 docker image load -i /opt/preloaded_docker_images.tar
。为了提高性能,我们对常见 OpenAI 内部图像(如 Dota 图像)的白名单执行相同的操作。
联网
随着我们的实验规模越来越大,它们也变得越来越复杂,严重依赖网络进行操作的分布式系统。当我们第一次开始运行分布式实验时,很明显我们的网络配置不当。我们直接在机器之间获得了 10-15Gbit/s 的吞吐量,但是我们使用 Flannel 的 Kube pod 的吞吐量达到了 ~2Gbit/s。Machine Zone 的 公共基准测试 显示了类似的数字,这意味着问题可能不仅仅是错误的配置,而是我们环境固有的问题。(相比之下,Flannel 不会在我们的物理机器上增加这种开销。)
为了解决这个问题,用户可以添加两个不同的设置来为他们的 pod 禁用 Flannel: hostNetwork: true
和 dnsPolicy: ClusterFirstWithHostNet
. (尽管在执行此操作之前阅读 Kubernetes 文档中的警告。)
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 个月没有发生任何事故,我们计划在 2018 年扩展到更大的集群。我们最近升级到 1.8.4 版本,很高兴看到它现在正式支持 5,000 个。