微服务策略
在了解了传统软件架构和微服务的基本概念后,在实际项⽬开发中应该如何进⾏微服务的拆分呢?
有些观点认为,所有的服务都可以看作是微服务,根本就不存在微服务这种概念。这种观点显然是偏激的。如果服务拆分得不够优秀就会存在⼤量的分布式事务,⽐如⼀个订单产品信息的更新需要和其他诸如库存、运输等多个事务操作保持⼀致,那可能导致⽆法达成微服务简单开发的⽬标,这种毫⽆策略的服务开发不仅不是好的服务,也不能称为微服务。
微服务架构风格
微服务有两个⾮常重要的风格:⼀是每个服务都拥有独⽴的数据库;⼆是系统基于API的模块化。
每个服务都拥有独⽴的数据库
与单体应⽤不同,为了满⾜在开发和部署上的简易性,每个微服务都对应⼀个数据库表,这种⽅式让整个系统以松耦合的⽅式进⾏整合。
习惯了单体应⽤开发的⼯程师应该很熟悉数据访问层和业务层及API路由层分离的⽅式。这种⽅式就像滚雪球⼀样,随着应⽤的增多,应⽤会越来越⼤,因此难以维护,修改⼀个API的功能很有可能会影响其他
API,⽽微服务则在开发⼤型应⽤及多⼈协作的场景中有着明显优势。当某个API的表出现死锁等情况时,也不会影响其他API。
每个服务都拥有独⽴的数据库遵循了SRP(Single Repository Principle)原则。分布式和微服务的关系
基于API的模块化
⼀个⼤型的项⽬中,为了⽅便维护和开发协作,都会进⾏模块化切分。
微服务的模块化与传统应⽤的模块化是不同的。⽽传统应⽤的模块化主要是分包,通过包的安装可以调⽤新的⽅法、函数等。⽽微服务的模块化则是通过API进⾏的,其他服务⽆法直接调⽤被封装的⽅法,只能通过API访问。
通过API进⾏模块化可以避免随着应⽤的增⼤⽽导致内部关系复杂。API这种天然的切分则会让微服务的运维和新增功能更为简单。
微服务化进程问题
微服务的通信
微服务是分布式系统中的⼀种,所以必须设计好进程间通信(IPC)。进程间通信是整个微服务设计中⾮常复杂的⼀部分,可以细分为以下⼏个⽅⾯:
● 通信风格:是使⽤消息通信,还是远程调⽤,⼜或者是领域特定,这要结合语⾔特点和项⽬需求来定。
● 服务发现:在微服务的实现过程中,特别是微服务数量特别多的情况下,客户端如何发现具体的服务实例请求地址。
● 可靠性:服务不可⽤的情况发⽣时,如何确保服务之间的通信是可靠的。
● 事务性消息:如何将业务上的⼀个事件,⽐如消息发送,与存放业务数据的数据库表的事务进⾏集成。
● 外部API:客户端如何与微服务进⾏通信。
事务管理的⼀致性
为了保持松耦合,⼯程师们总是让每个微服务使⽤⾃⼰独⽴的数据库。这样做虽然有其优点,但是同时也带来了很⼤的难题。⽐如在传统单体应⽤中可以把对数据库的⼏步操作放在同⼀个事务中,⽽现在就不能使⽤这种⽅式了,进⽽导致很多业务处理难以仅通过组合多步操作来完成。
⾯临的难题
要理解传统的数据库,⾸页要理解ACID,ACID对应Atomicity、Consistency、Isolation、Durability四个原则。
● Atomicity:原⼦性表明数据库修改必须遵循“全部或全⽆”规则。每个事务都被称为“原⼦的”。如果事务的⼀部分失败,则整个事务都会失败。⽆论是DBMS,还是操作系统或硬件故障,数据库管理系统都必须保持事务的原⼦性,这⼀点⾄关重要。
● Consistency:⼀致性规定仅将有效数据写⼊数据库。如果由于某种原因执⾏的事务违反了数据库的⼀致性规则,则将回滚整个事务,并将数据库恢复到与那些规则⼀致的状态。如果事务成功执⾏,它则将使数据库从与规则⼀致的⼀种状态转移到也与规则⼀致的另⼀种状态。
● Isolation:隔离要求同时发⽣的多个事务不影响彼此的执⾏。例如,如果Joe在Mary发出事务的同时对数据库发出事务,则两个事务都应以隔离的⽅式在数据库上进⾏操作。数据库应该在执⾏Mary之前执⾏Joe的全部交易,反之亦然。这样可以防⽌Joe的交易被读取作为Mary交易的⼀部分⽽产⽣中间数据,这些中间数据最终不会提交给数据库。请注意,隔离属性不能确保哪个事务将⾸先执⾏,只是确保事务不会相互⼲扰。
● Durability:持久性确保了提交给数据库的任何事务都不会丢失。可通过使⽤数据库备份和事务⽇志来确保持久性,即使随后发⽣任何软件或硬件故障,这些⽇志也有助于恢复已提交的事务。
不过⾮常遗憾,这些特征都只有在⼀个数据库内并且允许跨表操作时才可以满⾜。可是微服务是部署在不同服务器上的,它们各⾃⼜有⾃⼰的数据库,所以要⽤微服务框架下的分布式事务管理。
对于微服务开发,强烈建议开发⼈员⽤单⼀存储库原则(SRP),这意味着每个微服务都要维护⾃⼰的数据库,并且任何服务都不应直接访问其他服务的数据库。没有直接简单的⽅法来跨多个数据库维护ACID原则。这是微服务中交易管理⾯临的真正挑战。
SRP的折衷
尽管微服务准则强烈建议为每个微服务使⽤单独的数据库服务器(SRP),但作为设计⼈员,在微服务开发的早期阶段,出于实际原因,需要选⽤某些折衷⽅案。
折衷⽅案⼀般是把所有微服务要⽤到的表放在⼀个数据库内,这些表使⽤对应的前缀或后缀进⾏区分,但是表与表之间不允许有任何的主外键关系。
这样就可以把事务从微服务⾥⾯提取出来,在调⽤⼏个微服务以后再根据是否报错统⼀进⾏commit或者rollback。
图 SRP的折衷⽅案
这种⽅式仍然不允许跨数据库表的直接调⽤,只能通过服务在同⼀个库的不同表之间进⾏数据的操作。
不过因为底层是⼀个数据库,所以继续使⽤ACID特征成为可能,很多框架会在事务层⾯提供辅助功能,⽐较著名的是Java的Spring框架。
只是这种折衷⽅案仅仅适⽤于项⽬开始阶段,它可以在服务压⼒不⼤的情况下作为过渡⽅案。⼀旦请求数增⼤,外部请求和服务之间的内部请求全部都会压在⼀个数据库上,这会给⽹络和数据库服务器都造成很⼤的压⼒。这时可以考虑数据的复制,把⼀个库复制到多个节点,但这⼜会牵扯出另⼀个问题,即最终的数据⼀致性问题。
微服务中处理事务的⼏种⽅式
在⼀段时间内,微服务社区使⽤了不同的⽅式来处理跨微服务的事务。⼀种⽅法是设计级别解决事务管理,⽽另⼀种⽅法则是设计编码级别来解决。
⽤来管理事务的⽅法或原则有多种,较为常⽤的有三种。这三种⽅式都是贴近实战的,可以在给定的微服务环境中使⽤以下⼀种或所有⽅法(原则)。在给定的环境中,两个微服务可以使⽤⼀种⽅法,⽽其他微服务可以遵循不同的⽅法进⾏事务管理。
1. 避免使⽤跨服务的事务。
2. 两步提交法,分别是XA标准和REST-AT标准。
3. 最终的⼀致性和补偿⽅案。
避免跨微服务的事务
在设计程序时应该把避免跨微服务的事务作为⼀个重要的要素处理,在不影响其他要素的情况下,尽可能减少跨微服务的事务。
⽐如可以通过合理的模型设计,让订单、订单⾏项⽬在⼀个服务内。这种设计⽅式可以让订单的变更和订单⾏项⽬的变更都在⼀个微服务内完成,避免了跨微服务事务的出现。
当然,不可能没有跨微服务的事务,因为那样就⼜回到了单体应⽤。
⽐如,在电商⽹站中的⼀个下单动作,涉及库存查询、扣款两个服务。如果为了减少跨微服务的事务⽽把库存查询和扣款两个服务合并到⼀起,就会让库存和费⽤相关的表放在⼀个数据库中。这种情况短期来看影响不⼤,但是当客户、运输等服务也进⼊⼀个服务、⼀个底层数据库时,整个微服务就⼜回到了单体应⽤。
所以说,微服务中不可能没有跨微服务的事务。
微服务架构设计追求的是⼀种平衡,同等情况下,跨微服务事务越少越好。
基于XA协议的两阶段提交协议
两阶段提交协议(Tow-Phase Commit Protocol)是⾮常成熟和传统的解决分布式事务的⽅案。先来举⼀个例⼦说明什么是两阶段提交,假如你要在明天中午组织⼀次团队聚餐,按照两阶段协议应该这么做。
第⼀阶段,你作为“协调者”,给A和B(参与者、节点)发邮件邀请,告知明天中午聚餐的具体时间和地址。
第⼆阶段,如果A和B都回复确认参加,那么聚餐如期举⾏。如果A或者B其中⼀⼈回答说“另有安排,⽆法参加”,你需要⽴即通知另⼀位同事明天聚餐⽆法举⾏。
仔细看这个简单的事情,其实当中有各种可能性。如果A或B都没有看邮件,你是不是要⼀直等呢?如果A早早确认了,已经推掉了其他安排,⽽B却在很晚才回复不能参加呢?
这就是两阶段提交协议的弊病,所以后来业界⼜引⼊了三阶段提交协议来解决该类问题。
两阶段提交协议在主流开发语⾔平台、数据库产品中都有⼴泛应⽤和实现,下⾯来介绍⼀下XOpen组织提供的DTP模型。
从上图可知,XA规范中分布式事务由AP、RM、TM组成。
● 应⽤程序(Application Program, AP):AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源。
● 资源管理器(Resource Manager, RM):RM管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含数据库、⽂件系统、打印机服务器等。
● 事务管理器(Transaction Manager, TM):负责管理全局事务,分配事务唯⼀标识,监控事务的执⾏进度,并负责事务的提交、回滚、失败恢复等操作。
XA规范主要规定了RM与TM之间的交互,通过下图来看⼀下XA规范中定义的RM和TM交互的接⼝。XA规范本质上也是借助两阶段提交协
议来实现分布式事务的。
XA事务使⽤了两个事务ID:每个XA资源的全局事务ID和本地事务ID(xid)。在两阶段协议(准备)的第⼀
阶段,事务管理器通过在资源上调⽤prepare(xid)⽅法来准备参与该事务的每个资源。资源可以以OK或ABORT投票的形式进⾏响应。在从每个资源中获得OK票后,管理器决定执⾏提交(xid)操作(提交阶段)。如果XA资源发送ABORT,则在每个资源上调⽤end(xid)⽅法进⾏回滚。这⾥有多种情况,例如,⼀个节点可能在⽤OK响应之后但在可以提交之前重新启动。
这种分布式事务解决⽅式的实现不算复杂,即便是不借助第三⽅框架和包也可以⾃⼰编码实现。不过这种⽅案的缺点是性能不够好,当微服务的负载⽐较⼤的时候往往造成性能问题。
最终⼀致性和补偿
为了加强对⽐,在介绍最终⼀致性(Eventual Consistency)之前,先来介绍下强⼀致性(Strong Consistency)。
强⼀致性模型是最严格的,在此模型中,对数据项X的任何读取都将返回与X的最新写⼊结果相对应的值,如下图所⽰。
图中的两个进程虽然是同时开始的,但是P1先完成了写操作,所以P2就可以读取到刚写⼊的a。
这种模式在微服务框架的分布式事务下是不可能实现的。然⽽⼜不可能放弃⼀致性,所以退⽽求其次,寻实现最终⼀致性的可⾏性⽅案。
最终⼀致性是弱⼀致性的⼀种特殊形式,在这种形式中,存储系统保证最终所有访问将在写⼊静默(没有更新)时返回最后更新的值。在不发⽣故障的情况下,可以计算出不⼀致窗⼝的最⼤数量(查看诸如⽹络延迟和负载之类的信息)。
此类系统的⼀个⽰例是域名系统(DNS)。该系统中名称的更新是按照设定的模式分配的,因此,在初始更新阶段,并⾮所有的节点都具有最新信息。少数节点托管具有⽣存时间(TTL, Time-To-Live)的缓存,并且它们将在缓存过期后获得最新更新。
为了实现最终⼀致性,在不⼀致窗⼝内确定担保时,需要考虑⼤量模型。
消息⼀致性⽅案通过消息中间件保证上、下游应⽤数据操作的⼀致性。基本思路是将本地操作和发送消息放在⼀个事务中,保证本地操作和消息发送要么两者都成功,要么两者都失败。下游应⽤向消息系统订阅该消息,收到消息后执⾏相应的操作。
另外就是存储的最终⼀致性⽅案,为了达到这个效果,就需要对⼀个数据存储做多个副本,在Go语⾔
的微服务实战中可以考虑使⽤CockroachDB数据库,这是⼀款⽤Go语⾔开发的开源分布式数据库。接下来具体介绍通过数据存储的最终⼀致性来解决微服务分布式事务的⽅案。
分布式系统中的数据⼀致性与可⽤性是本章⼀直在讨论的问题,这⾥将其和NoSQL数据库结合起来。如今,传统的ACID关系数据库有被NoSQL数据库取代的趋势,后者基于BASE模型中的最终⼀致性原理进⾏操作。BASE与有界上下⽂(Bounded Context)相结合经常构成分布式微服务体系结构中持久性的基础。
在NoSQL⾥,BASE代表NoSQL的如下三个特征。
● Basically Available:基本可⽤。
● Soft-state:软状态或柔性连接,其实可理解为⽆连接。
● Eventual Consistency:最终⼀致性。
有界的上下⽂和最终的⼀致性可以稍微简化⼀下。
有界上下⽂是DDD的中⼼模式,其在设计微服务体系结构时⾮常有⽤。例如,如果有⼀个“account”微服务和⼀个“order”微服务,则它们应该在单独的数据库中拥有⾃⼰的数据(例如“account”和“order”),
⽽彼此之间没有传统数据库中的外键约束。每个微服务全权负责从⾃⼰的域中写⼊和读取数据。如果“order”微服务需要了解给定“order”拥有的“account”,则“order”微服务必须
向“account”微服务询问账户数据,在任何情况下,“order”微服务都可能不会直接访问或写⼊“account”微服务的底层数据库表中。
最终⼀致性会涉及如下事件。在通过存储解决最终⼀致性时,主要通过数据复制机制来完成,其中给定的数据写⼊最终将在整个分布式存储系统中进⾏复制,因此任何给定的读取都将产⽣最新版本的数据。也可以认为它是有界上下⽂模式的必要条件,例如对于外部查看者来说看似“原⼦”的“业务事务”(business transaction)写⼊,在许多微服务⾥可能会涉及跨多个有界上下⽂的数据写⼊,⽽没有任何分布式机制来保证全局ACID交易。取⽽代之的是,最终所有涉及的微服务都将执⾏其写⼊操作,因此,从业务事务的⾓度来看,整个分布式系统的状态都⼀致。
通过存储来解决微服务的分布式事务时,可以考虑分布式数据库,然后使⽤之前介绍的SRP折衷⽅案,让每个微服务对应⼀个分布式数据库的表,这样就可以达到“最终⼀致”的效果,来看下图中给出的⽰例。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论