Tinder:如何在两年内迁移上 Kubernetes
技术
作者: 才云 Caicloud
译者:bot
2019-09-03 10:01

大约两年前,全球最受欢迎的交友网站之一 Tinder 决定将其平台迁移到 Kubernetes。因为 Kubernetes 允许 Tinder 通过不可变部署推动工程的容器化和低接触服务化,将应用程序构建、部署和基础结构定义为代码。

与此同时,他们之前也在可扩展性和稳定性上遇到了难题。以可扩展性为例,当这个问题非常突出时,工程师们经常需要花好几分钟等待新 EC2 实例联机。因此 Kubernetes 带来的容器调度和在几秒钟内提供流量的想法也对他们很有吸引力

但整个迁移过程并不容易。今年年初,Tinder 的 Kubernetes 集群达到了临界点,并因流量、集群大小和 DNS 遭遇了大量问题。但好在最后他们还是成功完成了 200 个服务的迁移并运行了 Kubernetes 集群。该集群共有 1000 个节点、15000 个 Pod 和 48000 个容器

迁移过程

  • 2018 年 1 月,将所有服务集中在一起,并将它们部署到一系列 Kubernetes 托管的临时环境中。
  • 2018 年 10 月,将所有遗留服务迁移到 Kubernetes。
  • 2019 年 3 月,完成迁移,Tinder 平台现在完全在 Kubernetes 上运行。

为 Kubernetes 建立镜像

在 Tinder 的 Kubernetes 集群中,微服务运行依赖 30 多个源代码仓库。这些仓库中的代码使用不同语言(例如 Java、Scala、Go)编写,具有针对同一语言的多个运行时环境。

因此,整个 build process 被设计成了允许每个微服务做完全可定制的“构建上下文”,这些微服务通常由 Dockerfile 和一系列 shell 命令组成。虽然它们的内容是完全可定制的,但这些“构建上下文”都是按照标准化格式编写的,标准化允许 Tinder 只用单个 build 系统就能处理所有微服务

通过 Builder 容器标准化 build process

为了实现运行时环境之间的最大一致性,Tinder 团队在开发和测试阶段都使用相同的 build process。而为了保证整个平台 build 环境的一致性,他们把所有 build process 都放在特殊的容器——Builder 中执行

为了实现 Builder 容器,他们用到了许多高级 Docker 技术:由于这个容器需要访问 Tinder 私有仓库,它继承了本地用户 ID 和 secret(例如 SSH 密钥、AWS 凭证等);由于需要存储 build artifact,它 mount 了源代码的本地目录。

这种方法提高了性能,因为它避免了在 Builder 容器和主机之间复制 built artifact,下次可以直接复用存储好的 artifact,无需进一步配置。

对于某些特定服务,Tinder 团队在 Builder 中创建了另一个容器,使编译时环境与运行时环境相匹配(例如安装 Node.js bcrypt 库会生成特定于平台的二进制 artifact)。不同服务可能有不同的编译时要求,最终的 Dockerfile 是即时编写的。

Kubernetes 集群架构和迁移

集群大小 对于集群配置,Tinder 采取的方法是在 Amazon EC2 实例上使用 kube-aws 进行自动集群配置。

在早期阶段,他们试着在一个通用节点池中运行所有内容,但由于运行少量多线程 Pod 比运行大量单线程 Pod 性能结果更平滑,为了优化资源利用,他们需要将工作负载分成不同大小和类型的实例。

因此他们决定:

  • 将 m5.4xlarge 用于监控(Prometheus)
  • 将 c5.4xlarge 用于 Node.js 工作负载(单线程工作负载)
  • 将 c5.2xlarge 用于 Java 和 Go(多线程工作负载)
  • 将 c5.4xlarge 用于控制平面(3 个节点)

迁移 

而为了能够更精细地迁移模块,不考虑服务依赖性的特定顺序。Tinder 把遗留基础架构迁移到 Kubernetes 的一项准备工作,是先把现有服务-服务的通信更改为通过在特定虚拟私有云(VPC)子网中创建的全新 Elastic Load Balancers(ELB)进行通信

这些端点是使用加权 DNS 记录集创建的,其中 CNAME 指向每个新 ELB。为了进行切换,他们添加了一条权重为 0 的新记录,指向新的 Kubernetes 服务 ELB。然后,他们又将记录集的 TTL(Time To Live)设置为 0,慢慢调整新旧权重值,直到新服务器的权重到达 100%。

切换完成后,再把 TTL 调整为合理的值。

他们的 Java 模块需要较低的 DNS TTL,但 Node 应用不需要。因此一位工程师重写了部分连接池代码,将其包装在一个每 60 秒刷新一次池的管理器中。最终效果很好,没有明显影响性能。

经验一:网络结构限制

2019 年 1 月 8 日清晨,Tinder 平台遭遇宕机。为响应当天早上平台延迟增加,Tinder 扩展了集群上的 Pod 和节点数,最终导致所有节点上的 ARP 缓存耗尽。

以下是三个与 ARP 缓存相关的 Linux 值:

gc_thresh3 是个 hard cap,如果日志里出现了“neighbor table overflow”,这意味着即便在 ARP GC 之后,他们也没有足够的空间来存储 neighbor entry。在这种情况下,内核会完全丢弃数据包

Tinder 使用 Flannel 作为 Kubernetes 的网络结构。数据包通过 VXLAN 转发。VXLAN 是第三层网络上的二层 overlay scheme。它使用 MAC Address-in-User Datagram Protocol 封装提供扩展二层网络 segment 的方式。物理数据中心网络上的传输协议是 IP+UDP。

Flannel

VXLAN 数据包

每个 Kubernetes 工作节点分配自己 /24 的虚拟地址空间。对于每个节点,它有 1 个路由表 entry、1 个 ARP 表 entry(在 flannel.1 接口上)和 1 个转发数据库(FDB)entry。这些 entry 是在工作节点首次启动或发现每个新节点时添加的

此外,节点到 Pod(或 pod-to-pod)通信最终流过 eth0 接口(如上面的 Flannel 图所示)。这将导致每个对应节点源和节点目的地的,都会在 ARP 表中添加条目。

在 Tinder 的环境中,这种类型的通信非常普遍。对于 Kubernetes 服务对象,它会创建 ELB,然后 Kubernetes 使用 ELB 注册每个节点。ELB 并不知道 Pod,被选中的节点也可能不是数据包的最终目的地。这是因为当节点从 ELB 接收数据包时,它会评估服务的 iptables 规则,并在另一个节点上随机选择一个 Pod

在宕机时,Tinder 集群中总共有 605 个节点。由于上面提到的原因,这足以超出默认的 gc_thresh3 值。一旦发生这种情况,不仅数据包会被丢弃,整个 Flannel/24s 的虚拟地址空间也会从 ARP 表中消失。节点到 Pod 通信和 DNS 查找也会失败。

为了解决这个问题,Tinder 提高了 gcthresh1、gcthresh2 和 gc_thresh3 的值,并重新启动 Flannel 以重新注册丢失的网络。

经验二:意外在大规模集群里运行 DNS

为了适应迁移,Tinder 重度依赖 DNS 来推进流量整形和将服务迁移到 Kubernetes 上。他们在 Route53 RecordSets 上设置了相对较低的 TTL 值。当他们在 EC2 实例上运行旧版基础架构时,解析器配置指向了亚马逊的 DNS。当时他们并没有感觉到异样。

随着越来越多服务迁移上 Kubernetes,他们发现自己运行的 DNS 服务每秒居然要响应 250000 个请求。应用程序中也开始出现间歇性的、明显的 DNS 查找超时。即使尝试了很多方法,包括将 DNS 供应商切换到 CoreDNS 部署上,这个问题仍然会出现。

在研究其他可能的原因和解决方案时,Tinder 团队注意到了一篇文章,指出一种竞争条件会影响 Linux 数据包过滤框架 netfilter。文章介绍的问题正是 Tinder DNS 超时、Flannel 接口上 insert_failed 递增的原因。

文章:https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts

这个问题发生在源和目标网络地址翻译(SNAT 和 DNAT)以及后续 contrack 表插入的过程中。社区经内部讨论后提出的一种解决方法是将 DNS 移动到工作节点本身。在这种情况下:

  • SNAT 不是必需的,因为流量在本地停留在节点上。它不需要通过 eth0 接口传输;
  • DNAT 不是必需的,因为目标 IP 是节点的本地 IP 而不是每个 iptables 规则随机选择的 Pod。

Tinder 团队决定采用这种方法。他们把 CoreDNS 作为 DaemonSet 部署在 Kubernetes 中,并通过配置 kubelet-cluster-dns 命令参数将节点的本地 DNS 服务器注入到每个 Pod 的 resolv.conf 中。这种方法对 DNS 超时有效。

但是,他们还是会发现数据包丢弃和 Flannel 接口上递增的 insert_failed。这是因为上述方法只避免了 DNS 流量的 SNAT 和 DNAT,竞争条件依然存在。幸运的是,Tinder 数据包大多数是 TCP,当竞争发生后,数据包可以被成功地重新传输。他们仍在讨论针对所有类型流量的长期解决方案。

经验三:使用 Envoy 实现更好的负载均衡

当 Tinder 将后端服务迁移到 Kubernetes 时,他们开始受到负载不均衡的影响。

他们发现由于 HTTP Keepalive,每次滚动部署时,ELB 连接会停留在第一个就绪的 Pod 中,因此大多数流量只能流过一小部分可用 Pod。他们尝试的第一个缓解措施是在新的 Deployment 中设置升级策略,即将 MaxSurge 设置成 100%。这是有效的,但对于一些较大的部署来说这并不是一种长期性的解决方案。

他们尝试的另一种方法是人为夸大关键服务的资源请求,以便共置 Pod 可以和其他高负载 Pod 一起拥有更多空间。但由于资源浪费和他们的 Node 应用程序是单线程的,这种方法并不可取。

因此唯一明确的解决方案是更好的负载均衡

Tinder 内部一直在评估 Envoy,这使他们有机会以非常有限的方式部署它并立即获益。Envoy 是一个开源的高性能七层代理,它专为面向服务的大型架构而设计,能够实现先进的负载均衡,包括自动重试、断流和全局限速。

团队采取的方法是在每个 Pod 里部署 Envoy Sidecar,然后连接到本地容器端口。为了最大限度地减少潜在的级联并保持较小的爆破半径,他们使用了 front-proxy Envoy Pod,每个服务的每个可用 Zone 里都有一个部署。这就是一个小型的服务发现机制,返回每个 AZ 中用于给定服务的 Pod 列表。

然后,front-Envoys 服务将此服务发现机制与一个上游集群和路由一起使用。他们配置了合理的超时时间,提升了所有断流设置,然后进行了最小的重试配置,以帮助解决瞬态故障,实现平稳部署。他们在 front-Envoys 服务前都放了 TCP ELB,即使来自代理层的 keepalive 固定在某些 Envoy Pod 上,它们也能够更好地处理负载并被配置为通过 least_request 平衡到后端。

对于部署,Tinder 在应用程序和 Sidecar Pod 上都使用了 preStop hook,这个 hook 每过一段时间调用 Sidecar 健康检查失败的 admin 端点,给一些时间让正在进行的连接完成并耗尽。

Tinder 之所以能这么快完成迁移,一个重要原因是他们能将丰富的指标轻松地与普通 Prometheus 设置集成,使团队在迭代配置中就能看到发生了什么,并减少流量。

如下图所示,用了 Envoy 的效果立竿见影。

切换到 Envoy 时一个服务的 CPU 收敛


最终结果

基于以上经历和其他研究,Tinder 打造了一个强大的内部基础架构团队,非常熟悉如何设计、部署和操作大型 Kubernetes 集群。整个团队也拥有如何在 Kubernetes 上集成和部署应用程序的知识和经验。

正如之前提到的,在迁移到 Kubernetes 之前,Tinder 在需要额外扩展时常常要花几分钟等待新的 EC2 实例联机,现在,容器可以在几秒钟内安排并提供流量,同时,在单个 EC2 实例上调度多个容器还提供了更好的水平密度

总而言之,与去年相比,Tinder 预计 2019 年在 EC2 上的成本将大幅降低。在团队内部,基础设施也不再只是运营的任务,相反地,全公司的工程师将共同承担这一责任,并控制应用程序的构建、部署,以及所有代码。

原文地址:https://medium.com/tinder-engineering/tinders-move-to-kubernetes-cda2a6372f44

1674 comCount 0