微服务策略

在了解了传统软件架构和微服务的基本概念后,在实际项目开发中应该如何进行微服务的拆分呢?

有些观点认为,所有的服务都可以看作是微服务,根本就不存在微服务这种概念。这种观点显然是偏激的。如果服务拆分得不够优秀就会存在大量的分布式事务,比如一个订单产品信息的更新需要和其他诸如库存、运输等多个事务操作保持一致,那可能导致无法达成微服务简单开发的目标,这种毫无策略的服务开发不仅不是好的服务,也不能称为微服务。

微服务架构风格

微服务有两个非常重要的风格:一是每个服务都拥有独立的数据库;二是系统基于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折衷方案,让每个微服务对应一个分布式数据库的表,这样就可以达到“最终一致”的效果,来看下图中给出的示例。

通过存储达到最终一致性

Image Service对应一个节点,Data Service对应另一个节点。下面的Image表和Account表其实同时放在多个节点上,至少两个以上都有备份。这时就不需要在Image Service和Data Service之间通过服务来操作底层数据存储了,分布式数据库会自动进行最终一致性处理,而且也不会增加网络资源消耗。关于负载问题,分布式数据库可以通过增加节点尽可能地解决,当然还是会存在瓶颈。

Saga模式

Saga模式最早是在1987年的一篇论文中提出的,而且一直在分布式事务处理领域非常著名,现在由于微服务风格的流行更是经常被提及。

Saga模式介绍

Saga模式的核心理念是避免使用长期持有锁(如之前介绍的两阶段提交)的长事务,而应该将事务切分为一组按序依次提交的短事务,Saga模式满足ACD(原子性、一致性、持久性)特征。

虽然Saga模式已经接近满足ACID,但是毕竟还是差了隔离性。这就意味着Saga可以从未完成的事务中读取和写入数据,同时可能引起各种隔离性异常。

下面通过示例来介绍一下Saga模式,比如在一个电商场景中有四个微服务:订购(OrderService)、库存(StockService)、支付(PaymentService)和交付(DeliveryService)。这四个动作在微服务中无法像单体应用一样使用一个数据库的ACID特征在一个事务内完成,但是这四个服务又必须保证一致成功或一致失败,这就需要使用分布式事务管理,此时,Saga模式便有了用武之地。

Saga由一系列的子事务“Ti”组成,每个Ti都有对应的补偿“Ci”,当Ti出现问题时Ci用于处理Ti执行带来的问题。可以通过下面的两个公式理解Saga模式。

T = T1 T2 … Tn T = TCT

在对应的例子,该事务(可以理解为“业务事务”)可以拆分为如下5个有序的子事务,而且每个子事务都是可以补偿的,如下图所示,分步骤业务事务。

具体到Saga模式的事务管理,有如下两种实现方式。
● 事件/编排(Choreography):一种分布式实现方式,通过事件驱动的方式进行事务协调,每个服务都需要将自己的事件通知其他服务,同时需要一直监听其他服务的事件并决定如何应对。
● 编配/协调/控制(Orchestrator):有一个集中的服务触发器,跟踪Saga模式中的所有步骤。

编排模式
在编排模式(Choreography)中,每当一个服务执行一个事务时都会发布一个事件,该事件会被一个或多个服务监听,它们会根据监听到的事件决定是否执行自己的本地事务,同样,这些事务也会在执行事务时发布事件。当最后一个事务执行完本地事务并不再发出事件时意味着整个分布式事务执行结束(如下图所示)。

整个分布式事务从OrderService开始,第一个事件是ORDER_CREATED_EVENT,意味着订单保存完成,后面的PaymentService会监听这个服务,然后发出事件2,依次往后执行,直到OrderService监听到事件4以后不再发出新的事件,订单会修改为完成状态,整个分布式事务执行完成。

如果订单要跟踪整个状态的变化,可以让OrderService监听事件2和3,这样就可以在付款完成和出库完成后更新订单的状态。

不过这仅仅完成了一个分布式事务拆分为多个短事务这一步骤,Saga模式的另一个要求是T=TCT,也就是说当出现事务不成功时,可以重新来一次。来看一下编排模式下如何完成这一要求。

事务失败时的回滚也是通过事件监听完成的,上图只是画出了短事务成功时的事件。事实上,当某个短事务失败时也可以发出一个对应的失败事件,其他服务监听到后就会采取对应的措施。

举例,比如StockService执行失败抛出一个Stock_ERR_EVENT事件,这时PaymentService会监听这个事件然后进行退款,OrderService监听到这个事件则会修改自身的状态。这些操作完成后,如果是网络问题,用户可以重新下单开始新一轮的分布式事务,不会有影响。

当然,在具体的实现阶段,需要对所有的事务都统一编码,并给出一个global ID,这样在一个事件被接收后,就可以立即知道这个事务是成功还是失败。

最后看一下这种模式的优缺点。
● 优点:松耦合、简单、容易实现,只需要设计好何时发送事件和哪些服务监听哪些事件即可。
● 缺点:一个分布式事务如果由几十个短事务组成,恐怕这种方式难以开发、难以维护、难以测试,有可能出现循环依赖。

接下来看一下另一种模式—编配模式。

编配模式

编配(Orchestrator)(命令/协调)模式需要定义一个新的服务,这个新服务就是一个集中的事务跟踪处理服务。Saga的编配器orchestrator以命令/回复的方式与所有服务进行通信,每个服务都会根据编配器的指令进行操作。

还是以电商事务为例,编配模式的分布式事务大概如下图所示。

可以看到,整个流程都是由编配器(Order Saga Orchestrator)发起的,并且需要返回答复(reply)信息。如果任何一个答复错误或者没有答复,编配器还会向相关的服务发送新的命令进行回滚。

所以这种模式要求编配器必须知道分布式事务的流程及相应的每一步的回滚。

来看一下编配模式的优点:
● 这种集中编配的方式可以避免循环依赖。
● 编配器之间的通信就是命令/回复,非常简单。
● 测试、维护都更简单。
● 即便短事务增加,也非常简单,不会出现指数级增加复杂的问题。

从以上优点可以看出,编配方式在项目中应用得更多,因为优点更多,不过这种模式也有比较明显的缺点,总体来看,这个编配器像不像一个单体应用?如果微服务非常多、非常复杂,试想一下这个编配器的维护是不是基本与一个复杂单体应用一样复杂?

posted @ 2022-02-22 10:32  牛奔  阅读(134)  评论(0编辑  收藏  举报