基于服务架构的世界

微服务和SOA都被认为是基于服务的架构,这意味着这两种架构模式都非常强调将“服务”作为其架构中的首要组件,用于实现各种功能(包括业务层面和非业务层面)。微服务和SOA是两种差异很大的架构模式,但是他们仍有一些相同的特征。

所有基于服务的架构的一个共性是他们一般都是分布式架构,也就是服务组件都是通过远程访问协议来实现的,例如REST、SOAP、AMQP、JMS、MSMQ、RMI或者.NET Remoting。相对于单体式架构和分层式架构,分布式架构有很多优势,包括可伸缩性、解耦能力以及对开发、测试和部署的可控性等。分布式架构中的组件更趋向于自包含,因此其变更管理和维护也更容易,从而使得相应的应用也更稳定,响应也更快。分布式架构也非常适用于各模块之间耦合度较低、更加模块化的应用。

在基于服务的架构语境中,“模块化”指的是将应用的各个部分分别封装为自包含的服务的做法。拆分后的每个服务都可以单独设计、开发、测试和部署,与其他组件或服务之间的依赖性很低甚至没有。模块化的架构还支持用重写的方式来维护组件的做法。随着业务增长,架构可以逐渐地、以很小的部件为单位进行重构或者替换,而不是大张旗鼓地对整个应用进行重构或者替换。

不幸的是,凡事都有代价,享受分布式系统的优点也一样。与优点相伴的缺点则是复杂性的增加和投入的增长。维护服务合约、选择正确的远程访问协议、处理不响应的或不可用的服务、加密远程服务和管理分布式事务,这些还只是构造基于服务的架构时许多复杂问题中的一部分。本章中,我会描述这些与基于服务的架构有关的复杂问题。

服务合约

服务合约是服务提供者(通常是远程的)和使用者(客户)之间使用合约语言(XML、JSON、Java Object等)约定数据输入和数据输出的一份协议。创建和维护服务合约是一项困难的工作,不应该被轻视或者当作补充条款。因此,服务合约的议题在基于服务的架构设计中值得特殊关注。

在基于服务的架构中,存在两种服务合约模型可供使用:基于服务的合约和客户驱动的合约。两者之间的真正差别在于合作程度。在基于服务的合约中,服务是合约的唯一拥有者,一般可以在不考虑服务客户需求情况下演化或修改合约。这种模式强迫服务的所有客户都要接受新的服务合约变更,而不管客户是否需要这些新功能。

与之相反,客户驱动的合约所基于的是服务和与服务客户之间更为密切地合作的一种关系。在这种模型下,服务拥有者和客户有很强的合作(关系),因此任何服务合约变更会充分考虑客户的需求。采用这种模型时,服务(拥有者)一般需要了解客户是谁以及每个客户都是如何使用这些服务的。客户可以对服务合约随意提出变更建议,服务方则可以根据是否影响其他客户而自行决定是否采纳。理想情况下,服务的客户向服务拥有者发起修改建议和测试用例,测试被执行时可以监测该修改是否影响其他客户。开源工具如Pact和Pacto可以帮助维护和测试这类客户驱动的合约。

服务合约的上下文中另一个重要议题是合约的版本。我们必须接受这一现实——在服务和服务客户间实现绑定的合约终究是要变化的。改变程度和范围取决于这些变更如何影响每个客户,以及合约改变后服务的向后兼容性。

合约的版本化使得启用新的、包含合约变更的服务功能时能够为仍旧使用老版本合约的客户提供向后兼容性。从开始开发之前就要为合约的版本化做出规划,即使开始你觉得不需要,到了最后一定需要,这也许是本章最重要的一条建议。有很多开源和商业版本的框架可以用来帮助管理和实施合约版本化策略,不过你也可以选择使用两项基本技术来实现自己的合约版本化策略:同质版本化和异质版本化。

同质版本化是指在同一个服务合约中使用合约版本号。图1-1中,被客户A和B同时使用的合约都用圆形(代表同一服务合约)来表现,但是其中各自包含着不同版本号。举一个简单例子,假定合约是基于XML的,用来表示某些商品的订单,采用合约版本号1.0。现在新版本1.1发布了,其中包含了额外字段用于提供当订单派送而接收人不在家的时候的交货规则。这种情况下,可以通过让新的delivery-instructions字段成为可选字段来保持向后兼容版本1.0的合约。

图1-1
微服务与SOA架构(1)_编程语言
异质版本化则涉及对不同类型合约的支持。这个技术跟本节之前描述的客户驱动的合约很相似。服务引入新的功能特性时,就引入新合约用来支持这一(些)功能特性。注意,图1-1与1-2之间不同在于表示服务合约的形状。1-2中,客户A使用圆圈所代表的合约,而客户B则使用三角形所代表的合约。在这种情况下,向后兼容性是由不同合约而不是同一合约的不同版本所支持的。许多基于JMS的消息系统都是采用这种方式,尤其是使用ObjectMessage消息类型的系统。例如,基于Java的接收端可以使用instanceof关键字来检查通过消息发送的对象类型,然后根据对象类型来采取响应的步骤。或者,XML负载可以通过JMS的 TextMessage来发送(每个不同合约包含完全不同的XML schema),由一个消息属性来指明XML schema和XML类型负载之间的对应关系。

微服务与SOA架构(1)_大数据_02
图1-2

合约版本化的主要目的就是提供向后兼容性。谨记,服务必须支持多个版本的合约(或者多个合约),这样才能使得开发团队能够更快地部署新的功能特性或者其它变更,而不必担心破坏与其它服务客户之间的现有合约。需要注意的是,这两种技术也可以组合起来,支持不同合约类型的多个版本。

最后一个关于服务合约中变更合约需要注意的是:一定要从开始就制定一个明确的服务客户沟通策略,以便客户能够及时获知合约变更的信息,或者特定合约类型不再被支持的信息。很多情况下,因为内部/外部客户很多,这类沟通并不可行。这时候,一个集成中心(integration hub),或者说消息中间件,可以在服务的客户与服务之间建立抽象层,实行服务合约的转译。我将在后续“合约解耦”一节种谈到这种可能性。

服务可用性

服务可用性(Availability)和服务响应能力(Responsiveness)是基于服务的架构通常需要考虑的另外两个问题。这两者都涉及服务客户与远程服务之间通信的能力,但他们还是有着轻微不同的内涵,因此客户也会采用不同方式来解决这两个问题。

服务可用性是指远程服务及时地接受请求的能力,例如,与此远程服务建立连接。服务响应能力是指客户及时地从服务接收响应的能力。图1-3展示了这种不同。

微服务与SOA架构(1)_大数据_03
图1-3

尽管这两种错误条件造成的结果是一样的(服务请求无法被处理),但是处理方式是不同的。因为服务可用性跟服务的可连接性有关,因此客户能够做的并不多,只能要么多试几次,要么在可能的情况下把请求放入队列,以后再处理。

处理服务响应能力问题就更困难一些。请求成功地发送给服务之后,客户需要等待多久?是否服务仅仅是很慢,又或者服务端发生了什么事情导致无法发送响应?解决超时条件问题对于远程服务的可连接性来说是相当具有挑战性的。一种常见的做法是在有负载的情况下获得最长响应时间作为基准,然后在此基础上添加额外的时延以处理负载的波动。例如,假设运行某个基准测试时,某个特定服务请求的最大响应时间为2000毫秒。那么你可能需要将这个值乘以二,用以处理负载较高的状况,因此预期的超时时长应该是4000毫秒。

尽管看起来这是一个合理的估算服务响应超时时长的解决方案,但有时也会有各种问题。首先,如果服务真的停止了或者未启动,每个请求都要等待四秒才能判定服务无响应。这实在是效率太低,而且会引起最终客户的不满意。另外一个问题在于,你所使用的基准测试可能并不精确,在高负载情况下,服务响应时间可能会是五秒而不是我们所算出来的四秒。这种情况下,服务端实际上能够做出响应,但是客户端会因为超时时长设得太低而拒绝了所有响应。

比较常见的一种解决方案是使用断路器模式(circuit breaker pattern)。如果服务不能及时做出响应或者干脆不响应,软件断路器将会起作用,提醒客户不必浪费时间等待超时事件发生。与物理断路器相比,软件断路器很不错的一点是在实现这种设计模式时,可以在服务端开始发送响应或者变得可用时对自己进行重置。软件断路器有不少开源实现,包括Netfix的Ribbon等。你可以在Michael Nygard的书Release It!中读到更多的关于断路器模式的内容。

在处理超时时长值的问题时,应该尽量避免使用全局的超时值来处理每个请求。恰恰相反,建议你基于上下文来定义超时时长,同时确保这些数值可以从外部通过配置来设置,这样你就可以在负载条件变化的情况下快速做出响应,而不用重建或者重新部署应用。另一种方案是在代码中使用“智能超时值(smart timeout values)”,以便在负载变化情况下自行调整超时值。例如,应用可以在高负载或者网络有问题的情况下自动增加超时时长值。当负载降低,响应时间变短后,应用可以重新计算平均响应时间,并对应地降低超时时长值。

安全性

在基于服务架构下,服务都是远程访问的,很重要的一点是需要确认给定的客户是否被授权访问某个特定服务。根据实际情况,服务的客户可能既需要被认证(authentication)也需要被授权(authorization)。认证是指服务客户是否可以连接到某个服务,一般是通过用户名和密码来建立登录凭证来进行认证。某些情况下,仅仅执行认证还不够:客户可以连接到某个服务并不意味着他可以访问服务所提供的所有功能。授权指的是某个服务客户是否被允许访问服务内部特定的业务功能项。

安全在早期SOA实现中是个大问题。原本被严格隔离的某个应用的功能突然被公开,从企业的全球各处都能访问到。这一变化所引发的问题促使我们换个角度思考,重新认识服务以及如何保护服务不会被不该访问的客户访问到。

对微服务而言,安全问题成为挑战主要是因为没有一个专门处理安全问题的中间件组件。相反地,每个服务必须各自处理安全性问题,或者在某些情况下需要增强API层以使之更加智能地处理应用的安全性问题。我看到的微服务中所实现的一个比较好的设计是将认证工作委托给一个独立的服务,而保留授权工作在微服务内部。尽管这一设计可以修改为将认证和授权都交由单独的服务来完成,我还是倾向于将授权包含在微服务自身当中。这样做可以避免与远程安全性服务之间的太多的不必要交互,还可以使得服务的上下文边界更加稳定,减少对外界的依赖。

事务

事务管理在基于服务的架构中也是很大的挑战。大多数时候我们所讨论的事务是指在大量业务应用中都存在的ACID(atomicity、consistency、isolation和durability)事务。ACID事务用于维持数据库一致性,对同一个请求中的多个数据库更新操作进行协调,使得当事务处理过程中发生问题时,该请求所作出的所有数据库更新都可以回滚。

考虑到基于服务的架构一般都是分布式架构,在多个远程服务之间传播和维护事务的上下文很困难。如图1-4所示,一个服务请求(红色X旁边的方框)可能需要调用多个远程服务来完成请求。红色X表明这种情况下无法实现ACID事务。

微服务与SOA架构(1)_java_04
图1-4

事务问题在SOA架构中更为普遍,因为与微服务架构不同,SOA架构中通常使用多个服务来完成一个业务请求。我将在对比架构特点一章的“服务编排”一节中详细讨论这个问题。

基于服务的架构一般更依赖于 BASE事务,而不是ACID事务。BASE事务一般包括基本的可用性、软性的状态和最终一致性(basic availability、soft state和eventual consistency)。分布式应用依赖BASE事务来追求数据库中的最终一致性而不是每个中间事务的一致性。一个典型的BASE事务的例子是往ATM机里存钱。当通过ATM机向你的账户中存入现金,大概几分钟甚至几个小时后才会在账号中显现出来。换句话说,钱从离开自己的手到真正存入账户之间有一个软性的转换状态:钱已经离手,但是还没到达你的银行账户中。我们可以容忍这一延迟,并且寄希望于软状态和最终一致性,因为我们知道并信任这笔钱最终一定会到达我们的账号。从全系统视图的角度看,批处理作业也是依赖最终一致性的。

迁移到到基于服务的架构需要我们改变对事务和一致性的认识。对于不能依赖最终一致性和软状态的而必需事务一致性的情况,可以将服务的粒度设计得较粗,从而把业务逻辑包装在一个服务内,最后通过ACID事务来实现事务级别的一致性。另一种方法是使用事件驱动技术,当请求状态变得一致时向相关客户推送通知。这种技术给应用带来了很高的复杂度,不过确实能够在使用BASE事务时实现事务状态管理。

太复杂了?

基于服务的架构相对于单体式应用来说是一种巨大的进步,但是正如你所看到的,他们同时也带来很多问题,例如服务合约、可用性、安全性以及事务管理等等。不幸的是,转向基于服务的架构,如微服务或者SOA,意味着必须在某些方面做出权衡。有鉴于此,除非做好准备并且原意去解决分布式系统所面对的种种问题,否则不要匆忙转向基于服务的架构。

本节讨论的问题很复杂,但它们肯定不是不可克服的障碍。大多数使用基于服务的架构的团队最终都能顺利通过开源、商用和定制化的方案解决和克服这些问题。

基于服务的架构是不是太复杂了?肯定是的。但是,事物总是两面的。伴随着复杂性,基于服务的架构也有一些额外的特性和能力,能够提高开发团队的效率,开发出更为可靠的、更为稳定的应用,同时降低总体开销并缩短产品进入市场的周期。接下来的三节中,我会对比微服务和SOA,帮助你了解哪种架构模式更适合自己。

对比服务特性

OASIS面向服务架构参考模型(OASIS Reference Model for Service Oriented Architecture)中将“服务”定义为“一种支持对一或多种能力进行访问的机制,这里的访问是通过使用预定义的接口来提供的,并且稳定地按照服务描述所规定的约束和策略来执行”。换句话说,一个服务要提供某些业务能力,并提供定义良好的接口以及定义良好的合约以便(客户)进行访问。这个定义没有规定的是如何基于类别、组织所有关系以及粒度『如服务的规模』(classification、organizational ownership and granularity)来进一步地定义服务。理解这些服务特性有助于在特定架构模式下为服务的上下文给出定义。

尽管微服务和SOA都有赖于服务作为其主要的架构组件,他们在服务特性上是有很大的差别的。本章将围绕不同模式下服务如何分类(也就是服务的分类学)、如何基于服务的所有者进行服务之间的协调以及微服务与SOA之间服务粒度上的不同展开讨论。