注意事项 佩雷夫 :Tinder员工最近分享了将其基础架构迁移到Kubernetes的一些技术细节。 该过程花费了将近两年的时间,并最终在K8上启动了一个非常大型的平台,该平台由在48000个容器上托管的200个服务组成。 Tinder工程师面临哪些有趣的困难以及他们取得了什么结果-请阅读此翻译。
怎么了
大约两年前,Tinder决定将其平台切换到Kubernetes。 Kubernetes将使Tinder团队可以通过
不变的部署以最小的努力将容器化并切换到操作状态。 在这种情况下,应用程序的组装,它们的部署和基础结构本身将由代码唯一地确定。
我们还寻找可扩展性和稳定性问题的解决方案。 当扩展变得至关重要时,我们常常不得不等待几分钟才能启动新的EC2实例。 因此,启动容器并在几秒钟而不是几分钟内开始提供流量的想法对我们非常有吸引力。
这个过程并不容易。 在迁移期间,2019年初,Kubernetes集群达到了临界数量,由于流量,集群规模和DNS的影响,我们开始面临各种问题。 在此过程中,我们解决了许多与200个服务的转移和Kubernetes集群的维护有关的有趣问题,该集群由1000个节点,15,000个Pod和48,000个工作容器组成。
怎么了
自2018年1月以来,我们经历了迁移的各个阶段。 我们首先将所有服务容器化并将它们部署到Kubernetes测试环境。 10月,开始有条不紊地将所有现有服务转移到Kubernetes。 到第二年3月,“迁移”完成,现在Tinder平台仅在Kubernetes上运行。
为Kubernetes构建映像
我们为Kubernetes集群中运行的微服务提供了30多个源代码存储库。 这些存储库中的代码用不同的语言(例如,Node.js,Java,Scala,Go)编写,并且许多运行时环境都使用相同的语言。
构建系统旨在为每个微服务提供完全可自定义的“构建上下文”。 它通常由一个Dockerfile和一系列shell命令组成。 它们的内容是完全可定制的,并且同时,所有这些构建上下文都是按照标准化格式编写的。 标准化构建上下文允许单个构建系统处理所有微服务。
图1-1。 通过容器构建器(Builder)进行标准化构建过程为了在运行时之间实现最大的一致性,在开发和测试期间使用相同的构建过程。 我们面临一个非常有趣的问题:我们必须开发一种方法来确保整个平台上组装环境的一致性。 为此,所有组装过程都在一个特殊的
Builder容器内进行。
它的实现需要使用高级技术来使用Docker。 Builder会继承访问Tinder私有存储库所需的本地用户ID和机密(例如SSH密钥,AWS凭证等)。 它装入包含源的本地目录,以自然地存储程序集工件。 通过消除在Builder容器和主机之间复制程序集工件的需要,该方法提高了性能。 存储的程序集工件无需额外配置即可重复使用。
对于某些服务,我们必须创建另一个容器以使编译环境与运行时相匹配(例如,在安装过程中,Node.js bcrypt库会生成特定于平台的二进制工件)。 在编译期间,对不同服务的要求可能会有所不同,最终的Dockerfile会即时进行编译。
Kubernetes集群架构和迁移
集群规模管理
我们决定使用
kube-aws在Amazon EC2实例上自动部署集群。 从一开始,所有工作都在一个公共节点池中进行。 我们很快意识到需要通过实例的大小和类型来分离工作负载,以更有效地利用资源。 逻辑是,与多个单线程Pod并存相比,启动多个已加载的多线程Pod的性能更可预测。
结果,我们决定:
- m5.4xlarge-用于监视(Prometheus);
- c5.4xlarge-用于Node.js工作负载(单线程工作负载);
- c5.2xlarge-适用于Java和Go(多线程工作负载);
- c5.4xlarge-用于控制面板(3个节点)。
迁移
从旧基础架构迁移到Kubernetes的准备步骤之一是将服务之间的现有直接交互重定向到新的负载均衡器(ELB,Elastic Load Balancers)。 它们是在特定的虚拟私有云(VPC)子网中创建的。 该子网已连接到Kubernetes VPC。 这使我们可以逐步迁移模块,而无需考虑服务依赖关系的特定顺序。
这些端点是使用DNS记录的加权集创建的,其中CNAME指向每个新的ELB。 要进行切换,我们添加了一条指向权重为0的指向新Kubernetes服务ELB的新记录。然后将记录集的生存时间(TTL)设置为0。此后,新旧权重进行了缓慢的调整,最终100%的负载被转移了到新服务器。 切换完成后,TTL值恢复到更适当的水平。
我们现有的Java模块可以处理低TTL DNS,但Node应用程序则不能。 一位工程师重写了部分连接池代码,将其包装在一个经理中,该经理每60秒更新一次连接池。 选择的方法效果很好,并且性能没有明显下降。
上课
网络设备限制
在2019年1月8日凌晨,Tinder平台突然崩溃。 响应于早上凌晨平台延迟的无关增加,群集中的Pod和节点数量增加了。 这导致我们所有节点上的ARP缓存耗尽。
ARP缓存与三个Linux选项相关:

(
来源 )
gc_thresh3是一个硬限制。 条目日志中以“邻居表溢出”形式出现意味着,即使在ARP缓存中进行了同步垃圾回收(GC)之后,也没有足够的空间来存储邻居记录。 在这种情况下,内核只是完全丢弃了数据包。
我们使用
Flannel作为Kubernetes中的
网络结构 。 数据包通过VXLAN传输。 VXLAN是通过L3网络提升的L2隧道。 该技术使用MAC-in-UDP(MAC地址在用户中的数据报协议)封装,并允许您扩展第二级的网段。 数据中心物理网络中的传输协议是IP加UDP。
图2-1。 绒布图( 来源 )
图2–2。 VXLAN数据包( 源 )每个Kubernetes工作节点从较大的块/ 9中分配一个带有掩码/ 24的虚拟地址空间。 对于每个节点,这
意味着路由表中的一个条目,ARP表(在
flannel.1接口上)中的一个条目以及交换表(FDB)中的一个条目。 它们是在首次启动工作节点或检测到每个新节点时添加的。
另外,node-pod(或pod-pod)连接最终会通过
eth0接口进行连接(如上面的Flannel图所示)。 这会在ARP表中为节点的每个相应源和目标添加一个附加条目。
在我们的环境中,这种通信非常普遍。 对于Kubernetes中的服务类型对象,将创建一个ELB,Kubernetes将在ELB中注册每个节点。 ELB不了解有关Pod的任何信息,并且所选节点可能不是数据包的最终目的地。 事实是,当节点从ELB接收数据包时,它会考虑针对特定服务的
iptables规则,并在另一个节点上随机选择pod。
发生故障时,群集具有605个节点。 由于上述原因,这足以克服
默认的gc_thresh3值。 发生这种情况时,不仅开始丢弃数据包,而且带有/ 24掩码的整个Flannel虚拟地址空间也会从ARP表中消失。 节点Pod通信和DNS查询被中断(DNS托管在群集中;有关详细信息,请参阅本文的其余部分)。
要解决此问题,请增加
gc_thresh1 ,
gc_thresh2和
gc_thresh3的值,然后重新启动Flannel以重新注册丢失的网络。
意外的DNS扩展
在迁移过程中,我们积极使用DNS来管理流量,并将服务从旧基础架构逐步转移到Kubernetes。 我们为Route53中的相关RecordSet设置了相对较低的TTL值。 当旧的基础架构在EC2实例上运行时,我们的解析器配置指向Amazon DNS。 我们认为这是理所当然的,低TTL对我们的Amazon服务(如DynamoDB)的影响几乎没有引起注意。
随着服务迁移到Kubernetes,我们发现DNS每秒处理250,000个查询。 结果,应用程序开始经历持续不断且严重的DNS查询超时。 尽管在优化DNS提供商并将其转换为CoreDNS方面做出了巨大努力,但仍发生了这种情况(峰值负载下,该Pod达到了在120个内核上运行的1000个pod)。
在探讨其他可能的原因和解决方案时,我们找到
了一篇文章,描述了影响Linux上的
netfilter数据包过滤框架的竞争条件。 我们观察到的超时以及Flannel接口中
insert_failed计数器的增加,与本文的结论相对应。
在源和目标网络地址转换(SNAT和DNAT)阶段以及随后的
conntrack表条目中会出现问题。 公司内部讨论和社区提出的解决方法之一是将DNS转移到工作节点本身。 在这种情况下:
- 不需要SNAT,因为流量保留在节点内。 不需要通过eth0接口进行路由。
- 不需要DNAT,因为目标IP在主机本地,而不是根据iptables规则随机选择的Pod。
我们决定坚持这种方法。 CoreDNS在Kubernetes中作为DaemonSet部署,我们通过配置
kubelet命令的
--cluster-dns标志在每个pod的
resolv.conf中实现了本地主机DNS服务器。 实践证明,该解决方案对于DNS超时有效。
但是,我们仍然观察到数据包丢失以及Flannel接口中
insert_failed计数器的增加。 引入解决方法后,这种情况继续存在,因为我们只能排除DNS流量的SNAT和/或DNAT。 其他类型流量的比赛条件仍然存在。 幸运的是,我们的大多数软件包都是TCP,一旦出现问题,它们将被简单地重新传输。 我们仍在努力寻找适合所有流量类型的解决方案。
使用Envoy实现更好的负载平衡
当我们将后端服务迁移到Kubernetes时,我们开始遭受Pod之间负载不平衡的困扰。 我们发现由于HTTP Keepalive,ELB连接挂在每个部署部署的第一个现成的Pod上。 因此,大部分流量都流经一小部分可用的Pod。 我们测试的第一个解决方案是在最坏情况下的新部署中将MaxSurge参数设置为100%。 对于更大的部署而言,这种影响微不足道且没有希望。
我们使用的另一个解决方案是人为地增加关键任务服务的资源请求。 在这种情况下,相邻的吊舱比其他笨重的吊舱有更大的回旋余地。 从长远来看,由于浪费资源,它也不起作用。 此外,我们的Node应用程序是单线程的,因此只能使用一个内核。 唯一真正的解决方案是使用更好的负载平衡。
我们长期以来一直想充分欣赏
Envoy 。 当前的情况使我们能够以非常有限的方式部署它并获得立竿见影的效果。 Envoy是为大型SOA应用程序设计的开源高性能第七级代理。 他能够应用先进的负载平衡技术,包括自动重试,断路器和全局速度限制。
( 注意翻译 :有关更多详细信息,请参阅有关Istio的最新文章 -基于Envoy的服务网格。)我们提出了以下配置:为每个吊舱和一条路线安排Envoy边车,以及集群-通过端口本地连接到容器。 为了最大程度地减少潜在的级联并保持较小的“损坏”半径,我们使用了Envoy前代理吊舱停放区,每个服务可用区(AZ)都有一个。 他们转向由我们的一位工程师编写的简单服务发现机制,该机制只是返回给定服务的每个可用区中的Pod列表。
然后,服务前使节将这种服务发现机制与一个上游群集和路由一起使用。 我们设置了足够的超时时间,增加了所有断路器设置,并添加了最小的重试配置,以帮助解决单个故障并确保无缝部署。 在每个服务前台特使之前,我们都放置了一个TCP ELB。 即使来自我们的主代理层的keepalive挂在某些Envoy吊舱上,它们仍然可以更好地处理负载,并且可以通过后端中的minimum_request进行平衡。
为了进行部署,我们在应用程序Pod和Sidecar Pod上都使用了preStop挂钩。 该挂钩在检查位于边车容器上的管理端点的状态时引发了一个错误,并“休眠”了一段时间以允许活动连接完成。
我们能够如此迅速地解决问题的原因之一,是与能够轻松集成到标准Prometheus安装中的详细指标有关。 使用它们,可以在选择配置参数并重新分配流量时准确查看正在发生的情况。
结果是立即而明显的。 我们从最不平衡的服务开始,目前,它已经可以在集群中的12个最重要的服务之前运行。 今年,我们计划转移到具有更高级服务发现,断路,异常检测,速度限制和跟踪功能的成熟服务网格。
图3-1。 向Envoy过渡期间一项服务的CPU收敛

最终结果
凭借我们的经验和额外的研究,我们建立了一支强大的基础架构团队,在设计,部署和运行大型Kubernetes集群方面具有良好的技能。 现在,所有Tinder工程师都拥有有关如何在Kubernetes中打包容器和部署应用程序的知识和经验。
当旧基础架构上需要更多容量时,我们不得不等待几分钟才能启动新的EC2实例。 现在,容器将启动并开始处理流量,而不是几分钟,而是几秒钟。 在单个EC2实例上调度多个容器还可以提高水平集中度。 因此,我们预计2019年EC2成本将比去年大幅下降。
迁移花费了将近两年的时间,但我们于2019年3月完成了迁移。 目前,Tinder平台仅在Kubernetes集群上运行,该集群包括200个服务,1000个节点,15,000个Pod和48,000个运行中的容器。 基础架构不再是运营团队的唯一责任。 我们所有的工程师都承担着这一责任,并仅使用代码控制构建和部署应用程序的过程。
译者的PS
另请阅读我们博客上的文章系列: