本文翻译自由 David Vroom, James Mulcahy, Ling Yuan, Rob Gulewich 编写的 Netflix 博客 Zero Configuration Service Mesh with On-Demand Cluster Discovery。
Netflix 相信大家并不陌生,在 Spring Cloud 生态中就有 Netflix 全家桶。多年前,我也曾将基于 Netflix OSS 构建的微服务架构搬上了 Kubernetes 平台,并持续折腾好几年。
Spring Cloud Netflix 有着庞大的用户群以及用户场景,其提供了微服务治理的一整套解决方案:服务发现 Eureka、客户端负载均衡 Ribbon、断路器 Hystrix、微服务网格 Zuul。
有过相同经历的小伙伴应该都会同感,这样一套微服务解决方案的架构,经过多年的演进也会让人痛苦不堪:复杂度越来越高、版本碎片化严重、多语言多框架的支持和功能无法统一等等。这也是 Netflix 自己也不得不面对的问题,随后他们将目光转向了服务网格,并寻求一个无缝迁移的方案。
这篇文章中,他们给出了答案:Netflix 与社区合作,并自建控制平面与已有的服务发现体系兼容。这套实现下来可能并不容易,也未看到他们要将其开源的想法,但是至少可以给大家一个参考。
顺便预告一下自己正在计划的一篇,如何将微服务平滑迁移到 Flomesh 服务网格平台。不止无缝兼容 Eureka,还有 HashiCorp Consul,未来还会兼容更多的服务发现方案。
以下是原文的翻译:
在这篇文章中,我们将讨论 Netflix 服务网格(Service Mesh)实践的相关信息:历史背景、动机,以及我们如何与 Kinvolk 和 Envoy 社区合作,在复杂的微服务环境中推动服务网格落地的一个特征:按需集群发现(On-demand Cluster Discovery)。
Netflix 的 IPC 简史
Netflix 是早期云计算的采纳者,特别是对于大规模的公司:我们在 2008 年开始了迁移,并且到 2010 年,Netflix的流媒体完全运行在AWS上。今天我们拥有丰富的工具,包括开源和商业的,所有这些都是为云原生环境设计的。然而,在 2010 年,几乎没有这样的工具存在:CNCF 直到 2015 年才成立!由于没有现成的解决方案,我们需要自己构建它们。
对于服务间的进程间通信(IPC),我们需要一个中间层负载均衡器通常提供的丰富功能集。我们还需要一种解决方案,来应对云环境的现实:一个高度动态的环境,其中节点不断上线和下线,服务需要快速响应变化并隔离失败。为了提高可用性,我们设计了可以单独发生故障的系统组件,以避免单点故障。这些设计原则引导我们走向了客户端负载均衡,而 2012年圣诞节的宕机 进一步坚定了这个决策。在云早期,我们构建了Eureka 用于服务发现,以及 Ribbon(内部名为NIWS)用于IPC。Eureka 解决了服务如何发现与其通信的实例的问题,而 Ribbon 提供了负载均衡的客户端,以及许多其他的弹性特性。这两项技术,加上一系列其他的弹性和混沌工具,产生了巨大的收益:我们的可靠性因此得到了显著的改善。
Eureka 和 Ribbon 提供了一个简单而强大的接口,让它们的使用变得容易。一个服务与另一个服务通信,需要知道两件事:目的服务的名称,以及流量是否应该是安全的。Eureka 为此提供的抽象是虚拟 IP(VIPs)用于非安全通信,和安全 VIPs(SVIPs)用于安全通信。服务向 Eureka 宣布一个 VIP 名和端口(例如:_myservice_,端口 _8080_),或一个 SVIP 名和端口(例如:_myservice-secure_,端口 8443),或同时使用两者。IPC 客户端针对该 VIP 或 SVIP 进行实例化,而 Eureka 客户端代码通过从 Eureka 服务器获取它们来处理该 VIP 到一组 IP 和端口对的转换。客户端还可以选择启用像重试或熔断这样的 IPC 功能,或者使用一组合理的默认值。
在这种架构中,服务间通信不再通过负载均衡器的单点故障通道。但问题是,Eureka 现在成为了 VIP 注册主机真实性的新的单一故障点。然而,如果 Eureka 宕机,服务仍然可以互相通信,尽管它们的主机信息随着 VIP 的上下线而逐渐过时。在故障期间以降级但可用的状态运行仍然是相比完全停止流量的明显改进。
在这种架构中,服务与服务间的通信不再经过负载均衡器的单一故障点。但问题是,Eureka 现在成为了 VIP 注册主机真实性的新的单一故障点。然而,如果 Eureka 宕机,服务仍然可以互相通信,尽管它们的主机信息随着 VIP 的上下线而逐渐过时。在故障期间以降级但可用的状态运行仍然是相比完全停止流量的明显改进。
为什么选择网格?
上述架构在过去的十年里为我们服务得很好,但随着业务需求的变化和行业标准的演变,我们的 IPC 生态系统在很多方面都增加了更多的复杂性。首先,我们增加了不同的 IPC 客户端的数量。我们的内部 IPC 流量现在是纯 REST、GraphQL 和 gRPC 的混合。其次,我们从只使用 Java 环境迁移到了多语言环境:我们现在也支持 node.js、Python 以及各种开源和现成的软件。第三,我们继续为 IPC 客户端增加更多功能,如 自适应并发限制、断路器、对冲和故障注入已成为我们工程师为使系统更可靠而采用的标准工具。与十年前相比,我们现在支持更多功能、更多语言、更多客户端。确保所有这些实现之间的功能一致性并确保它们都以相同的方式运行是具有挑战性的:我们希望这些功能有一个单一、经过充分测试的实现,这样我们可以在一个地方进行更改和修复错误。
这就是服务网格的价值所在:我们可以在一个实现中集中 IPC 功能,并使每种语言的客户端尽可能简单:它们只需要知道如何与本地代理通话。Envoy 对我们来说是代理的绝佳选择:它是一个经过战斗考验的开源产品,已经在行业中被广泛使用,拥有 许多关键的弹性功能,以及当我们需要扩展其功能时的 良好的扩展点。能够 通过一个集中的控制平面配置代理 是一个杀手级的功能:这使我们可以动态配置客户端负载均衡,就像它是一个集中的负载均衡器,但仍然避免了服务到服务请求路径中的负载均衡器作为单一的故障点。
转向服务网格
一旦认定我们决定转向服务网格是正确的选择,下一个问题便是:我们应如何进行迁移?我们确定了一些迁移的限制条件。首先:我们希望保留现有的接口。通过指定 VIP 名称加上安全服务的抽象为我们提供了良好服务,我们不想破坏向后兼容性。其次:我们希望自动化迁移,并使其尽可能无缝。这两个限制意味着我们需要支持 Envoy 中的 Discovery 抽象,以便 IPC 客户端可以继续在底层使用它。幸运的是,Envoy 已经有了 现成的抽象 可以用。VIP 可以表示为 Envoy 集群,代理可以从我们的控制平面使用集群发现服务 (CDS) 获取它们。这些集群中的主机表示为 Envoy 端点,可以使用端点发现服务 (EDS) 获取。
我们很快遇到了一个无缝迁移的障碍:Envoy 要求在代理的配置中指定集群。如果服务 A 需要与集群 B 和 C 通信,那么需要在 A 的代理配置中定义集群 B 和 C。这在规模上可能具有挑战性:任何给定的服务可能会与数十个集群通信,而每个应用程序的集群集合都是不同的。此外,Netflix 始终在变化:我们不断推出新的项目,如直播、广告 和游戏,并且不断发展我们的架构。这意味着服务通信的集群会随着时间的推移而改变。鉴于我们可用的 Envoy 原语,我们评估了一些填充集群配置的不同方法:
- 让服务所有者定义他们的服务需要与之通信的集群。这个选项看似简单,但实际上,服务所有者并不总是知道,或想要知道,他们与哪些服务通信。服务通常会导入由其他团队提供的库,这些库在底层与多个其他服务通信,或与像遥测和日志记录等其他操作服务通信。这意味着服务所有者需要知道这些辅助服务和库是如何在底层实现的,并在它们发生变化时调整配置。
- 根据服务的调用图自动生成 Envoy 配置。这种方法对于预先存在的服务来说很简单,但是在启动新服务或添加新的上游集群以进行通信时具有挑战性。
- 将所有集群推送到每个应用程序:这个选项以其简单性吸引了我们,但是纸巾上的简单计算很快向我们显示,将数百万个端点推送到每个代理是不可行的。
考虑到我们无缝采纳的目标,每个选项都有足够重大的缺点,使我们探索了另一个选项:如果我们能在运行时按需获取集群信息,而不是预先定义它,会怎样?当时,服务网格工作仍在启动阶段,只有少数几个工程师在致力于它。我们联系了 Kinvolk,看看他们是否能与我们和 Envoy 社区合作实施这个功能。这次合作的结果是 按需集群发现(On-Demand Cluster Discovery,ODCDS)。有了这个功能,代理现在可以在第一次尝试连接它时查找集群信息,而不是在配置中预先定义所有集群。
有了这个功能,我们需要给代理提供集群信息以供查询。我们已经开发了一个实现 Envoy XDS 服务的服务网格控制平面。然后我们需要从 Eureka 获取服务信息以返回给代理。我们将 Eureka 的 VIP 和 SVIP 表示为单独的 Envoy Cluster Discovery Service (CDS) 集群(因此,服务 myservice 可能有集群 myservice.vip 和 _myservice.svip_)。集群中的单个主机被表示为单独的 Endpoint Discovery Service (EDS) 端点。这使得我们能够重复使用相同的 Eureka 抽象,并且像 Ribbon 这样的 IPC 客户端可以通过最小的更改转移到网格。控制平面和数据平面的更改到位后,流程如下所示:
- 客户端请求进入 Envoy。
- 根据 Host / :authority 头(此处使用的头可配置,但这是我们的方法)提取目标集群。如果已知该集群,跳到步骤 7。
- 集群不存在,所以我们暂停了正在传输的请求。
- 向控制平面的 Cluster Discovery Service (CDS) 端点发出请求。控制平面根据服务的配置和 Eureka 注册信息生成定制的 CDS 响应。
- Envoy 获取集群(CDS),触发通过 Endpoint Discovery Service (EDS) 拉取端点。根据该 VIP 或 SVIP 的 Eureka 状态信息返回集群的端点。
- 客户端请求解除暂停。
- Envoy 正常处理请求:它使用负载平衡算法选择一个端点并发出请求。
这个流程在几毫秒内完成,但只在对集群的第一次请求时。之后,Envoy 的行为就好像集群是在配置中定义的一样。关键是,该系统允许我们无需任何配置即可无缝迁移服务至服务网格,满足我们的主要采纳限制之一。我们呈现的抽象继续是 VIP 名称加上安全,并且我们可以通过配置单个 IPC 客户端连接到本地代理而不是直接连接到上游应用程序来迁移到网格。我们继续使用 Eureka 作为 VIP 和实例状态的真实来源,这使得我们能够在迁移时支持一些应用程序在网格上,而另一些不在网格上的异构环境。还有一个额外的好处:我们可以通过仅为我们实际通信的集群获取数据来保持 Envoy 的内存使用率较低。
上图展示了一个 Java 应用中的 IPC 客户端通过 Envoy 与注册为 SVIP A 的主机通信。 Envoy 从网格控制平面获取 SVIP A 的集群和端点信息。网格控制平面从 Eureka 获取主机信息。
按需获取此数据的缺点是:这会增加对集群的第一次请求的延迟。我们遇到了服务在第一次请求时需要非常低延迟访问的用例,并且增加了几毫秒额外的开销。对于这些用例,服务需要预定义它们通信的集群,或在第一次请求之前准备好连接。我们还考虑过根据历史请求模式在代理启动时从控制平面预推送集群。总的来说,我们觉得系统中的降低的复杂性证明了对少量服务的缺点是合理的。
我们在服务网格之旅的初期。现在我们真诚地使用它,我们希望与社区合作做出更多的 Envoy 改进。将我们的 自适应并发限制 实现移植到 Envoy 是一个很好的开始 - 我们期待着与社区在更多方面合作。我们特别对社区在增量 EDS 方面的工作感兴趣。EDS 端点占了更新量的最大部分,这对控制平面和 Envoy 造成了不必要的压力。
我们要非常感谢 Kinvolk 的人员对 Envoy 的贡献:Alban Crequy, Andrew Randall, Danielle Tal, 特别是 Krzesimir Nowak 的出色工作。我们也要感谢 Envoy 社区的支持和锋利的审查:Adi Peleg, Dmitri Dolguikh, Harvey Tuch, Matt Klein, 和 Mark Roth。与你们所有人合作是一次很好的经历。
这是我们通向服务网格之旅的系列文章的第一篇,敬请期待。
关注"云原生指北"微信公众号 (转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)