Skip to content

云和微服务

云到底是什么

术语“云”已经被过度使用了。每个软件供应商都有云,每个软件供应商的平台都是支持云的,但是如果穿透这些天花乱坠的广告宣传,我们就会发现云计算有 3 种基本模式。它们是:

  • 基础设施即服务(Infrastructure as a Service,IaaS)
  • 平台即服务(Platform as a Service,PaaS)
  • 软件即服务(Software as a Service,SaaS)

为了更好地理解这些概念,让我们将每天的任务映射到不同的云计算模型中。当你想吃饭时,你有 4 种选择:

  1. 在家做饭

  2. 去商店买一顿预先做好的膳食,然后你加热并享用它

  3. 叫外卖送到家里

  4. 开车去餐厅吃饭

(不同的云计算模型归结于云供应商或你各自要负责什么。图片来源:Spring 微服务实战)

这些选择之间的区别是谁负责烹饪这些膳食,以及在哪里烹饪。

  1. 在内部自建模型中,想要在家里吃饭就需要自己做所有的工作,还要使用家里面的厨具和食材。

  2. 商店购买的食物就像使用“基础设施即服务”(IaaS)计算模型一样,使用店内的厨师和厨具预先制作餐点,但你仍然有责任加热膳食并在家里吃(可能还要清洗餐具)。

  3. 在“平台即服务”(PaaS)模型中,你仍然需要提供场地和家具,但餐厅老板提供厨具、食材和厨师来做饭,外卖员提供配送。

  4. 在“软件即服务”(SaaS)模型中,你去到一家餐厅,在那里,所有食物都已为你准备好。你在餐厅吃饭,然后在吃完后买单,你也不需要自己去准备或清洗餐具。

每个模型中的关键项都是控制:由谁来负责维护基础设施,以及构建应用程序的技术选择是什么?在 IaaS 模型中,云供应商提供基础设施,但你需要选择技术并构建最终的解决方案;而在 SaaS 模型中,你就是供应商所提供的服务的被动消费者,无法对技术进行选择,同时也没有任何责任来维护应用程序的基础设施。

新兴的云平台

上面介绍了目前正在使用的 3 种核心云平台类型(即 IaaS、PaaS 和 SaaS)。然而,新的云平台类型正在出现。这些新平台包括“函数即服务”(Functions as a Service,FaaS)和“容器即服务”(Container as a Service,CaaS)。基于 FaaS 的应用程序会使用像亚马逊的 Lambda 技术和 Google Cloud 函数这样的设施,应用会将代码块以“无服务器”(serverless)的形式部署,这些代码会完全在云提供商的平台计算设施上运行。使用 FaaS 平台,无需管理任何服务器基础设施,只需支付执行函数所需的计算周期。

使用“容器即服务”模型,开发人员将微服务作为便携式虚拟容器(如 Docker)进行构建并部署到云供应商。与 IaaS 模型不同,使用 IaaS 的开发人员必须管理部署服务的虚拟机,而 CaaS 则是将服务部署在轻量级的虚拟容器中。云供应商会提供运行容器的虚拟服务器,以及用于构建、部署、监控和伸缩容器的综合工具。

需要重点注意的是,使用云计算的 FaaS 和 CaaS 模型,开发人员仍然可以构建基于微服务的架构。请记住,微服务概念的重点在于构建有限职责的小型服务,并使用基于 HTTP 的接口进行通信。新兴的云计算平台(如 FaaS 和 CaaS)是部署微服务的替代基础设施机制。

为什么是云和微服务

微服务架构的核心概念之一就是每个服务都被打包和部署为离散的独立制品。服务实例应该迅速启动,服务的每一个实例都是完全相同的。

作为编写微服务的开发人员,我们迟早要决定是否将服务部署到下列某个环境之中。

  • 物理服务器 —— 虽然可以构建和部署微服务到物理机器,但由于物理服务器的局限性,很少有组织会这样做。开发人员不能快速提高物理服务器的容量,并且在多个物理服务器之间水平伸缩微服务可能会变得成本非常高。

  • 虚拟机镜像 —— 微服务的主要优点之一是能够快速启动和关闭微服务实例,以响应可伸缩性和服务故障事件。虚拟机是主要云供应商的心脏和灵魂。微服务可以打包在虚拟机镜像中,然后开发人员可以在 IaaS 私有或公有云中快速部署和启动服务的多个实例。

  • 虚拟容器 —— 虚拟容器是在虚拟机镜像上部署微服务的自然延伸。许多开发人员不是将服务部署到完整的虚拟机,而是将 Docker容器(或等效的容器技术)部署到云端。虚拟容器在虚拟机内运行。使用虚拟容器,可以将单个虚拟机隔离成共享相同虚拟机镜像的一系列独立进程。

基于云的微服务的优势是以弹性的概念为中心。云服务供应商允许开发人员在几分钟内快速启动新的虚拟机和容器。如果服务容量需求下降,开发人员可以关闭虚拟服务器,而不会产生任何额外的费用。使用云供应商部署微服务可以显著地提高应用程序的水平可伸缩性(添加更多的服务器和服务实例)。服务器弹性也意味着应用程序可以更具弹性。如果其中一台微服务遇到问题并且处理能力正在不断地下降,那么启动新的服务实例可以让应用程序保持足够长的存活时间,让开发团队能够从容而优雅地解决问题。

下面列出的是用于微服务的常见部署拓扑结构。

  • 简化的基础设施管理 —— IaaS 云计算供应商可以让开发人员最有效地控制他们的服务。开发人员可以通过简单的 API 调用来启动和停止新服务。使用 IaaS 云解决方案,只需支付使用基础设施的费用。

  • 大规模的水平可伸缩性 —— IaaS 云服务供应商允许开发人员快速简便地启动服务的一个或多个实例。这种功能意味着可以快速扩大服务以及绕过表现不佳或出现故障的服务器。

  • 通过地理分布实现高冗余 —— IaaS 供应商必然拥有多个数据中心。通过使用 IaaS 云供应商部署微服务,可以比使用数据中心里的集群拥有更高级别的冗余。

::: 为什么不是基于 PaaS 的微服务? 前面讨论了 3 种云平台(基础设施即服务、平台即服务和软件即服务)。虽然某些云供应商可以让开发人员抽象出微服务的部署基础设施,但目前来看,选择保持独立于供应商并部署应用程序的所有部分(包括服务器)是最常见的方式。

例如,亚马逊 和 Microsoft Azure 可以让开发人员无需知道底层应用程序容器即可部署服务。它们提供了一个 Web 接口和 API,以允许将应用程序部署为 WAR 或 JAR 文件。设置和调优应用程序服务器和相应的 Java 容器被抽象了出来。虽然这很方便,但每个云供应商的平台与其各自的 PaaS 解决方案有着不同的特点。

IaaS 方案虽然需要更多的工作,但可跨多个云供应商进行移植,并允许开发人员通过产品覆盖更广泛的受众。一般来说,基于 PaaS 的云解决方案可以快速启动开发工作,但一旦应用程序拥有足够多的微服务,开发人员就会开始需要云服务商提供的 IaaS 风格的灵活性。

前面提到过新的云计算平台,如函数即服务(FaaS)和容器即服务(CaaS)。如果不小心,基于 FaaS 的平台就会将代码锁定到一个云供应商平台上,因为代码会被部署到供应商特定的运行时引擎上。使用基于 FaaS 的模型,开发人员可能会使用通用的编程语言(Java、Python、JavaScript 等)编写服务,但开发人员仍然会将自己严格束缚在底层供应商的 API 和部署函数的运行时引擎上。 :::

微服务的特征

在微服务出现之前,项目往往倾向于遵循大型传统的瀑布开发方法,坚持在项目开始时界定应用的所有需求和设计。这些项目的开发人员非常重视软件说明书的“正确性”,却很少能够满足新的业务需求,也很少能够重构并从开发初期的错误中重新思考和学习。

但现实情况是,软件开发并不是一个由定义和执行所组成的线性过程,而是一个演化过程,在开发团队真正明白手头的问题前,需要经历与客户沟通、向客户学习和向客户交付的数次迭代。

使用传统的瀑布方法所面临的挑战在于,许多时候,这些项目交付的软件制品的粒度具有以下特点。

  • 紧耦合的 —— 业务逻辑的调用发生在编程语言层面,而不是通过实现中立的协议(如 SOAP 和 REST)。这大大增加了即使对应用程序组件进行小的修改也可能打破应用程序的其他部分并引入新漏洞的机会。

  • 有漏洞的 —— 大多数大型软件应用程序都在管理着不同类型的数据。例如,客户关系管理(CRM)应用程序可能会管理客户、销售和产品信息。在传统的模型里,这些数据位于相同的数据模型中并在同一个数据存储中保存。即使数据之间存在明显的界限,在绝大多数的情况下,来自一个领域的团队也很容易直接访问属于另一个团队的数据。这种对数据的轻松访问引入了隐藏的依赖关系,并让组件的内部数据结构的实现细节泄漏到整个应用程序中。即使对单个数据库表的更改也可能需要在整个应用程序中进行大量的代码更改和回归测试。

  • 单体的 —— 由于传统应用程序的大多数组件都存放在多个团队共享的单个代码库中,任何时候更改代码,整个应用程序都必须重新编译、重新运行并且需要通过一个完整的测试周期并重新部署。无论是新客户的需求还是修复错误,应用程序代码库的微小变化都将变得昂贵和耗时,并且几乎不可能及时实现大规模的变化。

基于微服务的架构采用不同的方法来交付功能。具体来说,基于微服务的架构具有以下特点。

  • 有约束的 —— 微服务具有范围有限的单一职责集。微服务遵循 UNIX 的理念,即应用程序是服务的集合,每个服务只做一件事,并只做好一件事。

  • 松耦合的 —— 基于微服务的应用程序是小型服务的集合,服务之间使用非专属调用协议(如 HTTP 和 REST)通过非特定实现的接口彼此交互。与传统的应用程序架构相比,只要服务的接口没有改变,微服务的所有者可以更加自由地对服务进行修改。

  • 抽象的 —— 微服务完全拥有自己的数据结构和数据源。微服务所拥有的数据只能由该服务修改。可以锁定微服务数据的数据库访问控制,仅允许该服务访问它。

  • 独立的 —— 微服务应用程序中的每个微服务可以独立于应用程序中使用的其他服务进行编译和部署。这意味着,与依赖更重的单体应用程序相比,这样对变化进行隔离和测试更容易。

为什么这些微服务架构属性对基于云的开发很重要?基于云的应用程序通常有以下特点。

  • 拥有庞大而多样化的用户群 —— 不同的客户需要不同的功能,他们不想在开始使用这些功能之前等待漫长的应用程序发布周期。微服务允许功能快速交付,因为每个服务的范围很小,并通过一个定义明确的接口进行访问。

  • 极高的运行时间要求 —— 由于微服务的分散性,基于微服务的应用程序可以更容易地将故障和问题隔离到应用程序的特定部分之中,而不会使整个应用程序崩溃。这可以减少应用程序的整体宕机时间,并使它们对问题更有抵御能力。

  • 不均匀的容量需求 —— 在企业数据中心内部部署的传统应用程序通常具有一致的使用模式,这些模式会随着时间的推移而定期出现,这使这种类型的应用程序的容量规划变得很简单。但在一个基于云的应用中,微博上的一条热搜或双十一期间的购物活动就能够极大带动对基于云计算的应用的需求。

因为微服务应用程序被分解成可以彼此独立部署的小组件,所以能够更容易将重点放在正处于高负载的组件上,并将这些组件在云中的多个服务器上进行水平伸缩。

何时不应该使用微服务

什么时候不应该使用微服务来构建应用程序呢?以下几点是需要考量的因素:

  1. 构建分布式系统的复杂性
  2. 虚拟服务器/容器散乱
  3. 应用程序的类型
  4. 数据事务和一致性

构建分布式系统的复杂性

因为微服务是分布式和细粒度(小)的,所以它们在应用程序中引入了一层复杂性,而在单体应用程序中就不会出现这样的情况。微服务架构需要高度的运维成熟度。除非组织愿意投入高分布式应用程序获得成功所需的自动化和运维工作(监控、伸缩),否则不要考虑使用微服务。

服务器散乱

微服务最常用的部署模式之一就是在一个服务器上部署一个微服务实例。在基于微服务的大型应用程序中,最终可能需要 50~100 台服务器或容器(通常是虚拟的),这些服务器或容器必须单独搭建和维护。即使在云中运行这些服务的成本较低,管理和监控这些服务器的操作复杂性也是巨大的。也就是说,必须对微服务的灵活性与运行所有这些服务器的成本进行权衡。

应用程序的类型

微服务面向可复用性,并且对构建需要高度弹性和可伸缩性的大型应用程序非常有用。这就是这么多云计算公司采用微服务的原因之一。如果正在构建小型的、部门级的应用程序或具有较小用户群的应用程序,那么搭建一个分布式模型(如微服务)的复杂性可能太昂贵了,不值得。

数据事务和一致性

开始关注微服务时,需要考虑服务的数据使用模式以及服务消费者如何使用它们。微服务包装并抽象出少量的表,作为执行“操作型”任务的机制,如创建、添加和执行针对存储的简单(非复杂的)查询,其工作效果很好。

如果应用程序需要跨多个数据源进行复杂的数据聚合或转换,那么微服务的分布式性质会让这项工作变得很困难。这样的微服务总是承担太多的职责,也可能变得容易受到性能问题的影响。

还要记住,在微服务间执行事务没有标准。如果需要事务管理,那就需要自己构建逻辑。另外,微服务可以通过使用消息进行通信。消息传递在数据更新中引入了延迟。应用程序需要处理最终的一致性,数据的更新可能不会立即出现。

微服务不只是编写代码

尽管构建单个微服务的概念很易于理解,但运行和支持健壮的微服务应用程序(尤其是在云中运行)不只是涉及为服务编写代码。编写健壮的服务需要考虑几个主题。

(微服务不只是业务逻辑,还需要考虑服务的运行环境以及服务的伸缩性和弹性。图片来源:Spring 微服务实战)

  • 大小适当 —— 如何确保正确地划分微服务的大小,以避免微服务承担太多的职责?请记住,适当的大小允许快速更改应用程序,并降低整个应用程序中断的总体风险。

  • 位置透明 —— 在微服务应用程序中,多个服务实例可以快速启动和关闭时,如何管理服务调用的物理细节?

  • 有弹性 —— 如何通过绕过失败的服务,确保采取“快速失败”的方法来保护微服务消费者和应用程序的整体完整性?

  • 可重复 —— 如何确保提供的每个新服务实例与生产环境中的所有其他服务实例具有相同的配置和代码库?

  • 可伸缩 —— 如何使用异步处理和事件来最小化服务之间的直接依赖关系,并确保可以优雅地扩展微服务?

以下模式可以回答这些问题。

  • 核心微服务开发模式
  • 微服务路由模式
  • 微服务客户端弹性模式
  • 微服务安全模式
  • 微服务日志记录和跟踪模式
  • 微服务构建和部署模式

核心微服务开发模式

核心微服务开发模式解决了构建微服务的基础问题。

(在设计微服务时必须考虑服务是如何通信以及被消费的。图片来源:Spring 微服务实战)

  • 服务粒度 —— 如何将业务域分解为微服务,使每个微服务都具有适当程度的职责?服务职责划分过于粗粒度,在不同的业务问题领域重叠,会使服务随着时间的推移变得难以维护。服务职责划分过于细粒度,则会使应用程序的整体复杂性增加,并将服务变为无逻辑的(除了访问数据存储所需的逻辑)“哑”数据抽象层。

  • 通信协议 —— 开发人员如何与服务进行通信?使用 XML(Extensible Markup Language,可扩展标记语言)、JSON(JavaScript Object Notation,JavaScript 对象表示法)或诸如 Thrift 之类的二进制协议来与微服务传输数据?

  • 接口设计 —— 如何设计实际的服务接口,便于开发人员进行服务调用?如何构建服务 URL 来传达服务意图?如何版本化服务?精心设计的微服务接口使服务变得更直观。

  • 服务的配置管理 —— 如何管理微服务的配置,以便在不同云环境之间移动时,不必更改核心应用程序代码或配置?

  • 服务之间的事件处理 —— 如何使用事件解耦微服务,以便最小化服务之间的硬编码依赖关系,并提高应用程序的弹性?

微服务路由模式

微服务路由模式负责处理希望消费微服务的客户端应用程序,使客户端应用程序发现服务的位置并路由到服务。在基于云的应用程序中,可能会运行成百上千个微服务实例。需要抽象这些服务的物理 IP 地址,并为服务调用提供单个入口点,以便为所有服务调用持续强制执行安全和内容策略。

服务发现和路由回答了这个问题:如何将客户的服务请求发送到服务的特定实例?

  • 服务发现 —— 如何使微服务变得可以被发现,以便客户端应用程序在不需要将服务的位置硬编码到应用程序的情况下找到它们?如何确保从可用的服务实例池中删除表现不佳的微服务实例?

  • 服务路由 —— 如何为所有服务提供单个入口点,以便将安全策略和路由规则统一应用于微服务应用程序中的多个服务和服务实例?如何确保团队中的每位开发人员不必为他们的服务提供自己的服务路由解决方案?

(服务发现和路由是所有大规模微服务应用的关键部分。图片来源:Spring 微服务实战)

在图中,服务发现和服务路由之间似乎具有硬编码的事件顺序(首先是服务路由,然后是服务发现)。然而,这两种模式并不彼此依赖。例如,我们可以实现没有服务路由的服务发现,也可以实现服务路由而无需服务发现(尽管这种实现更加困难)。

微服务客户端弹性模式

因为微服务架构是高度分布式的,所以必须对如何防止单个服务(或服务实例)中的问题级联暴露给服务的消费者十分敏感。为此,这里将介绍 4 种客户端弹性模式。

  • 客户端负载均衡 —— 如何在服务客户端上缓存服务实例的位置,以便对微服务的多个实例的调用负载均衡到该微服务的所有健康实例?

  • 断路器模式 —— 如何阻止客户继续调用出现故障的或遭遇性能问题的服务?如果服务运行缓慢,客户端调用时会消耗它的资源。开发人员希望出现故障的微服务调用能够快速失败,以便主叫客户端可以快速响应并采取适当的措施。

  • 后备模式 —— 当服务调用失败时,如何提供“插件”机制,允许服务的客户端尝试通过调用微服务之外的其他方法来执行工作?

  • 舱壁模式 —— 微服务应用程序使用多个分布式资源来执行工作。如何区分这些调用,以便表现不佳的服务调用不会对应用程序的其他部分产生负面影响?

(使用微服务时,必须保护服务调用者远离表现不佳的服务。记住, 慢速或无响应的服务所造成的中断并不仅仅局限于直接关联的服务。图片来源:Spring 微服务实战)

图中展示了这些模式如何在服务表现不佳时,保护服务消费者不受影响。

微服务安全模式

微服务绕不开安全性。下面介绍 3 种基本的安全模式。

  • 验证 —— 如何确定调用服务的客户端就是它们声称的那个主体?

  • 授权 —— 如何确定调用微服务的客户端是否允许执行它们正在进行的操作?

  • 凭据管理和传播 —— 如何避免客户端每次都要提供凭据信息才能访问事务中涉及的服务调用?

(使用基于令牌的安全方案,可以实现服务验证和授权,而无需传递客户端凭据。图片来源:Spring 微服务实战)

图中展示了如何实现上述 3 种模式来构建可以保护微服务的验证服务。

微服务日志记录和跟踪模式

微服务架构的优点是单体应用程序被分解成可以彼此独立部署的小的功能部件,而它的缺点是调试和跟踪应用程序和服务中发生的事情要困难得多。下面介绍 3 种核心日志记录和跟踪模式。

  • 日志关联 —— 一个用户事务会调用多个服务,如何将这些服务所生成的日志关联到一起?

  • 日志聚合 —— 借助这种模式,可以将微服务(及其各个实例)生成的所有日志合并到一个可查询的数据库中。

  • 微服务跟踪 —— 如何在涉及的所有服务中可视化客户端事务的流程,并了解事务所涉及的服务的性能特征?

(一个深思熟虑的日志记录和跟踪策略使跨多个服务的调试事务变得可管理。图片来源:Spring 微服务实战)

图中展示了这些模式如何配合在一起。

微服务构建和部署模式

微服务架构的核心原则之一是,微服务的每个实例都应该和其他所有实例相同。“配置漂移”(某些文件在部署到服务器之后发生了一些变化)是不允许出现的,因为这可能会导致应用程序不稳定。

为此,将基础设施的配置集成到构建部署过程中是一个不错的方案,这样就不再需要将软件制品(如 Java WAR 或 EAR )部署到已经在运行的基础设施中。相反,开发人员希望在构建过程中构建和编译微服务并准备运行微服务的虚拟服务器镜像。部署微服务时,服务器运行所需的整个机器镜像都会进行部署。

(开发人员希望微服务及其运行所需的服务器成为在不同环境间作为整体部署的原子制件。图片来源:Spring 微服务实战)

  • 构建和部署管道 —— 如何创建一个可重复的构建和部署过程,只需一键即可构建和部署到组织中的任何环境?

  • 基础设施即代码 —— 如何将服务的基础设施作为可在源代码管理下执行和管理的代码去对待?

  • 不可变服务器 —— 一旦创建了微服务镜像,如何确保它在部署之后永远不会更改?

  • 凤凰服务器(Phoenix server) —— 服务器运行的时间越长,就越容易发生配置漂移。如何确保运行微服务的服务器定期被拆卸,并重新创建一个不可变的镜像?

使用这些模式和主题的目的是,在配置漂移影响到上层环境(如交付准备环境或生产环境)之前,尽可能快地公开并消除配置漂移。

基于微服务架构构建应用程序的视角

构建一个成功的微服务架构的过程需要结合软件开发组织内多个人的视角。尽管交付整个应用程序需要的不仅仅是技术人员,但一般来说,成功的微服务开发的基础是从以下 3 个关键角色的视角开始的。

  • 架构师 —— 架构师的工作是看到大局,了解应用程序如何分解为单个微服务,以及微服务如何交互以交付解决方案。

  • 软件开发人员 —— 软件开发人员编写代码并详细了解如何将编程语言和该语言的开发框架用于交付微服务。

  • DevOps 工程师 —— DevOps 工程师不仅为生产环境而且为所有非生产环境提供服务部署和管理的智慧。DevOps 工程师的口号是:保障每个环境中的一致性可重复性

架构师的故事:设计微服务架构

架构师在软件项目中的作用是提供待解决问题的工作模型。架构师的工作是提供脚手架,开发人员将根据这些脚手架构建他们的代码,使应用程序所有部件都组合在一起。

在构建微服务架构时,项目的架构师主要关注以下 3 个关键任务:

  1. 分解业务问题
  2. 建立服务粒度
  3. 定义服务接口

分解业务问题

在微服务架构中,架构师将业务问题分解成代表离散活动领域的块。这些块封装了与业务域特定部分相关联的业务规则和数据逻辑。

分离业务领域是一门艺术,而不是非黑即白的科学。可以采用以下指导方针将业务问题识别和分解为备选的微服务。

  1. 描述业务问题,并聆听用来描述问题的名词。在描述问题时,反复使用的同一名词通常意味着它们是核心业务领域并且适合创建微服务。

  2. 注意动词。动词突出了动作,通常代表问题域的自然轮廓。如果发现自己说出“事务 X 需要从实务 A 和实务 B 获取数据”这样的话,通常表明多个服务正在起作用。

  3. 寻找数据内聚。将业务问题分解成离散的部分时,要寻找彼此高度相关的数据。如果在会话过程中,突然读取或更新与迄今为止所讨论的内容完全不同的数据,那么就可能还存在其他候选服务。微服务应完全拥有自己的数据。

建立服务粒度

构建微服务架构时,粒度的问题很重要,可以采用以下思想来确定正确的解决方案。

  1. 开始的时候可以让微服务涉及的范围更广泛一些,然后将其重构到更小的服务 —— 在开始微服务旅程之初,容易出现的一个极端情况就是将所有的事情都变成微服务。但是将问题域分解为小型的服务通常会导致过早的复杂性,因为微服务变成了细粒度的数据服务。

  2. 重点关注服务如何相互交互 —— 这有助于建立问题域的粗粒度接口。从粗粒度重构到细粒度是比较容易的。

  3. 随着对问题域的理解不断增长,服务的职责将随着时间的推移而改变 —— 通常来说,当需要新的应用功能时,微服务就会承担起职责。最初的微服务可能会发展为多个服务,原始的微服务则充当这些新服务的编排层,负责将应用的其他部分的功能封装起来。

糟糕的微服务的“味道”

如何知道微服务的划分是否正确?如果微服务过于粗粒度,可能会看到以下现象。

  • 服务承担过多的职责 —— 服务中的业务逻辑的一般流程很复杂,并且似乎正在执行一组过于多样化的业务规则。

  • 该服务正在跨大量表来管理数据 —— 微服务是它管理的数据的记录系统。如果发现自己将数据持久化存储到多个表或接触到当前数据库以外的表,那么这就是一条服务过于粗粒度的线索。一个指导方针 —— 微服务应该不超过 3~5 个表。再多一点,服务就可能承担了太多的职责。

  • 测试用例太多 —— 随着时间的推移,服务的规模和职责会增长。如果一开始有一个只有少量测试用例的服务,到了最后该服务需要数百个单元测试用例和集成测试用例,那么就可能需要重构。

如果微服务过于细粒度呢?

  • 问题域的一部分微服务像兔子一样繁殖 —— 如果一切都成为微服务,将服务中的业务逻辑组合起来会变得复杂和困难,因为完成一项工作所需的服务数量会快速增长。一种常见的“坏味道”出现在应用程序有几十个微服务,并且每个服务只与一个数据库表进行交互时。

  • 微服务彼此间严重相互依赖 —— 在问题域的某一部分中,微服务相互来回调用以完成单个用户请求。

  • 微服务成为简单CRUD(Create,Read,Update,Delete)服务的集合 —— 微服务是业务逻辑的表达,而不是数据源的抽象层。如果微服务除了 CRUD 相关逻辑之外什么都不做,那么它们可能被划分得太细粒度了。

应该通过演化思维的过程来开发一个微服务架构,在这个过程中,你知道不会第一次就得到正确的设计。这就是最好从一组粗粒度的服务而不是一组细粒度的服务开始的原因。同样重要的是,不要对设计带有教条主义。我们可能会面临两个单独的服务之间交互过于频繁,或者服务的域之间不存在明确的边界这样的物理约束,当面临这样的约束时,需要创建一个聚合服务来将数据连接在一起。

最后,采取务实的做法并进行交付,而不是浪费时间试图让设计变得完美,最终导致没有东西可以展现你的努力。

互相交流:定义服务接口

架构师需要关心的最后一部分,是应用程序中的微服务该如何彼此交流。使用微服务构建业务逻辑时,服务的接口应该是直观的,开发人员应该通过学习应用程序中的一两个服务来获得应用程序中所有服务的工作节奏。

一般来说,可使用以下指导方针思考服务接口设计。

  1. 拥抱 REST 的理念 —— REST 对服务的处理方式是将 HTTP 作为服务的调用协议并使用标准 HTTP 动词(GET、PUT、POST 和 DELETE)。围绕这些 HTTP 动词对基本行为进行建模。

  2. 使用 URI 来传达意图 —— 用作服务端点的 URI 应描述问题域中的不同资源,并为问题域内的资源的关系提供一种基本机制。

  3. 请求和响应使用 JSON —— JavaScript 对象表示法(JavaScript Object Notation,JSON)是一个非常轻量级的数据序列化协议,并且比 XML 更容易使用。

  4. 使用 HTTP 状态码来传达结果 —— HTTP 协议具有丰富的标准响应代码,以指示服务的成功或失败。学习这些状态码,并且最重要的是在所有服务中始终如一地使用它们。

所有这些指导方针都是为了完成一件事,那就是使服务接口易于理解和使用。我们希望开发人员坐下来查看一下服务接口就能开始使用它们。如果微服务不容易使用,开发人员就会另辟道路,破坏架构的意图。

开发人员的故事:用编程语言和该语言的开发框架构建微服务

在构建微服务时,从概念到实现,需要视角的转换。具体来说,开发人员需要建立一个实现应用程序中每个微服务的基本模式。虽然每项服务都将是独一无二的,但我们希望确保使用的是一个移除样板代码的框架,并且微服务的每个部分都采用相同的布局。

可以基于以下 4 条原则来构建微服务。这些原则具体如下。

  1. 微服务应该是独立的可独立部署的,多个服务实例可以使用单个软件制品进行启动和拆卸。

  2. 微服务应该是可配置的。当服务实例启动时,它应该从中央位置读取需要配置其自身的数据,或者让它的配置信息作为环境变量传递。配置服务无需人为干预。

  3. 微服务实例需要对客户端是透明的。客户端不应该知道服务的确切位置。相反,微服务客户端应该与服务发现代理通信,该代理将允许应用程序定位微服务的实例,而不必知道微服务的物理位置。

  4. 微服务应该传达它的健康信息,这是云架构的关键部分。一旦微服务实例无法正常运行,客户端需要绕过不良服务实例。

这 4 条原则揭示了存在于微服务开发中的悖论。微服务在规模和范围上更小,但使用微服务会在应用程序中引入了更多的活动部件,特别是因为微服务在它自己的分布式容器中彼此独立地分布和运行,引入了高度协调性的同时也更容易为应用程序带来故障点。

DevOps 工程师的故事:构建运行时的严谨性

对于 DevOps 工程师来说,微服务的设计关乎在投入生产后如何管理服务。编写代码通常是很简单的,而保持代码运行却是困难的。

从 DevOps 的角度来看,必须解决微服务的运维需求,并将这 4 条原则转化为每次构建和部署微服务到环境中时发生的一系列生命周期事件。这 4 条原则可以映射到以下运维生命周期步骤。

  • 服务装配 —— 如何打包和部署服务以保证可重复性和一致性,以便相同的服务代码和运行时被完全相同地部署?

  • 服务引导 —— 如何将应用程序和环境特定的配置代码与运行时代码分开,以便可以在任何环境中快速启动和部署微服务实例,而无需对配置微服务进行人为干预?

  • 服务注册/发现 —— 部署一个新的微服务实例时,如何让新的服务实例可以被其他应用程序客户端发现。

  • 服务监控 —— 在微服务环境中,由于高可用性需求,同一服务运行多个实例非常常见。从 DevOps 的角度来看,需要监控微服务实例,并确保绕过微服务中的任何故障,而且状况不佳的服务实例会被拆卸。

下图展示了这 4 个步骤是如何配合在一起的。

(当微服务启动时,它将在其生命周期中经历多个步骤。图片来源:Spring 微服务实战)

服务装配:打包和部署微服务

从 DevOps 的角度来看,微服务架构背后的一个关键概念是可以快速部署微服务的多个实例,以应对变化的应用程序环境(如用户请求的突然涌入、基础设施内部的问题等)。

为了实现这一点,微服务需要作为带有所有依赖项的单个制品进行打包和安装,然后可以将这个制品部署到安装了 Java JDK 的任何服务器上。这些依赖项还包括承载微服务的运行时引擎(如 HTTP 服务器或应用程序容器)。

这种持续构建、打包和部署的过程就是服务装配。下图展示了有关服务装配步骤的其他详细信息。

(在服务装配步骤中,源代码与其运行时引擎一起被编译和打包。图片来源:Spring 微服务实战)

服务引导:管理微服务的配置

服务引导发生在微服务首次启动并需要加载其应用程序配置信息的时候。下图为引导处理提供了更多的上下文。

(服务启动(引导)时,它会从中央存储库读取其配置。图片来源:Spring 微服务实战)

任何应用程序开发人员都知道,有时需要使应用程序的运行时行为可配置。通常这涉及从应用程序部署的属性文件读取应用程序的配置数据,或从数据存储区(如关系数据库)读取数据。

微服务通常会遇到相同类型的配置需求。不同之处在于,在云上运行的微服务应用程序中,可能会运行数百甚至数千个微服务实例。更为复杂的是,这些服务可能分散在全球。由于存在大量的地理位置分散的服务,重新部署服务以获取新的配置数据变得难以实施。

将数据存储在服务器外部的数据存储中解决了这个问题,但云上的微服务提出了一系列独特的挑战。

  1. 配置数据的结构往往是简单的,通常读取频繁但不经常写入。在这种情况下,使用关系数据库就是“杀鸡用牛刀”,因为关系数据库旨在管理比一组简单的键值对更复杂的数据模型。

  2. 因为数据是定期访问的,但是很少更改,所以数据必须具有低延迟的可读性。

  3. 数据存储必须具有高可用性,并且靠近读取数据的服务。配置数据存储不能完全关闭,否则它将成为应用程序的单点故障。

服务注册和发现:客户端如何与微服务通信

从微服务消费者的角度来看,微服务应该是位置透明的,因为在基于云的环境中,服务器是短暂的。短暂意味着承载服务的服务器通常比在企业数据中心运行的服务的寿命更短。可以通过分配给运行服务的服务器的全新IP地址来快速启动和拆除基于云的服务。

通过坚持将服务视为短暂的可自由处理的对象,微服务架构可以通过运行多个服务实例来实现高度的可伸缩性和可用性。服务需求和弹性可以在需要的情况下尽快进行管理。每个服务都有一个分配给它的唯一和非永久的IP地址。短暂服务的缺点是,随着服务的不断出现和消失,手动或手工管理大量的短暂服务容易造成运行中断。

微服务实例需要向第三方代理注册。此注册过程称为服务发现。下图是有关此过程的详细信息。

(服务发现代理抽象出服务的物理位置。图片来源:Spring 微服务实战)

当微服务实例使用服务发现代理进行注册时,微服务实例将告诉发现代理两件事情:服务实例的物理 IP 地址或域名地址,以及应用程序可以用来查找服务的逻辑名称。某些服务发现代理还要求能访问到注册服务的 URL,服务发现代理可以使用此 URL 来执行健康检查。

然后,服务客户端与发现代理进行通信以查找服务的位置。

传达微服务的“健康状况”

服务发现代理不只是扮演了一名引导客户端到服务位置的交通警察的角色。在基于云的微服务应用程序中,通常会有多个服务实例运行,其中某些服务实例迟早会出现一些问题。服务发现代理监视其注册的每个服务实例的健康状况,并从其路由表中移除有问题的服务实例,以确保客户端不会访问已经发生故障的服务实例。

在发现微服务后,服务发现代理将继续监视和 ping 健康检查接口,以确保该服务可用。下图提供了此步骤的上下文。

(服务发现代理使用公开的健康状况 URL 来检查微服务的“健康状况”。图片来源:Spring 微服务实战)

通过构建一致的健康检查接口,我们可以使用基于云的监控工具来检测问题并对其进行适当的响应。

如果服务发现代理发现服务实例存在问题,则可以采取纠正措施,如关闭出现故障的实例或启动另外的服务实例。

将视角综合起来

云中的微服务看起来很简单,但要想成功,却需要有一个综合的视角,将架构师、开发人员和 DevOps 工程师的视角融到一起,形成一个紧密结合的视角。每个视角的关键结论概括如下。

  1. 架构师 —— 专注于业务问题的自然轮廓。描述业务问题域,并听取别人所讲述的故事,按照这种方式,筛选出目标备选微服务。还要记住,最好从“粗粒度”的微服务开始,并重构到较小的服务,而不是从一大批小型服务开始。微服务架构像大多数优秀的架构一样,是按需调整的,而不是预先计划好的。

  2. 软件工程师 —— 尽管服务很小,但并不意味着就应该把良好的设计原则抛于脑后。专注于构建分层服务,服务中的每一层都有离散的职责。避免在代码中构建框架的诱惑,并尝试使每个微服务完全独立。过早的框架设计和采用框架可能会在应用程序生命周期的后期产生巨大的维护成本。

  3. DevOps 工程师 —— 服务不存在于真空中。尽早建立服务的生命周期。DevOps 视角不仅要关注如何自动化服务的构建和部署,还要关注如何监控服务的健康状况,并在出现问题时做出反应。实施服务通常需要比编写业务逻辑更多的工作,也更需要深谋远虑。

将服务配置与服务代码分开

在某种程度上来说,开发人员是被迫将配置信息与他们的代码分开的。毕竟,自上学以来,他们就一直被灌输不要将硬编码带入应用程序代码中的观念。许多开发人员在应用程序中使用一个常量类文件来帮助将所有配置集中在一个地方。将应用程序配置数据直接写入代码中通常是有问题的,因为每次对配置进行更改时,应用程序都必须重新编译和重新部署。为了避免这种情况,开发人员会将配置信息与应用程序代码完全分离。这样就可以很容易地在不进行重新编译的情况下对配置进行更改,但这样做也会引入复杂性,因为现在存在另一个需要与应用程序一起管理和部署的制品。

许多开发人员转向低层级的属性文件(即 YAML、JSON 或 XML)来存储他们的配置信息。这份属性文件存放在服务器上,通常包含数据库和中间件连接信息,以及驱动应用程序行为的相关元数据。将应用程序分离到一个属性文件中是很简单的,除了将配置文件放在源代码控制下(如果需要这样做的话),并将配置文件部署为应用程序的一部分,大多数开发人员永远不会再对应用程序配置进行实施。

这种方法可能适用于少量的应用程序,但是在处理可能包含数百个微服务的基于云的应用程序,其中每个微服务可能会运行多个服务实例时,它会迅速崩溃。

配置管理突然间变成一件重大的事情,因为在基于云的环境中,应用程序和运维团队必须与配置文件的“鼠巢”进行斗争。基于云的微服务开发强调以下几点。

  1. 应用程序的配置与正在部署的实际代码完全分离。

  2. 构建服务器、应用程序以及一个不可变的镜像,它们在各环境中进行提升时永远不会发生变化。

  3. 在服务器启动时通过环境变量注入应用程序配置信息,或者在微服务启动时通过集中式存储库读取应用程序配置信息。

下面将介绍在基于云的微服务应用程序中管理应用程序配置数据所需的核心原则和模式。

管理配置(和复杂性)

对于在云中运行的微服务,管理应用程序配置是至关重要的,因为微服务实例需要以最少的人为干预快速启动。每当人们需要手动配置或接触服务以实现部署时,都有可能出现配置漂移、意外中断以及应用程序响应可伸缩性挑战出现延迟的情况。

通过建立要遵循的 4 条原则,我们来开始有关应用程序配置管理的讨论。

  1. 分离 —— 我们希望将服务配置信息与服务的实际物理部署完全分开。应用程序配置不应与服务实例一起部署。相反,配置信息应该作为环境变量传递给正在启动的服务,或者在服务启动时从集中式存储库中读取。

  2. 抽象 —— 将访问配置数据的功能抽象到一个服务接口中。应用程序使用基于 REST 的 JSON 服务来检索配置数据,而不是编写直接访问服务存储库的代码(也就是从文件或使用 JDBC 从数据库读取数据)。

  3. 集中 —— 因为基于云的应用程序可能会有数百个服务,所以最小化用于保存配置信息的不同存储库的数量至关重要。将应用程序配置集中在尽可能少的存储库中。

  4. 稳定 —— 因为应用程序的配置信息与部署的服务完全隔离并集中存放,所以不管采用何种方案实现,至关重要的一点就是保证其高可用和冗余。

要记住一个关键点,将配置信息与实际代码分开之后,开发人员将创建一个需要进行管理和版本控制的外部依赖项。为什么应用程序配置数据需要跟踪和版本控制,因为管理不当的应用程序配置很容易滋生难以检测的 bug 和计划外的中断。

配置管理架构

上一章中可以看出,微服务配置管理的加载发生在微服务的引导阶段。下图展示了微服务生命周期。

(应用程序配置数据在服务引导阶段被读取。图片来源:Spring 微服务实战)

我们先来看一下之前提到的 4 条原则(分离、抽象、集中和稳定),看看这 4 条原则在服务引导时是如何应用的。下图更详细地探讨了引导过程,并展示了配置服务在此步骤中扮演的关键角色。

(配置管理概念架构。图片来源:Spring 微服务实战)

在上图中,发生了以下几件事情。

  1. 当一个微服务实例出现时,它将调用一个服务端点来读取其所在环境的特定配置信息。配置管理的连接信息(连接凭据、服务端点等)将在微服务启动时被传递给微服务。

  2. 实际的配置信息驻留在存储库中。基于配置存储库的实现,可以选择使用不同的实现来保存配置数据。配置存储库的实现选择可以包括源代码控制下的文件、关系数据库或键值数据存储。

  3. 应用程序配置数据的实际管理与应用程序的部署方式无关。配置管理的更改通常通过构建和部署管道来处理,其中配置的更改可以通过版本信息进行标记,并通过不同的环境进行部署。

  4. 进行配置管理更改时,必须通知使用该应用程序配置数据的服务,并刷新应用程序数据的副本。

服务发现

在任何分布式架构中,都需要找到机器所在的物理地址。这个概念自分布式计算开始出现就已经存在,并且被正式称为服务发现。服务发现可以非常简单,只需要维护一个属性文件,这个属性文件包含应用程序使用的所有远程服务的地址,也可以像通用描述、发现与集成服务(Universal Description, Discovery, and Integration,UUDI)存储库一样正式(和复杂)。

服务发现对于微服务和基于云的应用程序至关重要,主要原因有两个。

  1. 可以快速地对在环境中运行的服务实例数量进行水平伸缩。

    通过服务发现,服务消费者能够将服务的物理位置抽象出来。由于服务消费者不知道实际服务实例的物理位置,因此可以从可用服务池中添加或移除服务实例。

  2. 有助于提高应用程序的弹性。

    当微服务实例变得不健康或不可用时,大多数服务发现引擎将从内部可用服务列表中移除该实例。由于服务发现引擎会在路由服务时绕过不可用服务,因此能够使不可用服务造成的损害最小。

我们已经了解了服务发现的好处,但是它有什么大不了的呢?难道我们就不能使用诸如域名服务(Domain Name Service,DNS)或负载均衡器等可靠的方法来帮助实现服务发现吗?

下面就来讨论一下,为什么这些方法不适用于基于微服务的应用程序,特别是在云中运行的应用程序。

我的服务在哪里

每当应用程序调用分布在多个服务器上的资源时,这个应用程序就需要定位这些资源的物理位置。在非云的世界中,这种服务位置解析通常由 DNS 和网络负载均衡器的组合来解决。

(使用 DNS 和负载均衡器的传统服务位置解析模型。图片来源:Spring 微服务实战)

应用程序需要调用位于组织其他部分的服务。它尝试通过使用通用 DNS 名称以及唯一表示需要调用的服务的路径来调用该服务。DNS 名称会被解析到一个商用负载均衡器(如流行的 F5 负载均衡器)或开源负载均衡器(如 HAProxy)。

负载均衡器在接收到来自服务消费者的请求时,会根据服务消费者尝试访问的路径,在路由表中定位物理地址条目。此路由表条目包含托管该服务的一个或多个服务器的列表。接着,负载均衡器选择列表中的一个服务器,并将请求转发到该服务器上。

服务的每个实例被部署到一个或多个应用服务器。这些应用程序服务器的数量往往是静态的(例如,托管服务的应用程序服务器的数量并没有增加和减少)和持久的(例如,如果运行应用程序的服务器崩溃,它将恢复到崩溃时的状态,并将具有与之前相同的 IP 和配置)。

为了实现高可用性,辅助负载均衡器会处于空闲状态,并 ping 主负载均衡器以查看它是否处于存活(alive)状态。如果主负载均衡器未处于存活状态,那么辅助负载均衡器将变为存活状态,接管主负载均衡器的 IP 地址并开始提供请求。

这种模型适用于在企业数据中心内部运行的应用程序,以及在一组静态服务器上运行少量服务的情况,但对基于云的微服务应用程序来说,这种模型并不适用。原因有以下几个。

  • 单点故障 —— 虽然负载均衡器可以实现高可用,但这是整个基础设施的单点故障。如果负载均衡器出现故障,那么依赖它的每个应用程序都会出现故障。尽管可以使负载平衡器高度可用,但负载均衡器往往是应用程序基础设施中的集中式阻塞点。

  • 有限的水平可伸缩性 —— 在服务集中到单个负载均衡器集群的情况下,跨多个服务器水平伸缩负载均衡基础设施的能力有限。许多商业负载均衡器受两件事情的限制:冗余模型和许可证成本。第一,大多数商业负载均衡器使用热插拔模型实现冗余,因此只能使用单个服务器来处理负载,而辅助负载均衡器仅在主负载均衡器中断的情况下,才能进行故障切换。这种架构本质上受到硬件的限制。第二,商业负载均衡器具有有限数量的许可证,它面向固定容量模型而不是更可变的模型。

  • 静态管理 —— 大多数传统的负载均衡器不是为快速注册和注销服务设计的。它们使用集中式数据库来存储规则的路由,添加新路由的唯一方法通常是通过供应商的专有 API(Application Programming Interface,应用程序编程接口)来进行添加。

  • 复杂 —— 由于负载均衡器充当服务的代理,它必须将服务消费者的请求映射到物理服务。这个翻译层通常会为服务基础设施增加一层复杂度,因为开发人员必须手动定义和部署服务的映射规则。在传统的负载均衡器方案中,新服务实例的注册是手动完成的,而不是在新服务实例启动时完成的。

当然,负载均衡器在企业级环境中工作良好,在这种环境中,大多数应用程序的大小和规模可以通过集中式网络基础设施来处理。此外,负载均衡器仍然可以在集中化 SSL 终端和管理服务端口安全性方面发挥作用。负载均衡器可以锁定位于它后面的所有服务器的入站(入口)端口和出站(出口)端口访问。在需要满足行业标准的认证要求,如 PCI(Payment Card Industry,支付卡行业)合规时,这种最小网络访问概念经常是关键组成部分。

然而,在需要处理大量事务和冗余的云环境中,集中的网络基础设施并不能最终发挥作用,因为它不能有效地伸缩,并且成本效益也不高。现在来看一下,如何为基于云的应用程序实现一个健壮的服务发现机制。

云中的服务发现

基于云的微服务环境的解决方案是使用服务发现机制,这一机制具有以下特点。

  • 高可用 —— 服务发现需要能够支持“热”集群环境,在服务发现集群中可以跨多个节点共享服务查找。如果一个节点变得不可用,集群中的其他节点应该能够接管工作。

  • 点对点 —— 服务发现集群中的每个节点共享服务实例的状态。

  • 负载均衡 —— 服务发现需要在所有服务实例之间动态地对请求进行负载均衡,以确保服务调用分布在由它管理的所有服务实例上。在许多方面,服务发现取代了许多早期 Web 应用程序实现中使用的更静态的、手动管理的负载均衡器。

  • 有弹性 —— 服务发现的客户端应该在本地“缓存”服务信息。本地缓存允许服务发现功能逐步降级,这样,如果服务发现服务变得不可用,应用程序仍然可以基于本地缓存中维护的信息来运行和定位服务。

  • 容错 —— 服务发现需要检测出服务实例什么时候是不健康的,并从可以接收客户端请求的可用服务列表中移除该实例。服务发现应该在没有人为干预的情况下,对这些故障进行检测,并采取行动。

服务发现架构

为了开始讨论服务发现架构,需要先了解 4 个概念。这些一般概念在所有服务发现实现中是共通的。

  • 服务注册 —— 服务如何使用服务发现代理进行注册?

  • 服务地址的客户端查找 —— 服务客户端查找服务信息的方法是什么?

  • 信息共享 —— 如何跨节点共享服务信息?

  • 健康监测 —— 服务如何将它的健康信息传回给服务发现代理?

下图展示了这 4 个概念的流程,以及在服务发现模式实现中通常发生的情况。

(随着服务实例的添加与删除,它们将更新服务发现代理,并可用于处理用户请求。图片来源:Spring 微服务实战)

服务在向服务发现服务进行注册之后,这个服务就可以被需要使用这项服务功能的应用程序或其他服务使用。客户端可以使用不同的模型来“发现”服务。在每次调用服务时,客户端可以只依赖于服务发现引擎来解析服务位置。使用这种方法,每次调用注册的微服务实例时,服务发现引擎就会被调用。但是,这种方法很脆弱,因为服务客户端完全依赖于服务发现引擎来查找和调用服务。

一种更健壮的方法是使用所谓的客户端负载均衡。

(客户端负载均衡缓存服务的位置,以便服务客户端不必在每次调用时联系服务发现。图片来源:Spring 微服务实战)

在这个模型中,当服务消费者需要调用一个服务时:

  1. 它将联系服务发现服务,获取它请求的所有服务实例,然后在服务消费者的机器上本地缓存数据。

  2. 每当客户端需要调用该服务时,服务消费者将从缓存中查找该服务的位置信息。通常,客户端缓存将使用简单的负载均衡算法,如“轮询”负载均衡算法,以确保服务调用分布在多个服务实例之间。

  3. 然后,客户端将定期与服务发现服务进行联系,并刷新服务实例的缓存。客户端缓存最终是一致的,但是始终存在这样的风险:在客户端联系服务发现实例以进行刷新和调用时,调用可能会被定向到不健康的服务实例上。

如果在调用服务的过程中,服务调用失败,那么本地的服务发现缓存失效,服务发现客户端将尝试从服务发现代理刷新数据。

客户端弹性模式

所有的系统,特别是分布式系统,都会遇到故障。如何构建应用程序来应对这种故障,是每个软件开发人员工作的关键部分。然而,当涉及构建弹性系统时,大多数软件工程师只考虑到基础设施或关键服务彻底发生故障。他们专注于在应用程序的每一层构建冗余,使用诸如集群关键服务器、服务间的负载均衡以及将基础设施分离到多个位置的技术。

尽管这些方法考虑到系统组件的彻底(通常是惊人的)损失,但它们只解决了构建弹性系统的一小部分问题。当服务崩溃时,很容易检测到该服务已经不在了,因此应用程序可以绕过它。然而,当服务运行缓慢时,检测到这个服务性能不佳并绕过它是非常困难的,这是因为以下几个原因。

  1. 服务的降级可能以间歇性问题开始,并形成不可逆转的势头 —— 降级可能只发生在很小的爆发中。故障的第一个迹象可能是一小部分用户抱怨某个问题,直到突然间应用程序容器耗尽了线程池并彻底崩溃。

  2. 对远程服务的调用通常是同步的,并且不会缩短长时间运行的调用 —— 服务的调用者没有超时的概念来阻止服务调用的永久挂起。应用程序开发人员调用该服务来执行操作并等待服务返回。

  3. 应用程序经常被设计为处理远程资源的彻底故障,而不是部分降级 —— 通常,只要服务没有彻底失败,应用程序将继续调用这个服务,并且不会采取快速失败措施。该应用程序将继续调用表现不佳的服务。调用的应用程序或服务可能会优雅地降级,但更有可能因为资源耗尽而崩溃。资源耗尽是指有限的资源(如线程池或数据库连接)消耗殆尽,而调用客户端必须等待该资源变为可用。

性能不佳的远程服务所导致的潜在问题是,它们不仅难以检测,还会触发连锁效应,从而影响整个应用程序生态系统。如果没有适当的保护措施,一个性能不佳的服务可以迅速拖垮多个应用程序。基于云、基于微服务的应用程序特别容易受到这些类型的中断的影响,因为这些应用程序由大量细粒度的分布式服务组成,这些服务在完成用户的事务时涉及不同的基础设施。

什么是客户端弹性模式

客户端弹性软件模式的重点是,在远程服务发生错误或表现不佳时保护远程资源(另一个微服务调用或数据库查询)的客户端免于崩溃。这些模式的目标是让客户端“快速失败”,而不消耗诸如数据库连接和线程池之类的宝贵资源,并且可以防止远程服务的问题向客户端的消费者进行“上游”传播。

有 4 种客户端弹性模式,它们分别是:

  1. 客户端负载均衡(client load balance)模式;

  2. 断路器(circuit breaker)模式;

  3. 后备(fallback)模式;

  4. 舱壁(bulkhead)模式。

下图展示了如何将这些模式用于微服务消费者和微服务之间。

(这 4 个客户端弹性模式充当服务消费者和服务之间的保护缓冲区。图片来源:Spring 微服务实战)

这些模式是在调用远程资源的客户端中实现的,它们的实现在逻辑上位于消费远程资源的客户端和资源本身之间。

客户端负载均衡模式

客户端负载均衡涉及让客户端从服务发现代理(如 Netflix Eureka)查找服务的所有实例,然后缓存服务实例的物理位置。每当服务消费者需要调用该服务实例时,客户端负载均衡器将从它维护的服务位置池返回一个位置。

因为客户端负载均衡器位于服务客户端和服务消费者之间,所以负载均衡器可以检测服务实例是否抛出错误或表现不佳。如果客户端负载均衡器检测到问题,它可以从可用服务位置池中移除该服务实例,并防止将来的服务调用访问该服务实例。

断路器模式

断路器模式是模仿电路断路器的客户端弹性模式。在电气系统中,断路器将检测是否有过多电流流过电线。如果断路器检测到问题,它将断开与电气系统的其余部分的连接,并保护下游部件不被烧毁。

有了软件断路器,当远程服务被调用时,断路器将监视这个调用。如果调用时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如果对某一个远程资源的调用失败次数足够多,那么断路器实现就会出现并采取快速失败,阻止将来调用失败的远程资源。

后备模式

有了后备模式,当远程服务调用失败时,服务消费者将执行替代代码路径,并尝试通过其他方式执行操作,而不是生成一个异常。这通常涉及从另一数据源查找数据或将用户的请求进行排队以供将来处理。用户的调用结果不会显示为提示问题的异常,但用户可能会被告知,他们的请求要在晚些时候被满足。

例如,假设我们有一个电子商务网站,它可以监控用户的行为,并尝试向用户推荐其他可以购买的产品。通常来说,可以调用微服务来对用户过去的行为进行分析,并返回针对特定用户的推荐列表。但是,如果这个偏好服务失败,那么后备策略可能是检索一个更通用的偏好列表,该列表基于所有用户的购买记录分析得出,并且更为普遍。这些更通用的偏好列表数据可能来自完全不同的服务和数据源。

舱壁模式

舱壁模式是建立在造船的概念基础上的。采用舱壁设计,一艘船被划分为完全隔离和防水的隔间,这称为舱壁。即使船的船体被击穿,由于船被划分为水密舱(舱壁),舱壁会将水限制在被击穿的船的区域内,防止整艘船灌满水并沉没。

同样的概念可以应用于必须与多个远程资源交互的服务。通过使用舱壁模式,可以把远程资源的调用分到线程池中,并降低一个缓慢的远程资源调用拖垮整个应用程序的风险。线程池充当服务的“舱壁”。每个远程资源都是隔离的,并分配给线程池。如果一个服务响应缓慢,那么这种服务调用的线程池就会饱和并停止处理请求,而对其他服务的服务调用则不会变得饱和,因为它们被分配给了其他线程池。

为什么客户端弹性很重要

接下来我们来看看一个常见场景,看看为什么客户端弹性模式(如断路器模式)对于实现基于服务的架构至关重要,尤其是在云中运行的微服务架构。

下图展示了一个典型的场景,它涉及使用远程资源,如数据库和远程服务。

(应用程序是相互关联依赖的图形结构。如果不管理这些依赖之间的远程调用,那么一个表现不佳的远程资源可能会拖垮图中的所有服务。图片来源:Spring 微服务实战)

在图中所示的场景中,3 个应用程序分别以这样或那样的方式与 3 个不同的服务进行通信。应用程序 A 和应用程序 B 与服务 A 直接通信。服务 A 从数据库检索数据,并调用服务 B 来为它工作。服务 B 从一个完全不同的数据库平台中检索数据,并从第三方云服务提供商调用另一个服务——服务 C,该服务严重依赖于内部网络区域存储(Network Area Storage,NAS)设备,以将数据写入共享文件系统。此外,应用程序 C 直接调用服务 C。

在某个周末,网络管理员对 NAS 配置做了一个他认为是很小的调整。这个调整似乎可以正常工作,但是在周一早上,所有对特定磁盘子系统的读取开始变得非常慢。

编写服务 B 的开发人员从来没有预料到会发生调用服务 C 缓慢的事情。他们所编写的代码中,在同一个事务中写入数据库和从服务 C 读取数据。当服务 C 开始运行缓慢时,不仅请求服务 C 的线程池开始堵塞,服务容器的连接池中的数据库连接也会耗尽,因为这些连接保持打开状态,这一切的原因是对服务 C 的调用从来没有完成。

最后,服务 A 耗尽资源,因为它调用了服务 B,而服务 B 的运行缓慢则是因为它调用了服务 C。最后,所有 3 个应用程序都停止响应了,因为它们在等待请求完成中耗尽了资源。

如果在调用分布式资源(无论是调用数据库还是调用服务)的每一个点上都实现了断路器模式,则可以避免这种情况。在上图中,如果使用断路器实现了对服务 C 的调用,那么当服务 C 开始表现不佳时,对服务 C 的特定调用的断路器就会跳闸,并且快速失败,而不会消耗掉一个线程。如果服务 B 有多个端点,则只有与服务 C 特定调用交互的端点才会受到影响。服务 B 的其余功能仍然是完整的,可以满足用户的要求。

断路器在应用程序和远程服务之间充当中间人。在上述场景中,断路器实现可以保护应用程序 A、应用程序 B 和应用程序 C 免于完全崩溃。

在下图中,服务 B(客户端)永远不会直接调用服务 C。相反,在进行调用时,服务 B 把服务的实际调用委托给断路器,断路器将接管这个调用,并将它包装在独立于原始调用者的线程(通常由线程池管理)中。通过将调用包装在一个线程中,客户端不再直接等待调用完成。相反,断路器会监视线程,如果线程运行时间太长,断路器就可以终止该调用。

(断路器跳闸,让表现不佳的服务调用迅速而优雅地失败。图片来源:Spring 微服务实战)

上图展示了这 3 个场景。第一种场景是愉快路径,断路器将维护一个定时器,如果在定时器的时间用完之前完成对远程服务的调用,那么一切都非常顺利,服务B可以继续工作。在部分降级的场景中,服务 B 将通过断路器调用服务 C。但是,如果这一次服务 C 运行缓慢,在断路器维护的线程上的定时器超时之前无法完成对远程服务的调用,断路器就会切断对远程服务的连接。

然后,服务 B 将从发出的调用中得到一个错误,但是服务 B 不会占用资源(也就是自己的线程池或连接池)来等待服务 C 完成调用。如果对服务 C 的调用被断路器超时中断,断路器将开始跟踪已发生故障的数量。

如果在一定时间内在服务 C 上发生了足够多的错误,那么断路器就会电路“跳闸”,并且在不调用服务 C 的情况下,就判定所有对服务 C 的调用将会失败。

电路跳闸将会导致如下 3 种结果。

  1. 服务 B 现在立即知道服务 C 有问题,而不必等待断路器超时。

  2. 服务 B 现在可以选择要么彻底失败,要么执行替代代码(后备)来采取行动。

  3. 服务 C 将获得一个恢复的机会,因为在断路器跳闸后,服务 B 不会调用它。这使得服务 C 有了喘息的空间,并有助于防止出现服务降级时发生的级联死亡。

最后,断路器会让少量的请求调用直达一个降级的服务,如果这些调用连续多次成功,断路器就会自动复位。

以下是断路器模式为远程调用提供的关键能力。

  1. 快速失败 —— 当远程服务处于降级状态时,应用程序将会快速失败,并防止通常会拖垮整个应用程序的资源耗尽问题的出现。在大多数中断情况下,最好是部分服务关闭而不是完全关闭。

  2. 优雅地失败 —— 通过超时和快速失败,断路器模式使应用程序开发人员有能力优雅地失败,或寻求替代机制来执行用户的意图。例如,如果用户尝试从一个数据源检索数据,并且该数据源正在经历服务降级,那么应用程序开发人员可以尝试从其他地方检索该数据。

  3. 无缝恢复 —— 有了断路器模式作为中介,断路器可以定期检查所请求的资源是否重新上线,并在没有人为干预的情况下重新允许对该资源进行访问。

在大型的基于云的应用程序中运行着数百个服务,这种优雅的恢复能力至关重要,因为它可以显著减少恢复服务所需的时间,并大大减少因疲劳的运维人员或应用工程师直接干预恢复服务(重新启动失败的服务)而造成更严重问题的风险。

微服务网关

在像微服务架构这样的分布式架构中,需要确保跨多个服务调用的关键行为的正常运行,如安全、日志记录和用户跟踪。要实现此功能,开发人员需要在所有服务中始终如一地强制这些特性,而不需要每个开发团队都构建自己的解决方案。虽然可以使用公共库或框架来帮助在单个服务中直接构建这些功能,但这样做会造成 3 个影响。

第一,在构建的每个服务中很难始终实现这些功能。开发人员专注于交付功能,在每日的快速开发工作中,他们很容易忘记实现服务日志记录或跟踪。遗憾的是,对那些在金融服务或医疗保健等严格监管的行业工作的人来说,一致且有文档记录系统中的行为通常是符合政府法规的关键要求。

第二,正确地实现这些功能是一个挑战。对每个正在开发的服务进行诸如微服务安全的建立与配置可能是很痛苦的。将实现横切关注点(cross-cutting concern,如安全问题)的责任推给各个开发团队,大大增加了开发人员没有正确实现或忘记实现这些功能的可能性。

第三,这会在所有服务中创建一个顽固的依赖。开发人员在所有服务中共享的公共框架中构建的功能越多,在通用代码中无需重新编译和重新部署所有服务就能更改或添加功能就越困难。当应用程序中有 6 个微服务时,这似乎不是什么大问题,但当这个应用程序拥有更多的服务时(大概 30 个或更多),这就是一个很大的问题。突然间,共享库中内置的核心功能的升级就变成了一个数月的迁移过程。

为了解决这个问题,需要将这些横切关注点抽象成一个独立且作为应用程序中所有微服务调用的过滤器和路由器的服务。这种横切关注点被称为服务网关(service gatervay)。服务客户端不再直接调用服务。取而代之的是,服务网关作为单个策略执行点(Policy Enforcement Point,PEP),所有调用都通过服务网关进行路由,然后被路由到最终目的地。

什么是服务网关

服务网关充当服务客户端和被调用的服务之间的中介。服务客户端仅与服务网关管理的单个 URL 进行对话。服务网关从服务客户端调用中分离出路径,并确定服务客户端正在尝试调用哪个服务。服务网关像交通警察一样指挥交通,将用户引导到目标微服务和相应的实例。服务网关充当应用程序内所有微服务调用的入站流量的守门人。有了服务网关,服务客户端永远不会直接调用单个服务的 URL,而是将所有调用都放到服务网关上。

由于服务网关位于客户端到各个服务的所有调用之间,因此它还充当服务调用的中央策略执行点(Policy Enforcement Point,PEP)。使用集中式 PEP 意味着横切服务关注点可以在一个地方实现,而无须各个开发团队来实现这些关注点。举例来说,可以在服务网关中实现的横切关注点包括以下几个。

  • 静态路由 —— 服务网关将所有的服务调用放置在单个 URL 和 API 路由的后面。这简化了开发,因为开发人员只需要知道所有服务的一个服务端点就可以了。

  • 动态路由 —— 服务网关可以检查传入的服务请求,根据来自传入请求的数据和服务调用者的身份执行智能路由。例如,可能会将参与测试版程序的客户的所有调用路由到特定服务集群的服务,这些服务运行的是不同版本的代码,而不是其他人使用的非测试版程序的代码。

  • 验证和授权 —— 由于所有服务调用都经过服务网关进行路由,所以服务网关是检查服务调用者是否已经进行了验证并被授权进行服务调用的自然场所。

  • 度量数据收集和日志记录 —— 当服务调用通过服务网关时,可以使用服务网关来收集数据和日志信息,还可以使用服务网关确保在用户请求上提供关键信息以确保日志统一。这并不意味着不应该从单个服务中收集度量数据,而是通过服务网关可以集中收集许多基本度量数据,如服务调用次数和服务响应时间。

难道服务网关不是单点故障和潜在瓶颈吗?

在构建服务网关实现时,要牢记以下几点。

在单独的服务组前面,负载均衡器仍然很有用。在这种情况下,将负载均衡器放到多个服务网关实例前面是一个恰当的设计,它确保服务网关实现可以伸缩。将负载均衡器置于所有服务实例的前面并不是一个好主意,因为它会成为瓶颈。

要保持为服务网关编写的代码是无状态的。不要在内存中为服务网关存储任何信息。如果不小心,就有可能限制网关的可伸缩性,导致不得不确保数据在所有服务网关实例中被复制。

要保持为服务网关编写的代码是轻量的。服务网关是服务调用的“阻塞点”,具有多个数据库调用的复杂代码可能是服务网关中难以追踪的性能问题的根源。

Released under the MIT License.