如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

在本章中,我们会站在更高的角度来思考由微服务组成的整个应用的设计和架构。我们无法代替读者深入了解开发者们自己的应用系统的业务领域,但是我们可以告诉读者的是,深入了解业务领域能够帮助读者构建出足够灵活的系统,这样的系统能够随着时间的推移不断发展和演进。

开发者会了解到,通常如何将微服务应用设计为四层结构——平台层、服务层、边界层和客户端层。开发者还会学习到这四层的具体内容,以及它们是如何组合起来交付面向客户的应用程序的。我们会重点介绍事件中枢(event backbone)在开发大规模微服务应用中的作用,还会讨论一些构建应用边界的不同模式,如API网关。最后,我们会介绍为微服务应用构建用户界面的最新趋势,如微前端和前端组合。

3.1 整体架构

软件设计师希望所开发出来的软件是易于修改的。许多外部力量都会对开发者的软件施加影响:新增需求、系统缺陷、市场需要、新客户、业务增长情况等。理想情况下,工程师可以自信满满地以稳定的步调来响应这些压力。如果想要做到这一点,开发方式就应该减少摩擦并将风险降至最低。

随着时间的不断推移,系统也在不断演进,工程团队想要将开发道路上的所有拦路石清理掉。有的希望能够无缝地快速替换掉系统中过时的组件,有的希望各个团队能够完全地实现自治,并各自负责系统的不同模块,有的则希望这些团队之间不需要不停地同步各种信息而且相互没有阻碍。为此,我们需要考虑一下架构设计,也就是构建应用的规划。

3.1.1 从单体应用到微服务

在单体应用中,主要交付的就是一个应用程序。这个应用程序可以被水平地分成几个不同的技术层。在典型的三层架构的应用中,它们分别是数据层业务逻辑层展示层(图3.1)。应用又会被垂直地分成不同的业务领域。MVC模式以及Rails和Django等框架都体现了三层模型。每一层都为其上一层提供服务:数据层提供持久化状态,业务逻辑层执行有效操作,而展示层则将结果展示给终端用户。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.1 典型的单体应用三层架构

单个微服务和单体应用是很相似的:微服务会存储数据、执行一些业务逻辑操作并通过API将数据和结果返回给消费者。每个微服务都具备一项业务能力或者技术能力,并且会通过和其他微服务进行交互来执行某些任务。单个服务的抽象架构如图3.2所示。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.2 单个微服务的抽象架构

 注意

在第4章中,我们将详细讨论微服务的范围划分-如何定义微服务的边界和职责。

在单体应用中,架构限定在整个应用本身的边界内;而在微服务应用中,开发者是在对从规模到范围都在不断演变的内容进行规划。用城市作类比的话,开发一个单体应用就像建造一幢摩天大厦,而构建微服务应用则像开发一个社区:开发者需要建造基础设施(自来水管道、道路交通、电路线缆),还要规划未来的发展(小型企业区vs. 住宅区)。

这个类比强调不仅要考虑组件自身,还要考虑这些组件之间的连接方式、放置位置以及如何并行地构建它们。开发者希望自己的方案能促使应用沿着良好的方向发展,而非强行规定或强迫应用采取某种结构。

最重要的是,微服务并不是孤立地运行的。每个微服务都会和其他的微服务一起共存于一个环境中,而我们就在这个环境中开发、部署和运行微服务。应用架构应该包含整个环境。

3.1.2 架构师的角色

软件架构师的职责是什么呢?许多公司都会招聘软件架构师,即便这个职位的实际工作效果和对它的要求出入很大。

微服务应用使得快速修改成为可能:因为团队在不断地开发新的服务、停用现有服务或者重构现有功能,所以应用也会随着时间慢慢地演进。架构师或者技术负责人的工作就是要确保系统能够不断演进,而不是采用了固化的设计方案。如果微服务应用是一座城市的话,开发者就是市*的规划师。

架构师的职责是确保应用的技术基础能够支持快节奏的开发以及频繁的变化。架构师应该具备纵观全局的能力,确保应用的全局需求都能得到满足,并进一步指导应用的演进发展。

(1)应用和组织远大的战略目标是一致的。

(2)团队共同承担一套通用的技术价值观和期望。

(3)跨领域的内容——诸如可观察性、部署、服务间通信——应该满足不同团队的需要。

(4)面对变化,整个应用是灵活可扩展的。

为了实现这些目标,架构师应该通过两种方式来指导开发:第一,准则——为了实现更高一层的技术目标或者组织目标,团队要遵循的一套指南;第二,概念模型——系统内部相互联系以及应用层面的模式的抽象模型。

3.1.3 架构准则

准则是指团队为了实现更高的目标而要遵循的一套指南(或规则)。准则用于指导团队如何实践。这一模型如图3.3所示。例如,如果某产品的目标是销售给那些对隐私和安全问题特别敏感的企业,那么开发者就要制定这些准则。

(1)开发实践必须符合那些公认的外部标准(如ISO 27001)。

(2)时刻牢记,所有数据必须是可转移的,并且在存储数据的时候要有效期限制。

(3)必须要能够在应用中清晰地跟踪和回溯追查个人信息。

准则是灵活的,它们可以并且应该随着业务优先级的变化以及应用的技术演进而变化。例如,早期的开发过程会将验证产品和市场需求的匹配度作为更高优先级的工作,而一个更加成熟的应用可能需要更专注于性能和可扩展性。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.3 基于技术准则的架构方法

3.1.4 微服务应用的4层架构

架构应该体现出清晰的高层概念模型。在对一个应用的技术结构进行分析时,模型是一个非常有用的工具。如图3.1中的3层模型,这样的多层模型是一种应用程序结构的常见方案,能够反映整个系统不同层次的抽象和职责。

在本章的其余部分中,我们会探讨微服务的4层模型。

(1)平台层——微服务平台提供了工具、基础架构和一些高级的基本部件,以支持微服务的快速开发、运行和部署。一个成熟的平台层会让技术人员把重心放在功能开发而非一些底层的工作上。

(2)服务层——在这一层,开发的各个服务会借助下层的平台层的支持来相互作用,以提供业务和技术功能。

(3)边界层——客户端会通过定义好的边界和应用进行交互。这个边界会暴露底层的各个功能,以满足外部消费者的需求。

(4)客户端层——与微服务后端交互的客户端应用,如网站和移动应用。

上述架构层次如图3.4所示。不管底层使用了什么技术方案,开发者应该都能够将它应用到所有微服务应用中。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.4 微服务应用架构的四层模型

每一层都是建立在下一层次的功能之上的,比如,每个服务都会利用下层的微服务平台提供的部署流水线、基础设施和通信机制。要设计良好的微服务应用,需要在每个层级上都进行大量的投入并精心设计。

很棒!开发者现在有了一个可用的模型。在后续的5节中,我们会一一介绍这一架构模型的4个层次,并讨论它们对构建可持续的、灵活的、可演进的微服务应用的贡献。

3.2 微服务平台

微服务并不是独立存在的。微服务需要由如下基础设施提供支持。

(1)服务运行的部署目标,包括基础设施的基本元件,如负载均衡器和虚拟机。

(2)日志聚合和监控聚合用于观测服务运行情况。

(3)一致且可重复的部署流水线,用于测试和发布新服务或者新版本。

(4)支持安全运行,如网络控制、涉密信息管理和应用加固。

(5)通信通道和服务发现方案,用于支持服务间交互。

这些功能及其与服务层的关系如图3.5所示。如果把每个微服务看作一栋住宅,那么平台层提供了道路、自来水、电线和电话线。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.5 微服务平台层的功能

一个具有鲁棒性的平台层既能够降低整体的实现成本,又能够提升整体的可稳定性,甚至能提高服务的开发速度。如果没有平台层,产品开发者就需要重复编写大量的底层的基础代码,无暇交付新的功能和业务价值。一般的开发者不需要也不应该对应用的每一层的复杂性都了然于胸。基本上,一个半独立的专业团队就可以开发出一套平台层,能够满足那些在服务层工作的团队的需求。

映射运行时平台

微服务平台有助于提升开发者的自信,让开发者确信团队编写的服务能够支持生产环境的流量压力,并且这些服务是可恢复的、透明的和可扩展的。

图3.6所示的是某个微服务的运行时平台。运行时平台(或者部署目标)——比如,像AWS的云环境或者像Heroku这样的PaaS平台——提供了运行多个服务实例以及将请求路由给这些实例的基础元件。除此之外,它还提供了相应的机制来为服务实例提供配置信息——机密信息和特定环境的变量。

开发者在这一基础之上来开发微服务平台的其他部分。观测工具会收集服务以及底层基础设施的数据并进行修正。部署流水线会管理这一应用栈的升级或回滚。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.6 在标准的云环境中运行微服务所需的部署配置

3.3 服务层

服务层,正如其名称所描述的——它就是服务所存在的地方。在这一层,服务通过交互完成有用的功能——这依赖于底层平台对可靠的运行和通信方案的抽象,它还会通过边界层将功能暴露给应用的客户端。我们同样还会考虑将服务内部的组件(如数据存储)也作为服务层的一部分。

业务特点不同,相应服务层的结构也会差异很大。在本节中,我们会讨论一些常见的模式:业务和技术功能、聚合和多元服务以及关键路径和非关键路径的服务。

3.3.1 功能

开发者所开发的服务实现的是不同的功能。

(1)业务能力是组织为了创造价值和实现业务目标所做的工作。划到业务功能的微服务直接体现的是业务目标。

(2)技术能力通过实现共享的技术功能来支持其他服务。

图3.7比较了两种不同类型的功能。SimpleBank的order服务公开了管理下单的功能——这是一个业务功能;而market服务是一个技术功能,它提供了和第三方系统通信的网关供其他服务(比如,market服务公开了市场信息数据或者贸易结算功能)使用。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.7 实现业务功能和技术功能的微服务

 注意

我们会在下一章介绍何时使用业务功能和技术功能,以及如何将它们映射到不同的服务上。

3.3.2 聚合与多元服务

在微服务应用的早期阶段,多个服务可能是扁平化的,每个服务的职责都是处于相似的层次的,比如,第2章中的order服务、fee服务、transaction服务和account服务——都处于大致相当的抽象水平。

随着应用的发展,开发者会面临服务增长的两大压力:从多个服务聚合数据来为客户端的请求提供非规范化的数据(如同时返回费用和订单信息);利用底层的功能来提供专门的业务逻辑(如发布某种特定类型的订单)。

随着时间的推移,这两种压力会导致服务出现层级结构的分化。靠近系统边界的服务会和某些服务交互以聚合它们的输出——我们将这种服务称为聚合器(aggregator)(图3.8),除此之外,还有些专门的服务会作为协调器(coordinator)来协调下层多个服务的工作。

在出现新的数据需求或者功能需求时,开发者要决定是开发一个新的服务还是修改已有的服务,这是开发者所面临的重大挑战。创建一个新的服务会增加整体的复杂度并且可能会导致服务间的紧耦合,但是将功能加到现有的服务又可能会导致内聚性降低以及难以替换。这是违背了微服务的基本原则的。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.8 聚合器服务通过将底层服务的数据进行关联来实现查询服务,协调器服务会向下游服务发出各种命令来编配它们的行动

3.3.3 关键路径和非关键路径

随着系统的不断演进,有一些功能对顾客的需求和业务的成功经营来说越来越重要。比如,在SimpleBank公司的下单流程中,order服务就处于关键路径。一旦这个服务运行出错,系统就不能执行客户的订单。对应地,其他服务的重要性就弱一些。即便客户的资料服务不可用,它也不大会影响开发者提供的那些关键的、会带来收入的部分服务。SimpleBank公司的一些路径示例如图3.9所示。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.9 服务链对外提供功能,许多服务会参与到多个的路径中

这是一把双刃剑。关键路径上的服务越多,系统出现故障的可能性就越高。因为不可能哪个服务是100%可靠的,一个服务累积的可靠性是它所依赖的那些服务的可靠性的乘积。

但是微服务使得我们可以清楚地确定这些路径,然后对它们单独处理,投入更多的精力来尽可能提高这些路径的可恢复性和可扩展性,对于不那么重要的系统领域,则可以少付出一些精力。

3.4 通信

通信是微服务应用的一个基本要素。微服务相互通信才能完成有用的工作。微服务会向其他微服务发送命令通知和请求操作,开发者选择的通信方式也决定着所开发的应用的结构。

 提示

在微服务系统中,网络通信也是一个很主要的不可靠因素。在第6章中,我们会讨论一些提升服务间通信的可靠性的技术。

在架构上,通信不是独立的一层,但是我们之所以将它拎出来单独作为一节来介绍,是因为网络使得平台层和服务层之间的边界变得模糊。有些组件,比如通信代理,属于平台层。但是负责组装和发送消息的确实服务自己。开发者希望所开发的端点实现智能化、但是管道傻瓜化。

在本节中,我们会讨论一些常用的服务通信模式及其对微服务应用的灵活性以及演化过程的影响。大部分成熟的微服务应用都会同时掺杂有同步和异步这两种交互方式。

3.4.1 何时使用同步消息

同步消息通常是首先会想到的设计方案。它们非常适合于那些在执行新的操作前需要获取前一个操作的数据结果或者确认前一个操作成功还是失败的场景。

一种请求-响应的同步消息模式如图3.10所示。左边的服务会构造要发给接收方的对应的消息,然后采用一种传输机制(如HTTP)来将消息发送出去。目标服务会收到这条消息并相应地进行处理。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.10 服务间同步的请求-响应生命周期

1.选择传输方式

无论是选择RESTful HTTP、RPC库还是选择其他传输方式,都会影响服务的设计方案。每种传输方式都具有不同的特性,这些特性包括时延、语言支持情况和规范性。比如gRPC支持生成使用Protobuf的客户端/服务器端的API契约,而HTTP与消息的上下文无关。在应用中,只使用一种同步传输方式能产生规模效益,这样也更易于通过一些监控和工具来排查问题。

微服务的关注点分离也同样重要。应该将传输方案的选择与服务的业务逻辑拆分开,服务不需要了解HTTP状态码或者gRPC的响应流。这么做有助于在未来应用演进时替换为另一种不同的机制。

2.缺点

同步消息的缺点如下。

(1)服务之间耦合更紧,因为服务必须知道协作者的存在。

(2)不能很好地支持广播模型以及发布-订阅模型。这限制了执行并行工作的能力。

(3)在等待响应的时候,代码执行是被阻塞的。在基于线程或者进程的服务模型中,这可能会耗尽资源并触发连锁故障。

(4)过度使用同步消息会导致出现很深的依赖链,而这又会增加调用路径整体的脆弱性。

3.4.2 何时使用异步消息

异步消息更加灵活。开发者可以通过事件通知的方式来扩展系统处理新的需求。因为服务不再需要了解下游的消费者。新服务可以直接消费已有的事件,而不需要对已有的服务进行修改。

提示

事件(event)表示了事后(post-hoc)的状态变化,例如,OrderCreated、OrderPlaced和OrderCanceled都是SimpleBank公司的order服务可以发出的事件。

这种方式下,应用演化更加平滑,服务间的耦合更低。但是付出的代价是:异步交互更加难以理解,因为整个系统行为不再是那种显式的线性顺序。系统行为变得更加危险——服务间的交互变得不可预测——需要在监控上增加投入来充分地跟踪所发生的情况。

 注意

有不同类型的事件持久化和事件查询的方式,如事件溯源(event sourcing)和命令查询的责任分离Command Query Responsibility Segregation,CQRS)。它们不是微服务的先决条件,但是与微服务方案会有协同效应。我们将在第5章中进行具体介绍。

异步消息通常需要一个通信代理(communication broker),这是一个独立的系统组件,负责接收事件并把它们分发给对应的消费者。有时候也叫作事件中枢(event backbone),这也表明了这个组件对整个应用是多么重要。常用作代理的工具包括Kafka、RabbitMQ和Redis。这些工具的意义并不相同:Kafka专门研究的是海量的、可重复的事件存储,而RabbitMQ提供了一套高度抽象的消息中间件(基于AMQP协议)。

3.4.3 异步通信模式

我们来看两种常见的基于事件的模式:任务队列和发布-订阅。在对微服务进行架构设计时,开发者会经常遇到这两种模式——大部分更高级的交互模式是基于这两种基本模式实现的。

1.作业队列

在这种模式中,工作者(worker)从队列种接收任务并执行它(图3.11)。不管开发者运行了多少个工作者实例,一个作业应该只处理一次。这种模式也称作赢者通吃。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.11 事件驱动的服务间异步通信

消费者也并不清楚是哪个服务发出的事件,如图3.12所示。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.12 一个任务队列将工作分发给1到n个消费者

market网关就可以按照这种方式来进行操作。order服务创建的每个订单都会触发一个OrderCreated事件,这个事件会放到队列*market网关服务来进行下单。这种模式在下列场景中很有用:

(1)事件与响应该事件所要做的工作之间是一对一的关系;

(2)要完成的工作比较复杂或者花费时间较长,所以需要和触发事件区分开。

默认这种方式并不需要复杂的事件传输。有许多任务队列类库可以用,它们使用的就是日常的数据存储方案,如Redis(Resque、Celery、Sidekiq)或SQL数据库。

2.发布-订阅

在发布-订阅模式中,服务可以向任意的监听器发送事件。所有接收到事件的监听器都要对事件相应地做出反应。在某些情况下,这是一种理想的微服务模式:一个服务可以发送任意的事件给外界,而不需要关心谁来处理它们(图3.13)。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.13 发布-订阅模式是如何将事件发送到订阅方的

比如,想象一下,开发者需要在订单发布以后触发另一个下游操作。开发者可能要给用户发送一条推送通知,需要用它满足订单统计和推荐功能的需要。这些功能都可以通过监听同样的事件来实现。

3.4.4 服务定位

在结束本节之前,我们来研究一下服务发现(service discovery)。要实现服务间的通信,它们需要能够发现彼此。平台层应该提供这一功能。

实现服务发现最简单的方式就是使用负载均衡器(图3.14)。比如,AWS上的弹性负载均衡器(ELB)就为服务分配了一个DNS名称并且负责管理底层节点的健康检查。这些节点是属于同一个虚拟机组的(在AWS中是自动扩容组)。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.14 使用负载均衡器和DNS名称的服务发现方案

这种方式是可行的,但是它无法处理那些更复杂的场景。如果想要将请求路由到不同版本的代码中支持“金丝雀部署”或者“灰度上线”呢?如果需要将请求路由到不同的数据中心呢?

一种更加复杂先进的方式是使用类似Consul这样的注册中心。每个服务实例把它们自己注册到注册中心,然后注册中心会提供一个API来对这些服务的请求进行解析——实现方式可以是通过DNS,也可以是其他自定义的机制。这种方案如图3.15所示。

服务发现需要依赖于所部署的应用的拓扑结构的复杂度。部署方式越复杂(比如多地部署),越要求服务发现的架构更具鲁棒性[1]。

 注意

我们在第9章中部署Kubernetes时,开发者会了解到Kubernetes所使用的服务发现的方案。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.15 以服务注册中心作为真实数据来源的服务发现方案

3.5 服务边界

边界层隐藏了内部服务的复杂交互,只展示了一个统一的外观。像移动App、网页用户界面或者物联网设备之类的客户端都可以和微服务应用进行交互。(这些客户端可以是自己进行开发的,也可以由消费应用的公共API的第三方来开发。)比如图3.16描述的SimpleBank公司的内部管理工具、投资网站、iOS和Android 应用以及公共API。

边界层对内部的复杂度和变更进行了封装和抽象(图3.17)。比如,工程师可以为一个要列出所有历史订单的客户端提供一个固定不变的接口,但是,可以在经过一段时间以后完全重构这个功能的内部实现。如果没有边界层的话,客户端就需要对每个服务都了解很多信息,最后变得和系统的实现耦合越来越严重。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.16 SimpleBank的客户端应用

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.17 边界层提供了服务层的外观,将内部的复杂性对消费者隐藏起来

其次,边界层提供了访问数据和功能的方法,以便客户端可以使用适合自己的传输方式和内容类型来访问。比如,服务之间相互通信采用的是gRPC的方式,而对外的边界层可以暴露一个HTTP API给外部消费者,这种方式更适合外部应用使用。

将这些功能合到一起,应用就变成了一个黑盒,通过执行各种(客户端并不知道的)操作来提供功能。开发者也可以在修改服务层时更加自信,因为客户端是通过一个入口来与服务层连接起来的。

边界层还可以实现一些其他面向客户端的功能:认证和授权——验证API客户端的身份和权限;限流——对客户端的滥用进行防卫;缓存——降低后端整体的负载;日志和指标收集——可以对客户端的请求进行分析和监控。

把这些边缘功能放到边界层可以将关注点的划分更加清晰——没有边界层的话,后端服务就需要独立实现这些事务,这会增加它们的复杂度。

开发者同样可以在服务层中用边界来划分业务领域,比如,下单流程包括几个不同的服务,但是应该只有一个服务会暴露出其他业务领域可以访问的切入点(图3.18)。

 注意

内部服务边界通常反应的是限界上下文:整个应用业务领域中关系比较紧密的有边界的业务子集。我们会在下一章探讨这部分内容。

我们已经从整体角度介绍了边界层的用法,接下来具体研究3种相关但又不同的应用边界模式:API网关、服务于前端的后端以及消费者驱动的网关。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.18 边界也可以存在于在微服务网应用的不同情境上下文中

3.5.1 API网关

我们在第2章介绍了API网关模式。API网关在底层的服务后端之上为客户端提供了一个统一的入口点。它会代理发给下层服务的请求并对它们的返回结果进行一定的转换。API网关也可以处理一些客户端关注的其他横向问题,比如认证和请求签名。

 提示

可选的API网关包括Mashape公司的Kong这样的开源方案,也包括类似AWS API Gateway这样的商业产品。

API网关如图3.19所示。网关会对请求进行认证,如果认证通过,它就会将请求代理到对应的后端服务上。它还会对收到的结果进行转换,这样返回的数据更适合客户端。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.19 API网关处理客户端请求

从安全的角度来看,网关也能够将系统的暴露范围控制到最小。我们可以将内部的服务部署到一个专用网络中,限制除网关以外的所有请求进入。

 警告

有时候,API网关会执行API组合(composition)的工作:将多个服务的返回结果汇总到一个结果中。它和服务层的聚合模式的界限很模糊。最好注意一些,尽量避免将业务逻辑渗透到API网关中,这会极大增加网关和下层服务之间的耦合度。

3.5.2 服务于前端的后端

服务于前端的后端(BFF)模式是API网关模式的一种变形。尽管API网关模式很简洁,但是它也存在一些缺点。如果API网关为多个客户端应用充当组合点的角色,它承担的职责就会越来越多。

比如,假设开发者同时服务于桌面应用和移动应用。移动设备会有不同的需要,可用带宽低,展示数据少,用户功能也会有不同,如定位和环境识别。在操作层面,这意味着桌面API与移动API的需求会出现分歧,因此开发者需要集成到网关的功能就会越来越宽泛。不同的需求可能还会相互冲突,比如某个指定资源返回的数据量的多少(以及体积的大小)。在开发内聚的API和对API进行优化时,在这些相互竞争的因素间进行平衡是很困难的。

在BFF方案中,开发者会为每种客户端类型提供一个API网关。以SimpleBank公司为例,它们提供的每种服务都有一个自己的网关(图3.20)。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.20 SimpleBank公司的客户端应用的BFF模式

这么做的话,网关就是高度专一的,对于消费者的需求响应更加及时,而不会导致臃肿或者冲突。这样的网关规模更小,更加简单,也使得开发过程可以更加集中。

3.5.3 消费者驱动网关

在前面两种模式中,API网关决定了返回给消费者的数据的结构。为了服务于不同的客户端,开发者可能需要开发不同的后台。现在我们反其道而行之。如果开发者可以开发一个允许消费者向服务端表达它们所需要的数据格式,会怎样呢?可以把它看作 BFF 方案的一种进化版:不构建多个API,开发者可以只构建一个“超级”API来让消费者决定他们所需要的响应数据的样子。

开发者可以通过GraphQL来实现上述目标。GraphQL是一种用于API的查询语言,它允许消费者指定他们需要的数据字段以及将多个不同资源的查询复用到一个请求中。比如,开发者可以将代码清单3.1所示的格式暴露给SimpleBank客户端。

代码清单3.1 SimpleBank的基本GraphQL格式

type Account { 
  id: ID!     ⇽---  “!”表明这个字段不可以为空                    
  name: String!
  currentHoldings: [Holding]!     ⇽---  一个账号包含一组持仓和订单信息             
  orders: [Order]!
}

type Order {
  id: ID!
  status: String!
  asset: Asset!
  quantity: Float!
}

type Holding {
  asset: Asset!
  quantity: Float!
}

type Asset {
  id: ID!
  name: String!
  type: String!
  price: Float!
}

type Root {                         
  accounts: [Account]!     ⇽---  返回所有数据或者按ID返回一个账户          
  account(id: ID): Account      
} 

schema: {
  query: Root    ⇽---  只有一个查询入口           
}

上述格式展示了消费者的账户以及每个账户所包含的订单和所持股份。客户端之后就可以按照这种格式来进行查询。如果移动应用屏幕显示出某个账户的所持股份和未完成订单,开发者就可以用一个请求来获取这些数据,如代码清单3.2所示。

代码清单3.2 使用GraphQL的查询体

{
  account(id: "101") {   ⇽---  按账户ID进行过滤
    orders      ⇽---  在请求中指定响应结果中返回成员字段                    
    currentHoldings                
  }
}

在后端,GraphQL服务器的表现像API网关,代理多个后端服务的请求并将数据进行组合(在本例中order服务和holding服务)。我们不会在本书中将进一步详细地介绍GraphQL,如果读者感兴趣的话,可以查看GraphQL的官方文档。我们用Apollo成功地将RESTful 的后端服务封装成了GraphQL API的格式。

3.6 客户端

与三层架构中的展示层一样,客户端层为用户提供了一个应用界面。将客户端层与下面的其他几层进行分离,就可以以细粒度的方式来开发用户界面,并且可以满足不同类型的客户端的需求。这也意味着,开发者可以独立于后端的功能来开发前端。正如前面几节中提到的,应用可能需要服务于许多形形色色的客户端——移动设备、网站、对内的和对外的——每种客户端都有自己不同的技术选型和限制。

微服务为它自己的用户界面提供服务的情况并不常见。通常来说,提供给特定用户的功能要比单个服务的功能要更广泛。比如,SimpleBank的管理人员需要处理订单管理、账户创建、对账、收税等工作。与之相随的是那些横向事务——认证、审计日志、用户管理——显然,它们并不是order服务或者account setup服务的职责。

3.6.1 前端单体

后端很明确地被分成了一个个可以独立部署维护的服务——相应地,我们还有10章的内容要介绍。但是在前端也完成这个工作是非常具有挑战性的。一个微服务应用中的标准前端可能依旧是一个单体——前端作为一个整体来部署和修改(图3.21)。专业的前端,特别是移动应用,通常需要专门的小组,这也使得端到端的功能所有权很难实现。

 注意

我们会在第13章对端到端的所有权(以及在微服务应用开发中的益处和价值)展开进一步的讨论。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.21 微服务应用中,标准的前端客户端可能变得越来越臃肿

3.6.2 微前端

随着前端应用的不断发展,它们开始和大规模的后端开发一样面临协作和摩擦问题。

如果可以像拆分后端服务那样将前端部分也进行拆分,那就太好了。在Web应用中出现的一个新趋势是微前端——以独立打包和部署的组件来提供UI的各个部分的功能,然后合并起来。这一方式如图3.22所示。

如何将微服务应用设计为四层结构:平台层/服务层/边界层/客户端层

图3.22 由各个独立片段组成的用户界面

这样,每个微服务团队就可以端到端地交付功能了。比如,如果开发者有一个订单团队,就可以独立地一起交付订单管理微服务以及负责下单和管理订单的Web界面。

虽然这种方式很有前途,但是也面临许多挑战:在不同的组件间保持视觉和交互的一致性,需要很大的精力来开发和维护通用组件以及设计准则;当需要从多个源头加载JavaScript代码时,Bundle的大小是难以管理的,进而它又会影响加载时间;接口重载和重绘可能会导致整体的性能变差。

微前端还不是很普遍,但是人们已经在这块荒地上使用了一些不同的技术方案,其中包括:Web组件通过清晰的、事件驱动的API来提供UI片段;使用客户端包含(client-side include)技术来集成片段;使用iframe来将微app放置到不同的屏幕区域;在缓存层使用ESI(edge side include)来集成组件。

如果开发者有兴趣了解更多内容的话,可参考Micro Frontends和Zalando的Mosaic项目。