我们使用Kubernetes做深度学习的研究已经超过两年。尽管我们最大的工作量直接管理裸云VM,但是Kubernetes提供了快速的迭代周期、具有合理的扩展性以及不含样板代码(a lack of boilerplate),所以在Kubernetes上进行的实验大多都达到了我们预期。我们现在运行着几个Kubernetes集群(一些在云上,一些在物理机机上),其中最大的已经超过2500节点。这些集群运行在Azure提供的D15v2和NC24 组合虚拟机上。
达到现在这个节点规模,我们也经历过很多问题。比如许多系统组件引起的问题:包括etcd、Kubernetes的master节点、Docker镜像获取问题、Network问题、KubeDNS,甚至是我们机器上的ARP缓存。我认为分享这些我们遇到的具体问题,以及我们是怎么解决这些问题是很有意义的。
etcd
这让我们非常怀疑是提供Kube master中央状态存储区的etcd集群出了问题。 从Datadog看来,尽管每台机器都使用能够达到5,000 IOPS的P30 SSD,但是在运行我们的etcd副本的DS15v2机器上,我们看到写入有几百毫秒的延迟。
这些延迟峰值阻塞了整个集群!
在fio[1]的基准测试中,我们发现etcd只能使用大约10%的IOPS,因为写延迟是2ms,etcd是连续的I/O,因此引起一连串的延迟。
然后,我们将每个节点的etcd目录移动到本地临时磁盘,这是一个直接连接到实例的SSD,而不是网络连接。切换到本地磁盘带后写延迟达到200us,etcd恢复正常!
直到我们达到大约1,000个节点以前,我们的集群运行良好。在1,000这一点上,我们再次看到了etcd的提交高延迟。这一次,我们注意到kube-apiservers从etcd上读取了超过500MB/s。我们设置了Prometheus来监视apiservers,还设置了--audit-log-path和--audit-log-maxbackup标志,以便在apiserver上启用更多的日志记录。这就出现了一些缓慢的查询和对事件列表API的过度调用。
根本原因:Fluentd和Datadog监控进程的默认设置是从集群中的每个节点查询apiservers(例如,现在已解决的问题[2])。 我们只是简单地改变了这些调用的过程,使apiservers的负载变得稳定。
etcd出口从500MB / s 下降到几乎为0(上图中的负值表示出口)
另一个有用的调整是将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节点都健康检查失败,我们的autoscaler决定它需要终止所有的任务。 我们用--quota-backend-bytes标志增加了max etc的大小,现在autoscaler有了一个智能的检查,如果它终止超过50%的集群,不会采取行动。
Kube masters
我们主要使用Kubernetes作为批量调度系统,并依靠我们的自动调节器动态扩容和缩容我们的集群——这使我们可以显著降低空闲节点的成本,同时在快速迭代时仍然保证低延迟。 默认的kube-scheduler策略是在负载均匀分布在节点之间。但是我们希望相反,这样可以终止未使用的节点,也可以快速调度大的Pods。 所以我们切换到以下策略:
我们的服务发现功能广泛使用KubeDNS,但在推出新的调度策略后不久就开始出现可靠性问题。 我们发现,失败只发生在KubeDNS的某些Pods上。 在新的调度策略下一些机器最终运行了10多个KubeDNS副本,创建了热点,而且我们已经超过了每个Azure虚拟机允许的查询〜200QPS限制。
我们通过为KubeDNS Pod添加一个anti-affinity规则[3]来解决这个问题:
Docker image pulls
即使在优化获取镜像速度之后,我们也看到Pod无法启动一个诡异的错误信息:rpc error: code = 2 desc = net/http: request canceled。 kubelet和Docker日志消息显示“由于缺乏进度,镜像的获取已经取消”。 我们追踪了问题的根源是需要花费太多时间来获取/加压提取的大镜像,或者当我们很多积压的镜像要获取的时候。 为了解决这个问题,我们将kubelet的-image-pull-progress-deadline标志设置为30分钟,并将Docker守护进程的最大并发下载选项设置为10。(第二个选项没有加速获取大镜像,但允许镜像队列并行获取。)
我们最后一个Docker问题是由于Google Container Registry造成的。 默认情况下,kubelet从gcr.io获取特殊的镜像(由--pod-infra-container-image标志控制)gcr.io经常用于创建一个新的容器。 如果因为任何原因(例如超过配额)而导致失败,该节点将无法启动任何容器。 由于我们的节点通过NAT到达gcr.io而不是拥有自己的公有IP,所以我们很可能会达到这个每IP配额的限制。 为了解决这个问题,我们通过使用docker image save -o /opt/preloaded_docker_images.tar和docker image load -i /opt/preloaded_docker_images.tar,简单地在我们的Kubernetes worker的机器镜像中预先加载了Docker镜像。 为了提高性能,我们对于像Dota镜像这样的常见OpenAI内部图像的白名单也是这样做的。
Networking
为了解决这个问题,用户可以添加两个不同的设置来禁用他们的Pod:hostNetwork: true和dnsPolicy: ClusterFirstWithHostNet。(在此之前,请阅读Kubernetes文档中的警告[5]。)
ARP Cache
在HPC集群中调优这个设置是很常见的,并且在Kubernetes集群中尤其重要,因为每个Pod都有自己的IP地址,它占用了ARP缓存中的空间。
我们的Kubernetes集群已经有3个月的历史了,我们计划在2018年扩展到更大的集群。我们最近升级到1.8.4版本,并且很高兴看到它现在正式支持5000。
相关链接:
https://github.com/axboe/fio
https://github.com/DataDog/dd-agent/issues/3381
https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
http://machinezone.github.io/research/networking-solutions-for-kubernetes/
https://kubernetes.io/docs/concepts/configuration/overview/