在微服务架构中,服务之间势必需要集成,而这种集成关系远比简单的API调用要复杂。在本文中,我们将系统分析服务集成的方式以及在微服务架构中的表现形式。关于服务之间的集成存在一些通用的模式,我们也将在梳理这些模式的同时给出实现过程中的最佳实践。
业界关于系统集成存在一些主流的模式和工程实践,包括文件传输(FileTransfer)、共享数据库(Shared Database)、远程过程调用(RPC)和消息传递(Messaging)。这四种主流的集成模式各有优缺点。文件传输方式最大的挑战在于如何进行文件的更新和同步;如果使用数据库,在多方共享的条件下如何确保数据库模式统一是一个大问题;RPC容易产生瓶颈节点;而消息传递在提供松耦合的同时也加大了系统的复杂性。RPC和消息传递面对的都是分布式环境下的远程调用,远程调用区别于内部方法调用,一方面网络不一定可靠和存在延迟问题,另一方面集成通常面对的是一些异构系统。
对于微服务架构而言,我们的思路是尽量采用标准化的数据结构并降低系统集成的耦合度。我们会根据需要采用上图中所示的四种典型的系统集成模式,同时还会引入其它一些手段来达到服务与服务之间的有效集成。个人把微服务架构中服务之间的集成模式分为如下图所示的四大类。
接口集成
接口集成是服务之间集成的最常见手段,通常基于业务逻辑的需要进行集成。RPC、REST、消息传递和服务总线都可以归为这种集成方式。
数据集成
数据集成同样可以用于微服务之间的交互,共享数据库是一个选择,但也可以通过数据复制的方式实现数据集成。
客户端集成
由于微服务是一个能够独立运行的整体,有些微服务会包含一些UI界面,这也意味着微服务之间也可以通过UI界面进行集成。
外部集成
这里把外部集成单独剥离出来的原因在于现实中很多服务之间的集成需求来自于与外部服务的依赖和整合,而在集成方式上也可以综合采用接口集成、数据集成和UI集成。
接下来我们将对这四大类集成策略展开讨论,给出对应的实现技术的简单描述。
(1)接口集成
首先我们讨论一下RPC。RPC(Remote Process Call,远程过程调用)架构是服务之间进行集成的最基本方式。我们可以对RPC架构进行剖析,得到下图的结构图,该结构图包括了微服务之间在分布式环境下交互时所需的各个基本功能组件。
从上图中,可以看到RPC架构有左右对称的两大部分构成,分别代表了一个远程过程调用的客户端和服务器端组件。客户端组件与职责包括负责编码和发送调用请求到服务方并等待结果、负责维持客户端和服务端连接通道和发送数据到服务端等;而服务端组件与职责则包括负责接收客户方请求并返回请求结果和负责调用服务端接口的具体实现并返回结果等。对于客户端和服务器端而言,都需要负责网络传输协议的编码和解码。目前业界也存在很多优秀的RPC框架,如应用非常广泛的Apache Dubbo等。
说道RPC,就不得不提REST。REST(Representational State Transfer,表述性状态转移)从技术上讲也可以认为是RPC架构的一种具体表现形式,因为RPC架构中最基本的网络通信、序列化/反序列化、传输协议和服务调用等组件都能在REST中有所体现。但REST代表的并不是一种技术,也不是一种标准和规范,而是一种设计风格。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。要理解RESTful架构,最好的方法就是去理解它的全称Representational StateTransfer这个词组,直译过来就是“表现层状态转移”,其实它省略了主语。“表现层”其实指的是“资源”的“表现层”,所以REST通俗来讲就是:资源在网络中以某种表现形式进行状态转移。主流的Spring Cloud采用的就是基于HTTP协议和RESTful风格的交互方式。
从软件设计的耦合度上讲,无论是RPC还是REST都存在一定的耦合度问题。就RPC而言,存在三种耦合度,即技术耦合、空间耦合和时间耦合,如下图所示:
上图中,技术耦合度表现在服务提供者与服务消费者之间需要使用同一种技术实现方式,如(a)中服务提供者与服务消费者都使用RMI作为通信的基本技术,而RMI是Java领域特有的技术,也就意味着其它服务消费者想要使用该服务也只能采用Java作为它的基本开发语言;空间耦合度指的是服务提供者与服务消费者都需要使用统一的方法签名才能相互协作,(b)中的getUserById(id)这个方法名称和参数的定义就是这种耦合的具体体现;而时间耦合度则表现在服务提供者与服务消费者两者只有同时在线才能完成一个完整的服务调用过程,如果出现图(c)中所示的服务提供者不可用的情况,显然服务消费者调用该服务就会发生失败。
对于REST而言,情况相对会好一点。基于HTTP的面向资源的架构风格能够支持在服务提供者与服务消费者之间采用多种不同的技术实现方式,从而规避技术耦合度。而对于空间耦合,也可以采用HATEOAS一定程度上缓解这种耦合度。但在时间耦合度上,REST风格面临与RPC同样的场景和问题。
消息传递(Messaging)机制能够降低技术、空间和时间耦合。如下图所示,消息传递机制在消息发送方和消息接收方之间添加了存储转发(Storeand Forward)功能。存储转发是计算机网络领域使用最为广泛的技术之一,基本思想就是将数据先缓存起来,再根据其目的地址将该数据发送出去。显然,有了存储转发机制之后,消息发送方和消息接收方之间并不需要相互认识,也不需要同时在线,更加不需要采用同样的实现技术。紧耦合的单阶段方法调用就转变成松耦合的两阶段过程,技术、空间和时间上的约束通过中间层得到显著缓解,这个中间层就是消息传递系统(MessagingSystem)。
在消息传递系统中,消息的发送者称为生产者(Producer),负责产生消息,一般由业务系统充当生产者;消息的接收者称为消费者(Consumer),负责消费消息,一般是后台系统负责异步消费。生产者行为模式单一,而消费者根据消费方式的不同有一些特定的分类,常见的有推送型消费者(PushConsumer)和拉取型消费者(Pull Consumer),推送指的是应用系统向消费者对象注册一个Listener接口并通过回调 Listener 接口方法实现消费消息,而在拉取方式下应用系统通常主动调用消费者的拉消息方法消费消息,主动权由应用系统控制。在微服务架构中,我们同样需要有一套能够提供消息传递功能的工具和框架从而实现消息驱动的服务开发和交互能力。这方面的工具也很多,例如RabbitMQ、Kafka、RocketMQ等。
与消息传递相关的另一个技术是服务总线。服务总线(Service Bus)本质上也是一种系统集成组件,用于解决分布式环境下的异步协作问题,可以看作是对消息传递系统的扩展和延伸。使用服务总线的典型需求包括:
将消息路由到一个或多个目的地
将消息转化为另一个表现形式
执行消息的分解和聚合功能,即能够实现消息的分解并将分解后的消息发到目的地之后再进行组装
使用发布-订阅模式来提供动态内容等。
围绕这些需求,ESB提供了实现这些需求的核心组件,包括路由器(Router)、转换器(Transformer)和端点(Endpoint)。对于这些组件本文不做具体展开。服务总线也可以看做是一种规范,业界基于如何实现服务总线提供了多种第三方工具,如MuleESB、Apache Camel和Spring Integration。这些工具都为我们提供了强大而齐全的端点集成机制,同时通过封装简化了这些端点的使用方式。以Spring家族的Spring Integration为例,该工具为我们提供的常见集成端点包括File、FTP、TCP/UDP、HTTP、JDBC、JMS、JPA、Mail、MongoDB、Redis、RMI、WebServices等不下数十种,且各个集成端点在使用方式上大同小异。实际上,SpringCloud Stream中的Spring Cloud Stream就是构建在Spring Cloud Integration之上。
(2)数据集成
如果共享数据库,数据的存储和表现形式不容易被修改和重构,因为有很多系统对这些数据持有访问权限。一旦对数据做出修改,就可能导致其中一个或多个系统不能正常运作。这就意味着对数据的修改需要协调各个应用系统,这显然会影响到系统的可扩展性。另一方面,这也会导致无法对系统功能进行快速迭代,而业务的快速迭代正是微服务架构所应具有的特性。
对共享数据库最难以把控的一点是如何统一数据库模式(Scheme)。试想如果一个应用系统需要删除某张表中的某个字段,对于普通的场景而言,这无疑是非常简单的事情。但对于共享的数据库而言,由于不知道其它系统是否还在使用该字段,所以也就无法进行直接删除。如果这样的场景很多,那么随着时间的推移,数据的复杂性和可维护性都会对系统造成很多影响。
共享数据库显然不能满足微服务架构中的集成需求,在微服务架构中,我们追求数据的独立性。但对于一些遗留系统而言,我们无法重新打造数据体系,数据复制(DataReplication)就成为一种折中的集成方法。所谓数据复制,就是在不同的数据容器中保存同一份业务数据。这里的同一份业务数据的概念不在于说数据内容的完全一致性,而是在于这些数据背后的业务逻辑的一致性。
实现数据复制的关键就在于打破数据库模式的限制。当我们采用某个方式在两个数据存储容器中同时存放两份数据时,如果它们的数据库模式是完全一样的,那么还是会碰到共享数据库模式下的诸多问题。当我们在实施数据复制的集成方式时,将某一份数据库模式转化成其他服务所需要的形式然后再进行数据同步是一项最佳实践。
有了数据复制的设计理念,接下去就要考虑数据冗余所带来的数据一致性(Consistency)的问题。我们明确数据的实时一致性通常都是不需要的,所以可以采取最终一致性(Eventually Consistency)的方式实现数据复制。实现数据复制有两种基本策略,一种是批量操作,一种是事件。
批量(Batch)操作一般通过定时任务的方式在某一个时间点对一批符合复制要求的数据进行同步操作。在实现上,批量操作最好能够支持全量和增量操作,同时为每一批数据确定一个全局唯一的版本号。通过版本号,数据集合就具备选择性和去重性,也就意味着批量操作是一个可以重复执行的过程。批量操作具备一定风险性,由于批量操作本身无法持有状态,利用版本号把状态放到数据中去。另一方面,在数据的接收方,确保采用一定的数据适配机制实现解耦。采用批量操作实现数据复制的结构图参考下图,这里的数据仓库泛指包含关系型数据库在内的各种数据存储媒介。
而对于事件而言,需要将所产生的数据建模成一系列离散事件,我们可以借助于消息传递系统达到数据同步的目的。相比批量操作,事件驱动的数据复制机制能达到较高的数据一致性要求。事件发送方相对简单,只需要将所产生的事件放入事件发布器即可,但对于事件的订阅者而言,可能存在多种表现形式。不管基于何种订阅者模式,在技术实现上我们都可以借助前面介绍的消息传递机制达到基于事件的数据复制效果。
(3)客户端集成
当微服务数量较多且客户端集成场景比较复杂时,通常就需要单独抽取一层作为客户端访问的统一入口,这一层在微服务架构里有个专门的叫法称之为API网关(Gateway)或服务网关。API网关的主要作用是对后端的各个微服务进行整合,从而为不同的客户端提供定制化的内容。API网关是微服务架构的基础组件。
BackEndFor FrontEnd(BFF)服务器是对API网关更为形象的叫法,也就是专门为前端服务的后端服务器。该服务器在定位上只应该是很薄的一层,不应该包含任何与业务相关的逻辑和实现。同时,如果整个系统非常庞大,所有的服务集成都放在一起也会加重这层的维护成本,所以针对不同的业务体系提供专门的BackEndFor FrontEnd服务器也是集成过程中的一项最佳实践。下图展示的就是BackEndFor FrontEnd服务器应用的示例,可以看到系统中存在移动后端、门户后端和管理后端三种BackEndFor FrontEnd服务器组件,分别面向移动应用、门户网站和内部管理系统。
在BackEnd For FrontEnd服务器上,由于服务数量大,修改和发布的频率也可能很高,微服务所提供的接口变化管理上通常采用逐步迁移的方案(见下图)。
(4)外部集成
随着服务化思想以及SaaS(SoftwareAs A Service,软件即服务)应用的日渐增多,与外部系统进行集成的方式也发生了很多变化。在服务集成领域,目前基于服务回调的集成方式应用非常广泛。
对于集成方式,实际上也可以简单理解为服务与服务的集成,只不过有些服务是来自第三方平台。回调作为消除循环依赖的一种有效方式,只需要我们提供回调入口即可完成与外部系统的集成。整个服务交互过程中,在服务访问入口添加防腐层是一项最佳实践。而防腐层的建立通常需要实现适配(Adapt)和转换(Convert)。考虑这样一个场景,当外部系统通过基于RESTful风格暴露访问接口给我们时,我们在使用该服务时,就需要考虑如何获取通过HTTP协议传输的数据以及如何将这些数据转换为该系统自身所能识别的业务数据。下图展示了该场景下防腐层的实现过程。同样,对于我们所提供的供外部系统访问的回调接口,防腐层的设计理念同样适用。
本文对微服务架构中服务之间如何集成的相关技术做了简要综述,希望对大家有所帮助。
更多内容可以关注我的公众号:程序员向架构师转型。