原文地址:https://philcalcado.com/2017/08/03/pattern_service_mesh.html
自数十年前首次引入分布式系统以来,我们了解到分布式系统可以提供给我们之前无法想到的用例,但是它们也引入了各种新问题。
当这些系统罕见且简单时,工程师通过减少远程交互的数量来应对增加的复杂性。处理分布式的最安全方法是尽可能避免分布式,即使这意味着会存在系统间的重复的逻辑和数据也是如此。
但是,作为一个行业,我们的需求促使我们进一步发展,从几台大型中央计算机到成千上万的小型服务。在这个新世界中,我们必须开始勇往直前,应对新的挑战和悬而未决的问题,首先要采取 case-by-case 的解决方案,然后再采用更复杂的解决方案。随着我们发现有关问题领域的更多信息并设计更好的解决方案,我们开始将一些最常见的需求明确化为模式、类库、甚至是平台。
当我们第一次开始联网计算机时发生了什么
由于人们首先想到让两台或更多台计算机相互通信,因此他们设想了以下内容:
一个服务与另一个服务进行通信以实现终端用户的某些目的。这显然是过度简化的视图,因为缺少了在代码操作的字节与通过网线发送和接收的电信号之间转换的许多层。但是,对于我们的讨论而言,抽象就足够了。让我们通过将网络堆栈显示为不同的组件来添加更多细节:
自1950年代以来,一直在使用上述模型的变体。最初,计算机是稀有且昂贵的,因此两个节点之间的每个链接都是经过精心设计和维护的。随着计算机变得越来越便宜,越来越流行,连接的数量和通过它们的数据量急剧增加。随着人们越来越依赖网络系统,工程师需要确保他们构建的软件能够满足其用户所需的服务质量。
而且,要达到所需的质量水平,还需要回答许多问题。人们需要找到机器相互查找的方式,通过同一根导线处理多个同时的连接,允许机器在未直接连接时彼此通信,在网络上路由数据包,对流量进行加密等。
其中,有一个称为流控的东西,我们将以它为例。流控是一种机制,可以防止一台服务器发送比下游服务器可以处理的数据包更多的数据包。这是必要的,因为在网络系统中,至少有两台彼此独立的、彼此不了解的计算机。计算机A以给定的速率向计算机B发送字节,但是不能保证B将以一致且足够快的速度处理接收到的字节。例如,B可能正在并行处理其他任务,或者数据包可能乱序到达,并且B正在阻塞等待应该最先到达的数据包。这意味着,不仅A不会达到B的预期性能,而且还可能使情况变得更糟,因为它可能会使B过载,现在B必须将所有这些传入数据包排队进行处理。
一段时间以来,人们期望构建网络服务和应用程序的人们能够应对他们编写的代码中的上述挑战。在我们的流控制示例中,这意味着应用程序本身必须包含逻辑以确保我们不会因数据包而使服务过载。大量的网络逻辑与你的业务逻辑并列。在我们的抽象图中,将是这样的:
幸运的是,技术飞速发展,并且很快有足够的标准(如TCP/IP)将解决方案集成到网络堆栈本身中,以解决流控和许多其他问题。这意味着该代码段仍然存在,但是已经从你的应用程序中提取到了操作系统提供的基础网络层:
这种模式已经取得了巨大的成功。很少有组织能不使用商业操作系统附带的 TCP/IP 技术栈来推动业务发展,即使需要高性能和可靠性也是如此。
当我们第一次开始使用微服务时发生了什么
多年来,计算机变得越来越便宜和普及,并且上述网络技术栈已被证明是可靠的实现系统连接的工具集。有了更多的节点和稳定的连接,整个行业出现了各种各样的网络系统,从细粒度的分布式代理和对象到由较大但仍分布很广的组件组成的面向服务的架构。
这种极端的分布式带来了许多有趣的高级用例和收益,但同时也带来了一些挑战。其中一些挑战是全新的,但其他挑战只是我们在谈论原始网络时所讨论的挑战的更高版本。
在上世纪90年代,Peter Deutsch 和他在 Sun Microsystems 的工程师合编了“分布式计算的8个谬论”,其中列出了人们在使用分布式系统时倾向于做出的一些假设。Peter 的观点是,这些可能在更原始的网络架构或理论模型中是正确的,但在现代世界却不成立:
- 网络可靠
- 延迟为零
- 带宽无限大
- 网络安全
- 拓扑不变
- 只有一名管理员
- 传输成本为零
- 网络是同质化的
将上面的列表宣布为“谬论”意味着工程师不能仅仅忽略这些问题,而必须对其进行明确处理。
问题进一步复杂化,迁移到更多的分布式系统(我们通常称为微服务架构)在可操作性方面提出了新的需求。之前我们已经详细讨论了其中一些问题,但是这里列出了必须处理的内容:
- 快速配置计算资源
- 基本监控
- 快速部署
- 易于调配存储
- 轻松的访问边界
- 认证/授权
- 标准化RPC
因此,尽管数十年前开发的 TCP/IP 技术栈和通用网络模型仍然是使计算机相互通信的强大工具,但更复杂的架构又引入了另一层要求,而这些要求又必须由从事此技术的工程师来满足。
例如,考虑服务发现和熔断器,这是用于解决上面列出的一些弹性和分配挑战的两种技术。
随着历史趋向于重演,最早的基于微服务构建系统的组织遵循的策略与前几代互联网计算机的策略非常相似。这意味着处理上面列出的需求的责任留给了编写服务的工程师。
服务发现是自动查找哪些服务实例满足给定查询的过程。例如名为 Teams 的服务需要查找将环境属性设置为生产的名为 Players 的服务的实例。你将调用一些服务发现过程,该过程将返回合适的服务器列表。对于很多单体架构来说,这是一个简单的任务,通常使用 DNS、负载均衡器和一些关于端口号的约定来实现(例如,将其所有 HTTP 服务绑定到8080端口)。在更加分布的环境中,任务开始变得更加复杂,以前可以盲目地依靠 DNS 查找来找到依赖关系的服务现在必须处理诸如客户端负载均衡、多种不同环境(例如,staging 与 production)、地理位置分散的服务器等。如果你之前只需要一行代码来解析主机名,那么现在你的服务就需要很多样板代码来处理系统更加分布式所带来的各种情况。
熔断器是 Michael Nygard 在他的书 Release It 中分类的一种模式。 我喜欢 Martin Fowler 对该模式的总结:
熔断器的基本原理非常简单。你将受保护的函数调用包装在熔断器对象中,该对象将监视故障。一旦故障达到某个阈值,熔断器将触发熔断,并且所有对该熔断器的进一步调用都会返回错误,而根本不会进行受保护的调用。 通常,熔断器触发熔断,你还需要某种监视器警报。
这些都是出色且简单的设施,可为你的服务之间的交互增加更多的可靠性。然而,就像其他所有内容一样,随着分布式水平的提高,它们往往会变得更加复杂。系统中出现问题的可能性随着分布的增加呈指数级增长,因此,即使诸如“如果熔断器触发熔断时出现某种监视器警报”之类的简单事件也不一定变得直截了当。一个组件中的一个故障会在许多客户端之间造成一连串的影响,从而触发成千上万的熔断器同时熔断。过去仅需几行代码,现在又需要大量样板代码来处理仅在这个新世界中存在的情况。
实际上,上面列出的两个示例很难正确的实现,以至于大型、复杂的库(例如 Twitter 的 Finagle 和 Facebook 的 Proxygen )非常流行,因为它避免在每个服务中重写相同的逻辑。
上面描述的模型被大多数开创了微服务架构的组织所采用,例如 Netflix,Twitter 和SoundCloud。随着系统中服务数量的增加,他们也发现了此方法的各种缺点。
即使使用像 Finagle 这样的库,最昂贵的挑战可能是组织仍需要花费其工程师团队的时间来建立类库与其他生态系统的连接。根据我在 SoundCloud 和 DigitalOcean 的经验,我估计在100-250个工程师的组织中采用这种策略后,需要将1/10的员工专用于构建工具。有时,这笔费用是明确的,因为工程师被分配到专门负责构建工具的团队中,但是价格标签通常是不可见的,但随着工作时间的增加,价格标签会逐渐显现出来。
第二个问题是,以上设置限制了可用于微服务的工具、运行环境和语言。微服务库通常是为特定平台编写的,无论是编程语言还是 JVM 之类的运行环境。如果组织使用的平台是类库不支持的,则通常需要将代码移植到新平台上。这浪费了宝贵的工程时间。工程师不能再致力于核心业务和产品,而必须再次构建工具和基础架构。这就是为什么诸如 SoundCloud 和 DigitalOcean 之类的中型组织决定为其内部服务仅支持一个平台的原因,分别是 Scala 和 Go。
这个模型值得讨论的最后一个问题是治理。类库模型可以抽象化解决微服务架构需求所需功能的实现,但是它本身仍然是需要维护的组件。确保成千上万的服务实例使用相同或至少兼容的类库版本并非易事,并且每次更新都意味着集成、测试和重新部署所有服务,即使该服务本身未遭受任何损害也是如此。
下一个合理的步骤
与在网络技术栈中遇到的问题的类似,非常需要将大规模分布式服务所需的功能提取到基础平台中。
人们使用 HTTP 等高级协议编写非常复杂的应用程序和服务,甚至无需考虑 TCP 如何控制其网络上的数据包。这种情况正是我们在微服务中所需要的,在微服务中,从事服务开发的工程师可以专注于他们的业务逻辑,避免浪费时间在编写服务基础设施代码或管理整个团队的类库和框架。
将这个想法整合到我们的图中,我们可能会得到如下所示的结果:
不幸的是,更改网络技术栈以添加此层不是可行的任务。许多从业人员发现的解决方案是将其作为一组代理来实现。这里的想法是,服务不会直接连接其下游的依赖,而是所有流量都将通过软件的一小部分透明地添加所需的功能。
在此领域中,最早记录在案的发展使用了“边车”的概念。边车是在你的应用程序旁边运行并为其提供附加功能的辅助进程。2013年,Airbnb 撰写了有关 Synapse 和 Nerve(他们的 Sidecar 的开源实现)的文章。一年后,Netflix 推出了 Prana,这是一种致力于使非 JVM 的应用程序能够从其 NetflixOSS 生态系统中受益的 sidecar。在 SoundCloud,我们构建了 sidecar,使我们的 Ruby 历史系统可以使用我们为 JVM 微服务构建的基础设施。
尽管有许多此类的开源代理实现,但它们往往旨在与特定的基础设施组件一起使用。例如,在服务发现方面,Airbnb 的 Nerve&Synapse 假定服务已在 Zookeeper 中注册,而对于 Prana,则应使用 Netflix 自己的 Eureka 服务注册中心。
随着微服务架构的日益普及,我们最近看到了新一轮的代理浪潮,这些代理足够灵活以适应不同的基础设施组件和偏好。这个领域的第一个广为人知的系统是 Linkerd,它是由 Buoyant 根据工程师在 Twitter 微服务平台上的预先工作创建的。很快,Lyft 的工程师团队发布了 Envoy,它遵循类似的原则。
Service Mesh
在这种模型中,你的每个服务都将有一个伴随的代理 sidecar。鉴于服务只通过 Sidecar 代理相互通信,因此我们最终得到了类似于下图的部署:
Buoyant 的首席执行官 William Morgan 观察到,代理之间的互连形成了网状网络。2017年初,William 为该平台编写了一个定义,并将其称为 Service Mesh:
Service Mesh 是用于处理服务到服务通信的专用基础结构层。 它负责通过构成现代云原生应用程序的复杂服务拓扑可靠地传递请求。 实际上,Service Mesh 通常被实现为轻量级网络代理的矩阵,这些轻量级网络代理与应用程序代码一起部署,而无需了解应用程序。
他的定义最强大的方面可能是,它摆脱了将代理视为独立组件的想法,并认识到它们形成的网络本身就是有价值的东西。
随着组织将其微服务部署移至更复杂的运行环境如 Kubernetes 和 Mesos,人员和组织已开始使用那些平台提供的工具来正确实现此网状网络的想法。他们正从一组独立工作的独立代理转移到一个适当的、有些集中的控制面板。
纵观我们的鸟瞰图,我们看到实际的服务流量仍然直接从代理流向代理,但是控制面板感知每个代理实例。控制面板使代理可以实现访问控制和指标收集之类的事情,这需要合作:
最近发布的 Istio 项目是此类系统最杰出的案例。
全面了解 Service Mesh 在大规模系统中的影响还为时过早。这种方法的两个好处对我来说已经很明显了。首先,不必编写自定义软件来处理微服务架构的商业代码,这将使许多较小的组织可以享受以前仅适用于大型企业的功能,从而创建各种有趣的用例。第二个问题是,这种架构可能使我们最终实现使用最佳工具/语言完成工作的梦想,而不必担心每个平台的类库和模式的可用性。