微服务架构设计原则
在微服务架构的设计过程中,首先需要通过统一的API网关对外提供服务,各微服务之间通过REST或gRPC协议通信。单个微服务可以调用多个不同的微服务来完成自己的功能,同时每个微服务都需要有自己独立的数据存储。微服务的部署、运维等需要通过自动化的手段来实现。
服务注册中心
一个服务可以有多个实例,但如何知道这个服务有哪些实例呢?为了减少手工维护的麻烦,需要有一个服务注册中心来完成相关的管理工作。每个服务实例启动时,都向服务注册中心注册自己的IP地址等信息。这样,当一个服务在调用其他服务的接口时,就可以通过服务注册中心查询到其他服务的实例,然后向该实例发起请求。
API网关
API 网关往往是微服务应用的统一入口,在完成微服务的开发之后,还需要考虑哪些微服务的API需要公开。之所以需要API网关,其中一个原因是,如果让外部服务直接访问微服务,当微服务开发人员在迭代过程中更新、修改微服务的API时,会影响到调用这个微服务的外部服务,从而影响这个应用。另一个原因是,有些微服务会提供gRPC或AMQP等非REST协议,这往往会受限于防火墙而无法穿透,因此需要API网关对所有微服务向外部公开的API进行封装。
API网关作为从防火墙外部进入应用的API请求的单一入口,负责请求路由、登录认证和协议转换。来自外部的所有请求先到达API网关,API网关将一部分请求直接路由到相应的服务上。在有些场景中,API网关使用API组合的模式处理其他请求,调用多个服务并聚合结果。API 网关还可以在客户端友好的协议与客户端不友好的协议之间转换。在微服务中,API网关往往采用后端前置模式,为一组微服务提供独立的API网关。
使用API网关的好处是它封装了应用的内部结构,外部不必调用特定的微服务,而是直接与API网关通信。
API网关的功能包括:路由、负载均衡、统一认证和鉴权、监控、日志、限流降级等。
API网关的缺点是增加了部署和运维的复杂度,同时需要适配大量接口,导致难以维护。
另外,因为通过API网关会在调用链增加一跳,所以会导致性能的下降。
跨服务通信
跨服务通信通常通过同步的REST和gRPC或者异步的消息队列来实现。
API设计
在单体应用中,因为应用独享数据库,所以实现诸如查询等操作相对简单。为了实现不同数据的聚合,可以通过执行SQL语句中的join来关联各个相关表。而在微服务架构中,因为所涉及的数据可能分布在多个微服务所拥有的数据库中,所以接口的设计和编写就变得很复杂。
1.API组合模式
客户端调用拥有数据的多个微服务,并组合服务返回结果。一般在实际操作过程中会创建一个单独的API组合服务来实现API组合的功能,对内调用各个微服务,对外提供标准的REST查询接口。API组合服务会尽可能并行调用各个微服务,最大限度地缩短查询等操作的响应时间。
API 组合的缺点在于它需要调用多个微服务和查询多个数据库,这带来了额外的开销,需要更多的计算和网络资源,也相应增加了运行应用程序的成本。同时因为API组合需要调用多个微服务,所以它的可用性也会随着所涉及服务数量的增加而降低。虽然可以通过缓存等方式来提升性能及可用性,但这仍然是一大隐患。另外,单体应用通常使用一个数据库事务执行查询等操作,即使它执行多个数据库查询,数据库的ACID事务属性也可以确保应用具有一致的数据视图。而通过API组合模式,因为数据分布于不同的微服务中,所以可能会造成数据的不一致。
2.CQRS模式
CQRS(Command and Query Responsibility Segregation)比API组合模式更强大,但也更复杂,它在提供接口的客户端维护一个或多个只读视图数据库,这类视图数据库的唯一目标就是支持查询类调用。CQRS 使用事务来维护从多个微服务复制而来的只读视图,借此来实现对多个微服务的数据查询。
CQRS将持久化数据的使用分为两个部分:修改更新类和查询类。涉及查询类的微服务独自位于视图数据库中,通过订阅事件的方式与源数据库同步,视图数据库会订阅相关领域事件并更新数据库,从而确保视图数据库不断更新。
CQRS一般包括4个子模块。
查询API:提供查询服务。
事件处理程序:负责订阅、处理一个或多个服务发布的事件来更新数据库。
数据库访问:实现数据库访问逻辑,查询API与事件处理程序都会使用该模块来更新和查询数据库。它往往采用数据访问对象(DAO),把上层代码映射到数据库API使用的数据类型上。
视图数据库:负责持久化相关领域模型的视图。NoSQL往往是CQRS的最好选择,因为它不受 NoSQL 事务处理能力的限制,只需要提供简单的事务并行执行固定查询即可。
独立的查询模型可以用来处理查询场景,查询模型往往比修改更新模型简单得多,因为它不需要负责实现具体的业务逻辑。尤其在微服务中,可以高效地实现跨微服务的查询。使用CQRS视图将会更加有效,因为该方法通过视图数据库预先加载了来自源数据库的数据。CQRS使用专有视图数据库来避免单个数据库的限制,每个视图数据库都有效地实现特定的查询,因为相关的微服务不必同时处理修改更新类和查询类操作,从而大大增强了隔离性。
CQRS 的缺点在于开发人员必须编写修改更新和查询两类操作的代码,因此大大增加了运维成本。
另外,由于数据库间实时同步的差异,也会造成数据一致性的缺陷。
数据一致性处理
1.数据库事务ACID强一致性模型
2.CAP原理
在只访问一个数据库的单体应用中,事务管理较为明朗,因为可以通过数据库ACID 特性来保障事务的完整。而在微服务架构下,事务往往需要横跨多个微服务,每个微服务都有自己的数据库。当对应用进行微服务拆分之后,对于更新多个微服务所拥有的数据的操作,传统的事务处理已经无法满足要求,所以需要更为高级的事务管理机制。
分布式系统的最大难点在于各个节点的状态如何同步。
传统应用采用集中式架构,因为不存在分区,所以可以同时确保一致性和可用性。在分布式系统中,因为分区无法避免,所以可以认为CAP中的P(Partition tolerance,分区容错性)总是存在,而CAP中的C(Consistency,一致性)和A(Availability,可用性)无法同时满足。所以在进行应用系统设计时应该考虑到这种问题,只能选择一个目标。如果追求一致性,那么就无法保证所有节点的可用性,ZooKeeper 的实现其实就是保证了一致性和分区容错性;如果追求所有节点的可用性,那么就没法做到一致性,大部分分布式应用的设计都选择这种模式。
3.BASE原理
BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)的缩写,是对CAP中AP的扩展。其核心理念如下。
基本可用:当分布式系统出现故障时,允许损失部分可用功能,保证核心功能可用。
软状态:允许系统中存在中间状态,这个状态不影响系统可用性,但会损失部分一致性。
最终一致性:是指经过一段时间后,所有节点数据都将达到一致。
BASE解决了CAP中没有考虑到的网络延迟问题,在BASE中用软状态和最终一致性来保证延迟后数据的一致性。BASE与ACID的理念是相反的,ACID秉持的是一种强一致性的理念,而BASE原理则是通过牺牲强一致性来获得可用性的,并允许数据在一定时间内是不一致的,但最终达到一致状态。
4.分布式事务
在多个微服务、数据库和消息代理之间,维护数据一致性的传统方式是采用分布式事务。分布式事务就是指事务的参与者、支持事务的服务器以及事务管理器分别位于不同的节点上。每次事务操作都由不同的小操作组成,这些小操作分布在不同的节点上且属于不同的微服务。分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上,分布式事务就是为了保证不同数据库的数据一致性。同时它本质上是将不同进程间的通信进行同步,但这会大大降低系统的可用性。为了让一个分布式事务完成提交,所有参与事务的服务都必须同时可用。
使用Saga时存在的问题是,Saga只满足ACD特性,而缺乏I特性,即隔离性,所以必须使用额外的对策来防止或减少因此而导致的异常。Saga是一种在微服务架构中维护数据一致性的机制,它由一系列本地事务组成,使用消息机制协调一组本地事务的序列。每个本地事务都负责更新其所在微服务的本地数据库。Saga通过使用异步消息来协调一系列本地事务,从而维护微服务间的数据一致性。
当一个本地事务执行完成时,这个本地事务所在的微服务会发布消息并触发Saga中的下一个本地事务。使用异步消息模式不仅可以确保Saga参与方之间的松耦合,还可以保证Saga的完成度。如果某个服务暂不可用,消息代理会缓存消息,直到消息被处理为止。Saga事务协调器负责对常规事务和补偿事务的执行排序。当启动Saga时,协调逻辑必须异步调用第一个Saga参与方执行本地事务。一旦事务执行完成,Saga 协调逻辑就会选择并异步调用下一个 Saga 参与方,直到 Saga 执行完所有步骤。任何一个本地事务执行失败,Saga 都必须以相反的顺序执行补偿事务,从而显式地实现回滚。Saga的核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调。如果正常结束,那么就是正常完成了;如果某个步骤失败,则需要以与事务执行顺序相反的顺序依次调用各个本地事务的补偿操作。
每个Saga事务都由一系列子事务(sub-transaction)Ti组成,每个Ti 都有对应的补偿操作Ci,补偿操作用于撤销Ti造成的结果,每个Ti都是一个本地事务。和TCC相比,Saga没有“Try (尝试)阶段”操作,它的Ti被直接提交到库,而通过补偿操作Ci来弥补失败。Saga事务的执行顺序如下:
T1,T2,T3,…,Tn
T1,T2,…,Tj,Cj,…,C2,C1,其中0<j<n
需要注意的是,在Saga模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或影响当前事务。