Kubernetes节点扩容规模至2500的历程


【编者的话】为了尽快解决复杂问题,企业一般会选择增加服务器节点作为解决方案。在很多情况下,资源的额外增加总是能换来时间上的回报。然而,随着节点的增加与之带来的问题也会接踵而至。因为所有的问题都是存在瓶颈的。本文作者尝试使用Kubernetes作为深度学习的工具之一,随着实验的案例的增多,节点膨胀,也遇到一些同样的问题。文章是作者自己关于扩容到2500节点过程中的一些踩坑经验。

我们使用Kubernetes做深度学习的研究已经超过两年。尽管我们最大的工作量直接管理裸云VM,但是Kubernetes提供了快速的迭代周期、具有合理的扩展性以及不含样板代码(a lack of boilerplate),所以在Kubernetes上进行的实验大多都达到了我们预期。我们现在运行着几个Kubernetes集群(一些在云上,一些在物理机机上),其中最大的已经超过2500节点。这些集群运行在Azure提供的D15v2和NC24 组合虚拟机上。

达到现在这个节点规模,我们也经历过很多问题。比如许多系统组件引起的问题:包括etcd、Kubernetes的master节点、Docker镜像获取问题、Network问题、KubeDNS,甚至是我们机器上的ARP缓存。我认为分享这些我们遇到的具体问题,以及我们是怎么解决这些问题是很有意义的。

etcd

在我们集群到500个节点之后,我们的researchers开始从kubectl命令行工具得到定时超时的警告。 我们尝试添加更多Kubernetes master(运行kube-apiserver的VM)。 这似乎暂时解决了这个问题,但是一连经历10次后,我们意识到这只是在处理症状,而没发现真正的原因(相比之下,GKE使用32位单核虚拟机支撑500个节点)。

这让我们非常怀疑是提供Kube master中央状态存储区的etcd集群出了问题。 从Datadog看来,尽管每台机器都使用能够达到5,000 IOPS的P30 SSD,但是在运行我们的etcd副本的DS15v2机器上,我们看到写入有几百毫秒的延迟。
1.png

这些延迟峰值阻塞了整个集群!

fio的基准测试中,我们发现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(例如,现在已解决的问题)。 我们只是简单地改变了这些调用的过程,使apiservers的负载变得稳定。
2.png

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

我们在同一台机器上对kube-apiserver、kube- controllermanagerkube-scheduler进程进行定位。对于高可用性,我们总是至少有2个master,并将apiserver-count标志设置为我们正在运行的apiservers的数量(否则,Prometheus监视会在实例间混淆)。

我们主要使用Kubernetes作为批量调度系统,并依靠我们的自动调节器动态扩容和缩容我们的集群——这使我们可以显著降低空闲节点的成本,同时在快速迭代时仍然保证低延迟。 默认的kube-scheduler策略是在负载均匀分布在节点之间。但是我们希望相反,这样可以终止未使用的节点,也可以快速调度大的Pods。 所以我们切换到以下策略:
{
"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的某些Pods上。 在新的调度策略下一些机器最终运行了10多个KubeDNS副本,创建了热点,而且我们已经超过了每个Azure虚拟机允许的查询〜200QPS限制。

我们通过为KubeDNS Pod添加一个anti-affinity规则来解决这个问题:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- weight: 100
 labelSelector:
   matchExpressions:
   - key: k8s-app
     operator: In
     values:
     - kube-dns
 topologyKey: kubernetes.io/hostname

Docker image pulls

我们的Dota项目是在Kubernetes上起步的,随着规模的扩大,我们注意到新增的Kubernetes节点经常有很长一段时间处在Pending。 游戏镜像大概是17GB,通常需要30分钟才能拉上一个新的集群节点,所以我们理解了Dota容器为什么会暂停一段时间——但是其他容器也是如此。 进一步挖掘,我们发现kubelet有一个--serialize-image-pullsflag默认为true,这意味着Dota镜像阻塞了所有其他镜像。 更改为false需要将Docker切换到overlay2而不是AUFS。 为了进一步提高获取镜像的速度,我们也将Docker root 移到了实例连接的SSD上,就像我们为etcd机器所做的那样。

即使在优化获取镜像速度之后,我们也看到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

随着我们的实验规模越来越大,它们也变得越来越复杂,这些系统在很大程度上依赖于网络的运作。当我们第一次开始运行分布式实验时,很明显我们的网络并没有被很好地配置。在机器之间,我们得到了10-15Gbit/s的吞吐量,但是我们使用Flannel的Kube Pod在~2Gbit/s上是最大的。机器区域的公共基准测试显示了类似的数字,这意味着这个问题不太可能是糟糕的配置,而是我们的环境所固有的一些东西。(相比之下,Flannel并没有在我们的物理机器上增加这个开销。)

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

ARP Cache

尽管我们进行了DNS调优,但我们仍然看到DNS解析的间歇性问题。有一天,一位工程师报告说,nc -v到他们的Redis服务器上花了30秒的时间才打印出连接。我们跟踪这个问题到内核的ARP堆栈。对Redis pod主机的初步调查显示,该网络出现了严重的问题:任何端口上的通信都被挂了好几秒钟,而且没有DNS名称可以通过本地的dnsmasq守护进程来解决,而dig命令只是打印一个加密的失败消息:socket。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版本,并且很高兴看到它现在正式支持5000。

原文链接:scaling-kubernetes-to-2500-nodes(翻译:ylzhang)

0 个评论

要回复文章请先登录注册