为什么大约两年前,Tinder决定将其平台迁移到Kubernetes。Kubernetes让我们有机会将TinderEngineering转向容器化,并通过不可变部署减少操作。应用程序的构建、部署和基础设施将被定义为代码。我们还想解决规模和稳定性方面的挑战。当扩展变得至关重要时,我们通常不得不等待几分钟才能让新的EC2实例上线。容器在几秒钟而不是几分钟内调度和服务流量的想法吸引了我们。这并不容易。在2019年初的迁移过程中,我们的Kubernetes集群达到了临界质量,并开始遇到由于流量、集群大小和DNS引起的各种挑战。我们解决了迁移200项服务和运行Kubernetes集群的有趣挑战,该集群扩展到总共1,000个节点、15,000个Pod和48,000个正在运行的容器。如何自2018年1月以来,我们已经完成了迁移的各个阶段。我们首先将所有服务容器化,并将它们部署到一系列由Kubernetes管理的各种环境中。从10月开始,我们开始有条不紊地将所有遗留服务迁移到Kubernetes。第二年三月,我们完成了迁移,Tinder平台现在完全运行在Kubernetes上。为Kubernetes构建镜像Kubernetes集群中运行的微服务有30多个源代码存储库。这些存储库中的代码是用不同的语言(例如,Node.js、Java、Scala、Go)编写的,并且具有针对同一语言的多个运行时环境。构建系统旨在为每个微服务在完全可定制的“构建上下文”上运行,它通常由一个Dockerfile和一系列shell命令组成。尽管它们的内容是完全可定制的,但这些构建上下文是以标准格式编写的。构建上下文的标准化允许单个构建系统处理所有微服务。为了在运行时环境之间实现最大的一致性,在开发和测试阶段使用相同的构建过程。当我们需要设计一种方法来保证跨平台的构建环境一致时,这提出了一个独特的挑战。因此,所有构建过程都在一个特殊的“Builder”容器中执行。Builder容器的实现需要很多先进的Docker技术。这个Builder容器集成了访问Tinder私有存储库所需的本地用户ID和密钥(例如SSH密钥、AWS凭证等)。它挂载包含源代码的本地目录,以自然的方式存储构建工件。这种方法提高了性能,因为它消除了在构建器容器和主机之间复制构建的工件的需要。下次无需进一步配置即可再次使用存储的构建工件。对于某些服务,我们需要在Builder中创建另一个容器,以便编译时环境与运行时环境相匹配(例如,安装Node.jsbcrypt库会产生平台特定的二进制工件)。编译时要求可能因服务而异,最终的Dockerfile是动态生成的。Kubernetes集群架构和迁移集群大小我们决定使用kube-aws在AmazonEC2实例上进行自动集群配置。早期,我们在公共节点池上运行所有内容。我们很快发现需要将工作负载拆分为不同大小和类型的实例,以更好地利用资源。这样做的理由是,与让它们与大量单线程pod共存相比,将更少的高线程pod一起运行可以为我们提供更可预测的性能结果。我们决定:m5.4xlarge用于监控(Prometheus)c5.4xlarge用于Node.js工作负载(单线程工作负载)c5.2xlarge用于Java和Go(多线程工作负载)c5.4xlarge用于控制平面(3个节点)迁移从我们的遗留基础设施迁移到Kubernetes的准备步骤之一是更改现有的服务到服务通信以指向在特定虚拟私有云(VPC)子网中创建的新弹性负载平衡器(ELB)。此子网与KubernetesVPC对等。这使我们能够在不依赖特定服务依赖顺序的情况下以细粒度级别迁移模块。这些端点是使用加权DNS记录集创建的,其中CNAME指向每个新的ELB。为了切换,我们添加一条新记录指向新的Kubernetes服务ELB,权重为0。然后,将记录上的生存时间(TTL)设置为0。然后慢慢调整新旧权重,最终得到100%新服务器上的流量。转换完成后,将TTL设置为更合理的值。我们的Java模块使用低DNSTTL,但我们的Node应用程序不使用。我们的一位工程师重写了部分连接池代码,将其包装在每60秒刷新一次连接池的管理器中。这对我们来说非常有效,没有明显的性能下降。2019年1月8日凌晨坑网结构受限,Tinder平台持续下线。为了应对当天早些时候平台延迟的增加,在集群上增加了pod和节点的数量。这导致我们所有节点上的ARP缓存都用完了。与ARP缓存相关的Linux值有3个:gc_thresh3是硬上限。如果您收到“neighbortableoverflow”日志记录,则表明即使在ARP缓存的同步垃圾收集(GC)之后,也没有足够的空间来存储邻居记录。在这种情况下,内核将简单地完全丢弃数据包。我们使用Flannel作为Kubernetes中的网络结构。数据包通过VXLAN转发。VXLAN是第3层网络上的第2层覆盖方案。它使用MAC地址用户数据报协议(MAC-UDP)封装来提供扩展第2层网段的方法。物理数据中心网络上的传输协议是IP加UDP。每个Kubernetes工作节点都从更大的/9块中分配了自己的/24虚拟地址空间。对于每个节点,这将导致1个路由表条目、1个ARP表条目(在flannel.1接口上)和1个转发数据库(FDB)条目。这些是在工作节点首次启动时添加的,或者在发现每个新节点时添加的。此外,节点到Pod(或Pod到Pod)的通信最终流经eth0接口(如上面的Flannel图所示)。这将在ARP表中为每个相应的节点源和节点目标添加一个附加条目。在我们的环境中,这种通信非常普遍。对于我们的Kubernetes服务对象,将创建一个ELB,Kubernetes将向ELB注册每个节点。ELB不支持Pod,选择的节点可能不是数据包的最终目的地。这是因为当一个节点从ELB收到数据包时,它会评估其iptables服务规则并随机选择另一个节点上的pod。中断时,集群中共有605个节点。由于上述原因,这将超过默认的gc_thresh3值。一旦发生这种情况,不仅数据包会被丢弃,整个Flannel/24虚拟地址空间也会从ARP表中丢失。节点到Pod的通信和DNS查找失败。(DNS托管在集群上,本文后面会详细介绍。)要修复此问题,gc_thresh1、gc_thresh2和gc_thresh3的值将提高,并且必须重新启动Flannel以重新注册丢失的网络。coredns的意外扩展为了适应我们的迁移,我们大量利用DNS来促进流量整形和从遗留到Kubernetes的增量切换以进行服务配置。我们在关联的Route53记录集上设置了一个相对较低的TTL值。当我们在EC2实例上运行传统基础设施时,我们的解析器配置指向亚马逊的DNS。我们认为这是理所当然的,我们的服务和Amazon服务(例如DynamoDB)的TTL成本相对较低,这在很大程度上没有引起注意。随着我们将越来越多的服务加载到Kubernetes中,我们发现自己运行的DNS服务每秒响应250,000个请求。我们在应用程序中遇到间歇性和影响深远的DNS查找超时。尽管进行了详尽的调整工作,并且DNS提供商切换到CoreDNS部署,但一次达到1,000个pod,消耗了120个内核,这还是发生了。在研究其他可能的原因和解决方案时,我们发现了一篇描述影响netfilter(Linux数据包过滤框架)的竞争条件的文章。我们看到的DNS超时和Flannel接口上增加的insert_failed计数器与本文的发现一致。该问题发生在源和目标网络地址转换(SNAT和DNAT)以及随后插入conntrack表的过程中。内部讨论并由社区提出的一种解决方法是将DNS移动到工作节点本身。在这种情况下:不需要SNAT,因为流量驻留在节点本地。不需要通过eth0接口传输。不需要DNAT,因为目标IP是节点本地的,而不是基于iptables规则随机选择的pod。我们决定继续采用这种方法。CoreDNS在Kubernetes中部署为一个DaemonSet,我们通过配置kubelet-cluster-dns命令标志将节点的本地DNS服务器注入到每个Pod的resolv.conf中。解决方法适用于DNS超时。但是,我们仍然看到数据包丢失,Flannel接口的insert_failed计数器增加。即使在上述解决方法之后,这仍将持续存在,因为我们只避免DNS流量的SNAT和/或DNAT。对于其他类型的流量,竞争条件仍然可能发生。幸运的是,我们的大部分数据包都是TCP,当这种情况发生时,数据包将被成功重传。我们仍在讨论针对所有类型流量的长期解决方案。使用Envoy实现更好的负载平衡当我们将后端服务迁移到Kubernetes时,我们开始遭受pod负载不平衡的困扰。我们发现,由于HTTPkeepalive,ELB连接卡在每个滚动部署的第一个就绪pod中,因此大部分流量流经可用pod的一小部分。我们尝试的第一个缓解措施是在新部署中使用100%MaxSurge来应对最严重的违规行为。在一些较大的部署中,这在长期内效果微不足道且不可持续。我们使用的另一种缓解措施是人为地增加对关键服务的资源请求,以便与其他笨重的pod相比,位于同一位置的pod拥有更大的资源。由于资源浪费,从长远来看,这也不成立,我们的Node应用程序是单线程的,因此实际上限制为1个核心。唯一明确的解决方案是利用更好的负载平衡。我们一直在内部评估Envoy。这使我们有机会以非常有限的方式部署它并获得立竿见影的好处。Envoy是一种开源、高性能的第7层代理,专为大规模面向服务的架构而设计。它支持高级负载平衡技术,包括自动重试、熔断和全局速率限制。我们想到的配置是在每个Pod中都有一个Envoysidecar,带有路由和集群以将流量定向到本地容器端口。为了最大程度地减少潜在的级联并保持较小的爆炸半径,我们使用了一组前端代理EnvoyPod,每个可用区(AZ)中的每个服务一个。这是一种新的服务发现机制,它只返回每个AZ中给定服务的Pod列表。服务前端Envoy然后将此服务发现机制与上游集群和路由器一起使用。我们配置了合理的超时,提高了所有断路器设置,然后配置了最少的重试次数以帮助解决瞬态故障和顺利部署。我们在每个前端Envoy服务中使用TCPELB。即使来自我们主要前端代理层的keepalives被固定到一些Envoy容器,它们也能更好地处理负载,并配置为使用minimum_request平衡到后端。对于部署,我们在应用程序和sidecarpod上使用preStop挂钩。这个钩子调用sidecar健康监控故障管理端点,休眠一小段时间,让板载连接完成并耗尽。我们能够如此快速地迁移的原因之一是我们能够轻松地与常规Prometheus设置集成的丰富指标集。这使我们能够在迭代配置设置和减少流量时准确地看到发生了什么。结果立竿见影。我们从最不平衡的服务开始,它现在运行在集群中12个最重要的服务之前。今年,我们计划迁移到具有更高级服务发现、熔断、异常检测、速率限制和跟踪功能的全服务网格。下图展示了切换Envoy前后服务CPU使用率的变化:最终结果通过这些学习和其他研究,我们建立了一个强大的内部基础设施团队,非常熟悉如何设计、部署和运营大规模Kubernetes集群。Tinder的整个工程组织现在拥有如何在Kubernetes上容器化和部署其应用程序的知识和经验。在我们的传统基础设施上,当需要进一步扩展时,我们通常需要等待几分钟才能让新的EC2实例上线。容器现在可以在几秒钟而不是几分钟内安排和服务流量。在单个EC2实例上调度多个容器也提高了水平密度。因此,我们预计EC22019与上一年相比将节省大量成本。花了将近两年的时间,我们在2019年3月完成了迁移。Tinder平台仅运行在一个Kubernetes集群上,该集群包含200个服务、1,000个节点、15,000个Pod和48,000个正在运行的容器。基础设施不再是我们运营团队的任务。相反,整个组织的工程师共同承担这一责任,并控制如何使用一切皆代码来构建和部署他们的应用程序。
