本篇文章我们讨论 Netflix's 所采用的服务网格,演进历史,动机,我们如何与 Kinvolk 团队 以及 Envoy 社区合作开发,一项在复杂微服务环境中简化服务网格的功能:按需集群发现(on-demand cluster discovery,ODCD)
Netflix 的 IPC 简史
对于大公司而言,Netflix 很早就涉足云计算,我们于 2008 年开始迁移上云,到 2010 年 Netflix 流媒体完全运行在 AWS 云计算上。现在,我们有丰富的工具,包括 OSS 和商业化工具,都是为云原生环境设计。然而,在 2010 年,这些几乎都不存在,CNCF 直到 2015 年才成立!由于没有现成可用的解决方案,我们需要完全自研。
对于服务之间的进程通信(Inter-Process Communication, IPC),我们需要中间层负载均衡器通常提供的丰富特性集。同时需要一个云计算现实环境中的解决方案:一个高度动态的环境,集群节点上线或者下线,服务需要快速响应集群变更并且绕过故障节点。为了提高可用性,我们设计的系统中组件可以单独失败,避免单点故障。这些设计原则引导我们实现客户端负载均衡器,同时,2012 年圣诞节前夕的故障进一步坚定了这一决策。
云计算早期阶段,我们研发了服务发现组件 Eureka 和 Ribbon(内部成为NIWS)用于 IPC,Eureka 解决了服务如何发现与之通信的实例问题,Ribbon 为负载均衡提供了客户端逻辑,以及许多其他的弹性特性。这两项技术,以及许多其他弹性和混沌工具一起产生了巨大的变化:我们系统可靠性得到显著提升。
Eureka 和 Ribbon 提供了简单但功能强大的接口,使得采用它们非常简单。一个服务为了与其他实例通信,它需要知道两件事:目标服务名称,以及流量是否需要安全加固。Eureka 为此提供了抽象接口,虚拟 IP(VIPs) 用于非安全通信,安全 VIPs(SVIPs) 用于安全通信。服务向 Eureka 发布注册 VIP 名称和端口(例如:服务名:myservice,端口:8080),或者 SVIP 名称和端口(例如:服务名:myservice,端口:8443),或者两种同时注册。IPC 客户端是根据 VIP 或者 SVIP 进行实例化,同时Eureka 客户端代码通过从 Eureka 服务端获取 IP 及端口,处理 VIP 到 IP 端口集的转换。客户端也可以自由选择启用 IPC 特性,例如重试或者熔断机制,或者坚持使用一组合理的默认值。架构图如下所示
在这种架构下,服务之间的通信不再经历负载均衡器的单点故障,缺点是,作为通过 VIPs 注册的真实实例源,Eureka 又是一个新的单点故障。如果 Eureka 宕机,服务之间仍可以通信。尽管随着时间推移,VIP 对应的服务实例上线或者下线,他们的主机信息会过时。与完全停止通信流量相比,在中断期间能够以降级但可用的状态运行仍然是一个显著的改进点。
为什么采用网格
在过去的十年中,尽管不断变化的业务需求和不断发展的行业标准在许多方面增加了我们的 IPC 生态系统的复杂性,上述架构依然为我们提供了很好的服务。
首先,我们增加了不同 IPC 客户端的数量。我们内部的 IPC 流量是纯 REST,GraphQL 以及 gRPC 的混合。
其次,我们已经从纯 Java 环境转变为多语言环境:我们现在也支持 node.js,python 以及各种 OSS和现有软件。
第三,我们继续给我们的 IPC 客户端增加许多功能:自适应并发限流,熔断,对冲,故障注入等特性,这些已经成为我们工程师用以提高系统可靠性的标准工具。相较于十年前,我们现在支持更多特性,更多语言,以及更多客户端。
保持这些特性所有语言实现功能相同,并且确保他们运行表现一致是具有挑战的:我们想要的是单一的,经过良好测试的,这些功能的所有实现,这样我们就可以将变更和bug修复收归到一处。
这就是服务网格的用武之地:我们可以将 IPC 功能集中在一个实现中,并使每种语言的客户端尽可能简单:它们只需要知道如何与本地代理通信。作为代理,Envoy 非常适合我们:它是一个在业界大规模使用,经过实战检测的 OSS 产品,具有许多关键的弹性特性,当我们需要扩展其功能时,它有很好的扩展点。通过中央控制平面配置代理的能力是一个杀手级特性:它允许我们动态配置客户端的负载均衡,就好像它本身就是中心负载均衡器一样,同时作为服务之间请求路径上的负载均衡器,避免了单点故障。
迁移至网格服务
一旦我们决策迁移到服务网格是正确的选择,接下来的问题就变成了我们该如何迁移,我们为迁移确定了一些约束前提条件。
第一,我们需要保持现有接口,指定VIP和安全加固的抽象对我们很有用,并且我们不想破坏兼容性。
第二,我们希望自动化并且无缝迁移。
这两个诉求意味着我们需要在 Envoy 中支持 Discovery 抽象,以便 IPC 客户端可以继续在底层使用它。幸运的是,针对这些,Envoy 已经可以使用抽象接口,VIPs 可以映射为 Envoy 集群,代理可以使用集群发现服务 (Cluster Discovery Service,CDS) 从我们的控制面板获取数据。这些集群中的主机可以映射为 Envoy endpoint,可以使用 Endpoint 发现服务(Endpoint Discovery Service, EDS)来获取。
但是,我们很快就遇到了无缝迁移的绊脚石,Envoy 要求将集群指定为代理配置的一部分,如果服务 A需要与集群 B,C 通信,就需要将集群 B,C 定义为服务A代理配置的一部分。这在大规模服务集群上有一定挑战,任何服务都可能与数十个集群通信,每个应用的集群集合都是不同的。此外,Netflix 一直在变化:我们业务不断拓展,比如直播,广告,游戏等,并且我们的架构也在不断演进。这意味着服务之间通信的集群也在随时间不断变化。通过可用的 Envoy 基元,我们评估了几种不同的方案来规划集群配置:
- 让服务所有者自己定义他们需要通信的集群。这种方案看似简单,但实践起来,服务所有者通常不知道或者想知道他们与哪些服务通信。服务所有者通常直接导入由其他团队提供的库,这些库底层与多个其他服务通信,或者与计量数据收集 (Telemetry) 服务和日志服务等其他运营服务通信,这意味着服务所有者需要知道这些运营服务和导入的库底层如何运行,并且在这些服务变化时,调整对应集群配置。
- 基于服务调用链自动生成 Envoy 集群配置,这种方案对于存量服务很简单,但是新增服务或者服务新增上游通信集群时,这种方法就很有挑战性了。
- 将所有集群推送到每个应用程序:这个方案很简单,但很快我们就发现,将数百万个 endpoint 推送到每个代理是不可行的。
考虑到我们无缝迁移的目标,这些选项都有明显的缺点,所以我们探索了另一种方案:如果我们可以运行时按需获取集群信息,而不是预先定义集群配置,是否可行?与此同时,服务网格仍处于引导阶段,只有几个工程师致力于此。我们联系了 Kinvolk 团队,看看他们是否可以与我们以及 Envoy 社区合作,实现这个特性。合作的结果就是按需集群发现(On-Demand Cluster Discovery,ODCDS)。具体详见这个 代码合并。有了这个特性,代理现在可以在第一次尝试链接时查找集群信息,而不是在配置中预定义所有集群信息。
有了这个功能,我们需要为代理提供集群信息以便于查找,我们已经开发了一个实现 Envoy XDS 服务的服务网格控制平面,接下来需要从 Eureka 获取服务信息以便返回给代理。我们将 Eureka VIPs 和SVIPs 表示为独立的 Envoy 集群发现服务(CDS)集群(因此服务 myservice 可能有集群 myservice.vip 和 myservice.svip),将集群中的单个主机表示为单独的 endpoint 发现服务(EDS)endpoint。这允许我们复用相同的 Eureka 抽象,并且像 Ribbon 这样的 IPC 客户端可以以最小的改动迁移到服务网格。控制平面和数据平面都改变后,整体工作流如下:
- 客户端请求进入 Envoy
- 根据 Host /:authority 请求头提取目标集群(这里使用请求头是可配置的,这只是我们的方案)。如果该集群信息已知,请跳转到步骤7
- 该集群不存在,我们暂停正在进行的请求
- 向控制平面上的集群发现服务(CDS) endpoint 发送请求,基于服务的配置和 Eureka 注册信息,控制平面生成自定义的 CDS 响应
- Envoy 获取返回的集群(CDS)响应,这会通过 EDS 触发拉取 endpoints,并基于该 VIP 或者 SVIP 在 Eureka 上的状态信息返回集群的 endpoints
- 客户端请求取消暂停
- Envoy 正常处理请求:使用负载均衡算法选择一个 endpoint 并发出请求
整个流程需要几 ms 完成,但是仅在对集群发出的第一次请求时完成。之后,Envoy 的行为表现就像集群已经在配置中定义了。关键的是,这一整套系统流程让我们不需要配置前提下,无缝迁移服务到服务网格,满足了我们主要诉求之一。我们提供的抽象定义仍然是服务名 安全通信,我们可以通过配置单个IPC 客户端链接到本地代理,而不是直接连接上游到上游应用来迁移到服务网格。我们继续使用 Eureka 作为 vip 和实例状态的数据源,这使我们在迁移时能够支持网格上某些异构环境和非异构环境的应用程序。还有一个额外的好处:通过只获取与我们实际通信的集群的数据,可以保持 Envoy 较低内存使用率。
按需获取集群数据的缺点是:对于集群的第一个请求会增加延迟。我们有一些用例,服务在第一次请求是要求非常低的请求延迟,增加几毫秒额外时间就会增加极大的开销。对于这些用例,服务需要预定义与之通信的集群配置,或者在第一个请求之前建立连接。我们也考虑在代理启动时,基于历史请求模式从控制平面预先推送集群信息。总体而言,我们认为仅一小部分服务的不足,但是降低系统复杂度是合理的。
我们还处于服务网格的早期阶段,现在我们正在认真使用它,我们很乐意与社区合作进行更多的 Envoy 改进。将我们的自适应限流实现移植到 Envoy 是一个很好的开始-我们期待着与社区的更多合作,我们对社区在增量 EDS 上的工作特别感兴趣。EDS endpoint 更新量最大,这给控制平面和Envoy 都带来了过度的压力。
我们衷心感谢 Kinvolk 团队对于 Envoy 的贡献:Alban Crequy, Andrew Randall, Danielle Tal,特别是Krzesimir Nowak,的出色工作。我们还要感谢Envoy社区的支持和犀利的评论:Adi Peleg、Dmitri Dolguikh、Harvey Tuch、Matt Klein和Mark Roth。和你们一起工作是一段很棒的经历。
这是我们服务网格之旅系列文章中的第一篇,敬请期待。如果这听起来很有趣,并且您想要大规模地开发服务网格,请加入我们。
本文翻译自 Netflix 技术博客,原文地址