微服务与SOA架构

【编者的话】本文是Mark Richards写的微服务与面向服务架构完整报告。

基于服务架构的世界

微服务和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.png

图1-1

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

图1-2

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

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

服务可用性

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

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

图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事务。
1-4.png

图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之间服务粒度上的不同展开讨论。

服务分类学

服务分类学指的是在某种架构下服务是如何归类的。有两种服务分类的基本类型:服务类型和业务领域。服务类型分类法会根据整个架构中服务所扮演的角色进行分类。例如,某些服务是实现业务功能的,而另一些服务可能是实现非业务功能的,例如日志、审计和安全。业务领域分类法会根据服务在特定业务功能领域中所扮演的角色来进行分类,例如报表、交易处理和订单送货等等。

服务类型分类一般在架构模式层进行定义,而业务领域分类则在架构实现层进行定义。尽管架构模式提供了很好的基础来定义服务类型,作为一个架构师,你可以按照自己的想法对其进行修改,定义自己的分类方法。本节中,我们会关注架构模式以及在微服务和SOA中比较常见的服务类型。

微服务架构就服务类型而言其分类法并不复杂,一般来说主要有两类服务类型,如图2-1所示。功能服务(functional services)是指用来支持特定业务操作或功能的服务,而基础服务(infrastructure services)则负责支持非业务工作,例如认证、授权、审计、日志和监控。在微服务架构中,这是非常重要的区别,因为基础服务并不对外开放而仅作为供内部使用的私有共享服务,对其它服务可用。功能服务则提供外部访问能力,而且不对其它服务共享。
2-1.png

图2-1

SOA内的服务分类法跟微服务有很大不同。在SOA中,从全局架构来看有非常明确的、非常正式的服务类型,各自在整体架构中扮演不同角色。尽管在SOA中可以有任意数量的服务类型,架构模式定义了四种基本类型,如图2-2所示:
2-2.png

图2-2

业务服务(business services)是一种抽象的、高层级的、粗粒度的服务,定义在企业层面的执行的核心业务操作。因为抽象,所以不依赖于任何实现或者协议,一般只包括服务名字,期望的输入以及期望的输出。可选地,这些服务类型还可以包括处理步骤或者跟服务相关的特殊编排规则。业务服务一般都用XML、Web Services Definition Language(WSDL)或者Business Process Execution Language(BPEL)等语言来表述。一般确认某个服务是否属于业务服务会在服务名上下文前后加上“我们是否在做某某的业务”来加以判断。例如,有两个服务,分别名为ProcessTrade(处理交易)和InsertCustomer(插入客户)。那么“我们是否在做处理交易的业务”可以很清楚看出ProcessTrade是一个业务服务;而“我们是否在处理插入客户的业务”听上去就不对,所以不是一个好的业务服务抽象,更像是一个在处理业务服务时所调用的某个具体服务。

企业服务(enterprise services)是具体的、企业层级的、粗粒度的服务,用以实现业务服务所定义的功能。如图2-2中所示,一般是介于抽象业务服务和对应具体企业服务实现之间的中间件,在其间起到桥梁作用。企业服务可以与业务服务之间存在一对一或一对多的对应关系。
它们可以用任何语言和平台进行定制,或者采用第三方采购的产品(COTS)来实现。企业服务很独特的一点是它们通常会在组织内共享。例如,一个RetrieveCustomer(检索客户)的企业服务可能被组织内很多模块使用,用来接收客户信息。其它例如CheckTradeCompliance(检查交易合规) , CreateCustomer(创建客户), ValidateOrder(验证订单) 和 GetInventory(获取库存目录)等都是企业服务很好的例子。企业服务通常依赖应用服务(application services)和基础服务(infrastructure services)来完成特定业务请求。但是在某些情况下,某个企业服务也可能把完成特定请求所需要的所有业务功能都归入自身,形成自包含的服务。

应用服务(application services)是细粒度的、特定于具体应用的服务,与某个特定应用的语境相关。应用服务提供在企业服务中没有的特定的业务功能。例如,一个大型保险公司汽车报价应用可能提供服务来计算汽车保险费率。这是一个只针对该应用而并不适用于整个企业的服务。应用服务可以从某个专用的用户界面直接调用,或者通过某个企业服务调用。应用服务的例子包括:AddDriver(添加司机)、AddVehicle(添加车辆)以及CalculateAutoQuote(计算机车报价)等等。

SOA中最后一个基本服务类型是基础服务(infrastructure services)。与微服务架构相同,这些服务用于实现非功能性任务,例如审计、安全和日志。在SOA中,基础服务可以从应用服务或者企业服务调用。

需要记住的是,作为一个架构师,你既可以使用架构模式所提供的标准服务类型,也可以完全抛弃他们创建自己的分类方式。不管采用哪种方式,重要的事情是要确保针对架构存在定义良好且文档齐备的服务分类法。

服务责任制与协调

服务责任人(service owner)是组织内负责创建和维护某个服务的组别的类型。因为微服务架构仅支持有限的服务类型(功能服务和基础服务),应用开发团队一般都会同时负责这两种服务。尽管大量应用开发团队可能负责编写服务,需要了解的是他们都属于同一组别类型(即应用开发团队)。图2-3展示了微服务的服务责任制模型。
2-3.png

图2-3

对于SOA而言,一般对不同服务类型有不同服务责任人。业务服务的责任人通常是业务用户,而企业服务的责任人大多是是共享的服务团队或者架构师。应用服务的责任人一般是应用开发团队,基础服务的责任人一般是应用开发团队或者基础服务团队。中间件在SOA架构中经常使用,尽管不是一种服务,其责任人一般是整合架构师或者中间件团队。图2-4展示了SOA架构下服务责任制模型。
2-4.png

图2-4

服务责任人的重要性体现在全局的服务协调。在SOA中,必须在创建或维护某个应用需求的时候在多个组之间进行协调。关于抽象的业务服务必须咨询业务用户,关于完成业务服务功能所需的企业服务必须咨询共享服务团队,应用开发团队之间必须相互协调才能保证企业服务可以调用底层功能。同样,必须协调基础团队来确保非功能性需求能够通过基础服务得到保障。最后,所有这些都需要与中间件团队或者管理消息中间件的整合架构师进行协调。

在微服务场景中,完成某一业务请求通常只需要很少甚至完全不需要对多个服务进行协调。如果需要在多个服务责任人之间进行协调,也可以通过规模较小的应用开发团队快速而高效地完成。

微服务和SOA在服务责任制和总体协调上的不同直接决定了两种架构模式下开发、测试、部署和维护服务上所需要的精力和时间。在这个问题上,微服务模式表现突出。因为服务比较轻量,各个组之间协调最少,服务可以很快被开发、测试、部署。而这些优势最终又转化为较短的投放市场周期、较低的开发和维护成本以及和更稳定的应用。

服务粒度

从服务角度来看微服务和SOA的最大区别在于服务粒度。如名字所示,微服务是规模较小的、细粒度的服务。更确切地说,微服务架构中的服务组件一般都是单一目的/用途的服务,只做一件事且做到极致。而对于SOA而言,服务组件规模相差可以很大,可能是很小的应用服务,也可以是很大的企业服务。实际上,很常见的一种情况是SOA架构中的某个服务组件是由一个很大规模的产品或者一个子系统来提供。

有趣的是,SOA一开始碰到的最大挑战来自于服务粒度。如果不理解服务粒度影响,架构师很可能会设计粒度过细的服务,导致太多信息交互而影响整体性能。架构师和组件设计师很快学习到大规模的、粗粒度的、提供多种数据视图的服务是更好的服务模式。例如,相比较于太细粒度的数据读写服务,像GetCustomerAddress(获得客户地址)、GetCustomerName(获得客户名称)、UpdateCustomerName(更新客户名称)等等,架构师和共享服务团队更愿意采用部署企业级Customer(客户)服务的做法,用于处理更多粗粒度更新和接收数据视图,而把底层数据读写功能委托给并未开放给外部企业的应用级别服务。通过这种方式,像GetCustomerDemographics(获得客户统计数据)或者GetCustomerInformation(获取客户信息)之类的操作会返回一批客户数据,而不是单一的数据字段。

粒度不同自然是跟服务组件的作用范围和功能差异相关。对于微服务而言,服务组件的功能(服务作用范围)倾向于做得较小,有时通过一两个模块就可以完成。对于SOA,服务倾向于包含尽量多的业务功能,有时会作为子系统(例如,索赔处理引擎或者库存系统)来实现。不过,SOA通常依赖于多个服务完成单个服务请求,而微服务却并不这样。我会在下一章的“服务编排”一节中详细讨论这个问题。

无论使用微服务架构还是SOA,选择合适的粒度来设计服务并不是件容易的事情。服务粒度既影响性能也影响事务管理。服务的粒度太细时需要服务间通信来完成单一业务请求,从而导致大量的远程服务请求,占用掉宝贵的时间。假设访问某服务时花在远程访问协议上的时间是100毫秒,如图2-5所示,仅仅花在数据传输上的时间就达到600毫秒。如果将这些服务整合到一个服务中,将会将传输时间减少为200毫秒,这样总处理时间将会减少一半。
2-5.png

图2-5

服务粒度也会影响事务管理。这里我指的是传统的ACID事务,而不是前一章提到的BASE事务。如果远程服务的粒度太细,如图2-6上图所示,就不能使用一个事务工作单位来协调这些服务。然而,如果把这些服务组合进一个较大规模的远程服务中,如图2-6下图所示,这时你就可以用一个事务来协调这些服务,从而确保数据库更新可以在一个事务工作单位内完成。
2-6.png

图2-6

在处理服务粒度时,我发现从粗粒度服务开始会更容易一些。随着对服务的用法的了解逐渐增加,不妨再考虑如何对其进行拆分。如Sam Newman在 Building Microservices书中所说,“从少数几个大服务开始”。只需要观察事务问题和服务之间通信量,尤其是在微服务架构下,如果发生相关问题很可能说明服务可能粒度太细了。

粒度与模式选择

本章所描述的三个服务特性中,服务粒度在根据情况进行架构模式选择的过程中具有最重要的潜在影响。微服务中规模很小的、细粒度的服务概念使得这种架构模式能够提升软件开发生命周期中的各个方面,包括开发、测试、部署和维护。尽管采用粗粒度服务肯定会解决性能和事务问题,这一转变反过来肯定也会给开发、测试、部署和维护带来负面影响。如果发现服务规模从小变大,那么最好选择SOA模式而不是更为简单的微服务架构模式。不过,如果能够将应用的业务功能分解为很小的、互相独立的部分,微服务模式应该是更好的选择。

当比较微服务和SOA架构时,除了以上服务特性外,还有很多其他方面需要考虑。下一章中,我们会更多地从全局角度比较这些架构方面,包括每种模式下组件共享水平、服务编排与布置、使用中间件或简单API层以及如何访问远程服务等方面的不同。

比较架构特性

组件(component)是软件中的一个单位,具有定义良好的接口、定义良好的角色/责任集合。组件是架构的构成元素。对于基于服务的架构,这些构成元素通常被称为服务(或者服务组件)。不管组件带上什么标签,当创建一个架构时,你都需要决定组件如何被共享、组件间如何通信、多个组件如何被整合起来完成业务请求以及如何从远程服务用户的位置访问他们。

为这些问题做出决定并不是件容易的事情,这就是为什么需要了解架构模式的原因。每种架构模式都有独特的拓扑结构用来定义架构的形状和一般属性,包括组件之间如何相互关联、如何通信、如何协作完成业务请求等等。通过分析架构模式的拓扑结构,你可以更好地决定哪种模式更适合你要开发的应用。
本章我主要分析微服务和SOA之间在总体结构拓扑、架构模式的决定性属性等方面的差异。值得一提的是,这两种架构模式之间的差异主要表现在服务组件的共享程度、服务组件的通信水平以及远程服务组件的典型访问方式。本章也会讨论在SOA架构模式中常见的消息中间件与微服务架构模式中常见的可选API层之间的不同。

组件共享

在组件共享方面,微服务与SOA是有本质上的差异的。SOA是建立在“能共享就共享(share-as-much-as-possible)”的架构风格之上的,而微服务是建立在“能不共享就不共享(share-as-little-as-possible)”的架构风格之上。本节中我会分析这两种概念与其对应的微服务和SOA之间的不同。

组件共享是SOA中的核心概念之一。事实上,组件共享也是企业服务的重中之重。例如,考虑一个大型零售公司,如图3-1所示,有很多应用跟订单处理有关,例如客户管理系统、库存管理系统和订单执行系统。所有这些系统都有自己的Order(订单)服务版本。本例中,假设更新订单的流程需要特殊业务逻辑,这意味着该特殊业务逻辑需要在企业的多个应用之间复制,并且需要应用之间额外的验证和协调。图3-1中还需要注意的是,每个系统都有各自的数据库,因此每个系统中都可能对一个订单有自己的表现形式。
3-1.png

图3-1

SOA期望通过企业级的共享服务(企业服务)来解决这个问题。回到前述的零售案例,假设现在新生成了一个集中共享的Order企业服务,如图3-2所示,更新订单所关联的同一业务逻辑可以被每个应用共享。
3-3.png

图3-2

注意在图3-2中,尽管Order服务是共享的,它仍然在访问三个不同数据库。这在SOA架构采用“能共享就共享”风格时是一个关键概念。Order服务足够智能,知道对不同系统而言需要从哪些数据库检索数据并更新订单数据,同时在三个系统之间同步数据。也就是说,订单并不是通过一个而是三个数据库的组合来完成表述。

尽管“能共享就共享”的架构解决了与业务功能重复相关的问题,但是却带来了组件间耦合过于紧密的问题,增加了与变更相关的整体风险。例如,假设对图3-2中的Order服务做了某个变更,因为Order服务是一个企业服务,对全公司范围可见,很难测试该服务的全部使用场景以确保这次变更不会影响到企业其他部门。

微服务架构,是基于“能不共享就不共享”的理念的,利用了一个领域驱动设计中的概念限定语境(bounded context)。架构上来看,限定语境是指组件(或者这里的服务)与其所关联的数据紧密耦合成一个封闭的单元,与外界有着最小的依赖。这种方式设计的服务组件本质上都是自包含的,只开放定义良好的接口和定义良好的合约。

切合实际地说,即使在微服务架构中也总有某些服务(例如,基础服务)是共享的。然而,SOA架构总是倾向于更大化组件共享,而微服务架构则通过限定语境的概念最小化共享。一种实现限定语境并将依赖最小化的极端方式是违反“不要重复自身(Don’t Repeat Yourself,DRY”原则,将公共性的功能在多个服务中复制,以获得全局上相互独立的效果。另外一个方式是将相对静态的模块编译到共享库中,以便服务组件可以在编译时或者运行时将它们链入。我的好朋友和同事Neal Ford对此有些不同的看法,他认为微服务架构是一种什么都不共享的架构,只有两个例外:一是如何实现服务之间的整合,而是如何夯实基础服务以确保(其它服务)在工程实现上的一致性。

发挥限定语境的概念有很多优点。首先,因为不存在依赖模块,服务的维护变得容易很多,服务的变更和演进都可以相互独立地进行;其次,部署也变得容易很多,需要部署的代码变少,对一个模块的变更也不太可能影响到应用的其它部分。用这种方式生成的应用更加稳定,很少会受到服务变更的副作用影响。

服务编制与编排

服务调配(orchestration)与编排(choreography)之间的区别并总不是很明显。本节中会解析两者之间的区别,以及这两种服务通信概念是如何在SOA和微服务中使用的。

术语服务调配指的是多个服务通过一个集中式调节者进行协调。这里的调节者可以是服务的客户,也可以是一个集成枢纽(Mule、Camel、Spring Integration等)。图3-3展示了服务调配的概念。
3-2.png

图3-3

理解服务调配的最容易的方法就是想象一个管弦乐队的构成。若干音乐家们在不同乐章中使用不同的乐器进行演奏,但他们的演奏都由乐队指挥一个人协调。同样地,服务调配中的协调者组件就扮演着乐队指挥的角色,负责协调所有服务调用以完成业务事务。

服务编排指的则是没有中央协调者的情况下多个服务调用之间的协调。讨论服务编排时,有时会涉及到术语“服务间通信(inter-service communication)”。通过服务编排,某个服务调用另外一个服务,而后者又调用其他服务等等,形成一个服务链(service chaining)。图3-4所展示的就是这个概念。
3-4.png

图3-4

理解服务编排的一种方法就是想象一个在台上表演的舞蹈团。所有舞者都相互同步地移动身位,但是没有人指挥或者指导每个舞者。舞蹈是通过每一个舞者与其他人共同协作实现的,而音乐会则是通过某个指挥的协调来实现的。

微服务架构更适合采用服务编排而不是服务调配,主要是因为这种架构的拓扑结构中没有一个集中式的中间件组件。图3-5所展示的全局架构拓扑中包含两类主要组件:服务组件和(是可选的)非智能化的API层。(我会在下一节讨论API层及其扮演的角色)。从实现的角度来看,架构中还可以有其他组件,例如服务注册和发现组件、服务监控组件和服务部署管理器等等,但是就微服务结构模式的服务分类法而言,它们更应该被视作基础服务。
3-5.png

图3-5

因为微服务架构是一种“能不共享就不共享”的架构,你应该尽量减少服务编排的数量,尽可能让功能服务与基础服务进行交互。如前一章所述,如果发现在功能服务之间需要进行大量的服务编排工作,很有可能是因为你的服务粒度太细了。

在微服务架构中执行太多的服务编排可能会导致出耦合度(efferent coupling)太高的问题。出耦合度这个术语指的是某个组件为了完成某个业务请求在多大程度上需要依赖其它组件。考虑图3-6中的例子,展示了处理订单请求时所需要的三个服务:validate order(验证订单),place order(下单)和notify customer(通知客户)。架构上来说,这个业务请求有很高的出耦合度,而这正是架构师们在采用大多数微服务架构中尽量避免的。
3-6.png

图3-6

服务编排中中发生的服务耦合问题可能引起性能不佳以及应用不稳定的问题。正如前一章所讨论的,因为微服务架构下的服务一般都是远程服务,使用服务编排进行服务协调时所产生的每个服务调用都会延长请求的响应时间,因为远程访问协议的通信传输时间总是存在的。另外,为同一业务请求而对多个服务进行协调时,调用链上的任何一个服务都可能出现不可用或者不响应的状况,从而会导致应用可靠性和稳定性都会下降。

为解决微服务架构中对多个功能服务进行编排的问题,可以考虑将多个细粒度服务合并成一个粗粒度服务。如果某个细粒度服务刚好被另外几个服务共享,那么可以保持它独立。或者,也可以根据此功能的规模及自身属性,有意违反DRY原则,将这一共享功能复制到每个粗粒度服务中。

图3-7中展示的是如何通过将三个细粒度服务整合为一个粗粒度服务,消除服务编排从而解决与服务编排相关的问题。这里所指的问题具体包括:首先,因为减少了远程调用,整体的性能会得到提升;第二,因为服务可用性的问题出现得更少,整体的稳定性得以改进;最后,因为不再需要远程服务合约,总体上的部署和维护得以简化。
3-7.png

图3-7

SOA,作为一种“能共享就共享”的架构,同时依靠服务调配和编排来完成业务请求的处理。如图3-8所示,SOA中的消息中间件,通过调用多个企业服务来处理同一业务服务请求,从而完成服务调配。一旦加入到企业服务中,服务编制组件可以用于调用应用服务或者基础服务来帮助完成特定业务请求。
3-8.png

图3-8

图3-8展示了SOA架构中使用服务编排的几种变化形式。例如,企业服务可能需要调用应用服务,而此应用服务又需要调用一个基础服务完成业务处理。另一种情况是,企业服务也可能只需要调用一个应用服务或者一个基础服务。还有可能业务逻辑直接自包含在企业服务中而不需要任何服务编排。

微服务和SOA在服务调配和编排方面的不同,进一步说明二种模式在架构特征方面存在诸多差异,包括性能、开发、测试和部署等等。因为SOA一般依赖多个服务(和服务类型)来完成同一业务请求,基于SOA架构构造的系统一般比微服务慢,而且需要更多时间和精力进行开发、测试、部署和维护。实际上,这也是让架构师慢慢从SOA转向更为简单和直接的微服务架构的部分原因。

中间件与API层

如果比较前一节中的图3-5和3-8,你就会注意到两种架构模式中都存在一个中间件组件来执行调度。然而,事情并不是这样的。微服务架构模式中通常包含一个称作“API层”的组件,而SOA中所包含的是消息中间件组件。本节中我们会比较这两种组件所扮演的角色和所提供的能力。

微服务模式不支持消息中间件(例如,集成枢纽或者企业服务总线)的概念,而是支持在所有服务的前端设置一个API层作为服务接入层。在客户和服务之间放置一个API层通常是个不错的主意,因为这个组件实质上构造了一个抽象层,使得客户不需要知道服务端的确切位置。同时也使得服务粒度的改变不会影响到服务客户。对服务粒度进行抽象的确需要在API层提供一定的智能和一定程度的调配能力,但这些问题可以慢慢通过重构解决,重要的是服务端可以根据需要演化,而不是要求服务的客户也经常做出变更。

例如,假设有个服务跟产品订单业务功能有关。我们发现它的粒度太粗了,想把它分解成两个粒度更细的服务,从而提高服务的伸缩能力并简化部署。如果没有API层来为实际的服务端提供抽象,使用该服务的每个客户都要做出变更,从调用一个服务转为调用两个服务。如果使用了API层,服务的客户端就不需要知道(甚至不在乎)同一请求现在将被分解成两个服务调用。

SOA依赖于其消息中间件来协调服务调用。使用消息中间件(我更愿意称为集成枢纽)可以获得一些在微服务架构风格中不存在的额外的架构特性,包括调解和路由、消息增强、消息转换和协议转换等。

调解和路由(mediation and routing)指的是架构基于特定业务或者用户请求来对服务进行定位和调用的能力。这种能力如图3-9所示。注意本图中对服务注册或服务发现组件的使用,以及对服务调配能力的利用。微服务和SOA都有这种能力,特别是服务注册或者服务发现模块所提供的能力。不过,服务调配在微服务中被严格控制甚至排除,而在SOA中却是经常用到的能力。
3-9.png

图3-9

消息增强(message enhancement)指的是架构在请求的数据部分到达服务之前之前对其进行修改、删除或者增加的能力。消息增强的例子包括改变日期格式、添加额外数值或者查询数据库进行数据转换,例如将统一安全鉴定程序委员会(Committee on Uniform Security Identification Procedures,CUSIP)代码转换为股票代码或者从后者转换为前者。微服务模式不提供这种能力,主要是因为其架构中不包含实现这一功能的中间件组件。SOA通过其消息中间件完全支持这种能力。

图3-10展示了这种能力。注意,图中客户所发送的是CUSIP代码(一种标准交易标识码)和格式为MM/DD/YY的日期数据,而服务期望接收的是证券交易所每日正式牌价表(Stock Exchange Daily Official List,SEDOL)代码(另外一种交易标识码)以及格式为YYYY.MM.DD的日期以及股票代码(假设是股票交易)。本例中,消息中间件将CUSIP代码(苹果公司是037833100)转化为SEDOL数(苹果公司是2046251),查询并添加代码(AAPL),将日期从04/23/15转化为2015.04.23。
3-10.png

图3-10

消息转换(message transformation)指的是架构将一种数据格式转换为另外一种格式的能力。例如,如图3-11所示,客户调用服务并以JSON格式发送数据,而服务期望接收的是Java对象。注意消息增强『译注:似应为消息转换』并不关心请求相关的数据本身,而只是关注数据格式的转换。微服务架构并不提供这种能力,而SOA架构则通过消息中间件提供这种功能。
3-11.png

图3-11

最后,协议转换(protocol transformation)所描述的是架构允许客户采用与服务端预期不匹配的协议来调用服务的能力。图3-12展示了这种能力。注意,图中服务客户采用REST进行通信,但是负责处理请求的服务要求建立RMI/IIOP链接(例如,Enterprise JavaBeans 3 [EJB3])和AMQP连接。微服务可以支持多种协议类型,但是服务的客户和服务必须采用同一通信协议。在SOA架构中,多个协议则可以根据需要混合使用。
3-12.png

图3-12

我会在下一章中详细讨论这些能力,因为它们跟微服务和SOA的架构能力比较更相关。

访问远程服务

由于微服务或者SOA架构中服务通常是远程访问的,这些架构模式都需要提供一种供客户访问远程服务的方式。就远程访问而言,两种架构模式的根本差别在于,微服务倾向于把REST作为首选远程访问协议,而SOA则没有这种限制。事实上,可以同时处理多种不同远程访问协议是SOA与微服务架构相较最关键的不同点之一。

微服务有一个基本原则是技术和架构选项非常受限,这也使得该架构模式比较简单。例如,大多数微服务架构都仅使用两种访问协议:REST和简单消息(JMS、MSMQ、AMQP等)协议。这并不是说,不能采用其他远程协议,例如SOAP或者.NET Remoting,而仅仅是说一般微服务都倾向于选用同质协议。换句话说,服务要么是基于REST的,要么基于消息的,要么是基于其他协议的,但是在同一应用或者系统内是很少会混合使用多种协议。一个例外也就是可能出现混合使用协议的状况是,依赖发布订阅(publish-and-subscribe)广播模式的服务可能是基于消息的,而其他非广播式服务都是基于REST的。

SOA则没有对架构模式中采用哪种远程访问协议进行限制。在下一章中可以看到,架构中的中间件组件能够支持任意数量的远程访问协议,并提供各种协议之间的转换。尽管如此,大部分SOA架构通常依赖于消息(例如JMS、AMQP、MSMQ)协议 SOAP作为主要的远程访问协议。根据SOA架构范围和规模,在异构服务之间使用六七种远程访问协议的情形也并不少见。

比较架构能力

上一章中我们讨论了架构模式如何帮助确定基本的架构特性。本章中,我们采用类似方法,集中讨论架构模式所描述的架构能力而不是架构特性。通过分析架构模式,你可以判定应用是否易伸缩、易维护和易扩展,以及是否相对地易于开发、测试和部署。

本章中,会对微服务和SOA的架构能力进行集中讨论,主要包括三个方面:每种架构模式所能支持的最大应用规模、使用每种架构模式可以集成的系统和组件类型以及架构模式支持合约解耦的能力。

应用范围

应用范围是指某种架构可以支持的应用的总体规模。例如,微内核或者管道架构更适合较小的应用或者子系统,而其他模式如事件驱动的架构则适合于大型、更复杂应用。那么在这个谱系中微服务和SOA模式适合哪种规模的应用呢?

SOA更适合大型、复杂的、企业级系统,一般都需要整合很多异构应用和服务。也比较适合有很多共享组件的应用,特别是包含全企业内共享组件的应用。因此,SOA更适合于大型保险公司,因为这类企业内部有很多异构的系统环境,并且需要在多个应用和系统之间共享很多公共服务,如:客户、索赔和策略等等。

然而,对于拥有很多定义良好的处理工作流的基于流程的应用(例如证券交易)而言,因为没有很多共享组件,用SOA实现起来比较困难。小的、基于web的应用也不太适合SOA因为不要大量的服务术语、不需要抽象层或消息中间件。

微服务模式更适合于小型的、功能划分明确的基于web的系统而不是大规模企业级系统。使之不适合大规模复杂业务应用环境的最主要原因之一就是架构中没有一个协调者(消息中间件)。适合微服务架构模式的另外一些应用的例子是共享组件很少以及以及可以划分为更小的操作模块的应用。

某些场合中你会发现微服务架构是业务开始时很好的架构选择。随着业务成长变得成熟时,会开始需要对复杂请求进行转换的能力、实时复杂调配的能力和整合异构系统的能力。这时,你很可能会用SOA架构模式替代初始的微服务架构。当然,反之亦然。你也可能最开始设计的是复杂的、大规模的SOA架构,在后来意识到其实并不需要SOA架构的所有的强大能力。这时候,你很可能又会希望从SOA架构迁移到微服务,以简化系统架构。

异构互操作

异构可互操作性(heterogeneous interoperability)指的是与用不同语言和平台实现的系统进行集成的能力。例如,可能出现的一种场景是对同一复杂业务请求的处理需要协调一个基于Java应用,一个.NET应用和一个客户信息控制系统(Customer Information Control System,CICS)的COBOL程序。其他例子还有,比如用.NET平台实现的一个交易系统需要访问一个AS400平台完成股票交易的合规检查;某个基于Java的零售商店需要与一个大型的第三方.NET数据仓库系统相集成。

这些例子在大公司中随处可见。许多银行与保险系统仍然有大量后台核心处理采用COBOL大机应用,而这些应用需要被现代的基于Web的平台访问。整合多个异构系统和服务的能力微服务架构与SOA相比稍显薄弱的地方之一。

微服务架构风格试图通过减少服务集成的选项来简化架构模式和相关实现。REST和简单消息机制是最长用到的两个主要远程访问协议。SOA则对异构协议的使用没有约束甚或通过消息中间件来促进了异构协议的多样化。

微服务架构风格支持协议感知异构互操作(protocol-aware heterogeneous interoperability)。如果具备了协议感知的异构互操作性,就意味着架构可以支持多种远程访问协议,但是特定客户和与所访问的服务之间必须使用同一协议(例如,REST)。如图4-1所示,事实上,了解服务客户与服务之间所采用的远程访问协议并不意味着就了解任何一方是如何实现的,也不意味着双方在实现上要保持一致。例如,通过REST,服务客户可以很容易地在.NET上用C#实现出来;而服务自身则可以通过Java实现。不过,对于微服务而言,客户端与服务端在协议上必须一致,因为二者之间没有中间件组件进行协议转换。
4-1.png

图4-1

SOA也支持协议感知的异构互操作能力,但是它支持得更进一步,可以做到协议无关的异构互操作能力。具备协议无关的异构互操作能力意味着服务的消费者不但不关心服务端如何实现而且也不关心服务端在采用哪种协议进行侦听。例如,如图4-2所示,在.NET平台上用C#实现的某个服务客户端可以使用REST调用对应的服务,但是服务(本例中是EJB3 Bean)只能使用RMI通信。能够将客户协议转移为服务协议的能力称作叫协议转换,是通过使用中间件组件来实现的。再重复一次,因为微服务架构没有消息中间件组件的概念,也就不支持协议无关的异构互操作能力。
4-2.png

图4-2

如果你发现自己所处的是异构环境,需要对多种使用不同协议的系统或者服务进行整合,那么很可能需要采用SOA架构而不是微服务架构。反过来,如果所有服务都可以通过同样远程访问协议(例如,REST)提供给客户访问,那么微服务应该是正确的选择。不论哪种情况,在选择架构模式之前,都需要充分了解互操作性需求。

合约解耦

合约解耦(contract decoupling)可以认为是抽象的最高境界。合约解耦的本质含义是允许服务客户用不同于服务所期望的消息格式与之通信。

合约解耦是一种强大的能力,能够为服务客户和服务之间提供最大程度的解耦。这种能力允许服务和服务客户之间相互独立地演化,同时仍然在彼此之间维护着合约。这种能力也使得服务客户有能力通过客户驱动的合约来推动合约变更,从而在服务客户和服务之间建立一种更加密切的合作关系。

合约解耦的形式主要有两种:消息转换和消息增强。消息转换只关注消息格式而不是请求数据本身。例如,一个服务可能要求消息请求以XML作为输入格式,但是某个服务客户决定发送JSON数据。这种转换工作比较直接,可以使用大多数开源的集成枢纽软件来处理,包括Apache Camel、Mule和Spring Integration等等。

当服务客户所发送的数据跟服务所期望的不同时,事情就会变得比较麻烦。实际合约数据中的失配可以通过消息增强能力来解决。消息转化关心的是请求数据的格式,消息增强则关注的是数据本身。消息增强能力允许组件(一般是中间件组件)添加或者改变请求数据,从而使得服务客户所发送的数据复合服务的期望。

考虑这样一个场景,客户为执行简单股票交易以JSON对象的格式发送数据。服务客户通过发送客户ID、CUSIP代码来指定对哪只股票执行操作、要操作多少股,最后是MM/DD/YY格式的交易日期。
{"trade": {
"cust_id": "12345",
"cusip": "037833100",
"shares": "1000",
"trade_dt": "10/12/15"
}}   

而服务本身期望接收的是XML格式数据,其中包括账户号、股票代码、交易股数以及格式为YYYY.MM.DD的日期。
<trade>
<acct>321109</acct>
<symbol>AAPL</symbol>
<shares>1000</shares>
<date>2015-10-12</date>
</trade>

如果客户和服务之间在合约格式上出现分歧,一般需要依靠消息中间件或者定制的客户适配器来执行必要数据转换以及数据查询功能使得不同合约之间能够平滑工作。图4-3给出了一个这样的例子。数据库或者缓存查询可以基于customer ID得到账户号,基于CUSIP代码的股票号码。日期可以被转换成不同格式,交易股数则可以不做转换地拷贝到新的数据结构。通过转换,可以允许客户采用与服务不同的协约,当发生合约变更发生时,消息中间件可以屏蔽这些差异。
4-3.png

图4-3

合约解耦显然有一些使用上的局限。如果服务所需数据无法从客户所发送数据转换获得也无法从其它数据源获得,服务调用只能返回失败,因为服务合约无法得到满足。幸运的是,服务客户与服务之间的协议分歧大多数时候可以通过查询能力和基本的转换(例如日期,时间和数字域)来予以弥补。

现在IT业界碰到 一个问题是如何防止技术(IT部门)驱动业务。无论是升级一个大规模子系统的软件版本还是替换财务或客户管理系统,都可以通过合约解耦来实现接口和合约抽象,进而使得IT部门的技术变更不会影响到企业的业务应用。之前的股票交易场景是一个很好的例子。将使用CUSIP代码的交易平台替换为使用SEDOL代码的平台并不意味着企业的所有业务应用都需要改成使用SEDOL代码。

不幸的是,微服务在这一架构上又输给了SOA。微服务架构不支持合约解耦,而合约解耦是SOA架构所提供的主要能力之一。如果你自己的架构中需要这种层次的抽象化,那么最好为自己的应用或系统选择SOA解决方案而不是微服务。

总结

微服务架构模式是IT行业正在升起的明星。尽管微服务模式解决了大规模单体式应用和复杂SOA架构下的很多问题,但是它的确也缺少某些SOA提供的核心能力,包括合约解耦和协议无关的异构互操作性。

需要记住的一个基础概念是,微服务架构是“能不共享则不共享”的架构模式,非常强调限定语境,而SOA是“能共享则共享”的架构模式,侧重抽象和业务功能的重用。通过理解这一基本概念以及微服务与SOA的其它特点、能力与不足,就可以在做架构选择时有更明确的判定标准。

如果了解更多微服务和SOA以及分布式架构,可以参看Neal Ford和Mark Richards的 Service-Based Architectures: Structure, Engineering Practices, and Migration

如果想深入了解微服务,建议参考Sam Newman的书: Building Microservices(O'Reilly)。

最后,如果想了解微服务和SOA等基于服务的架构中所涉及的消息技术,可以参看 Enterprise Messaging: JMS 1.1 and JMS 2.0 Fundamentals (O'Reilly video)和 Enterprise Messaging: Advanced Topics and Spring JMS(O'Reilly video)。

原文链接:Microservices vs. service-oriented architecture(翻译:杨峰 审校:滕启明)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值