随着公司的访问量增加,不但要求对用户的响应速度快,还要求吞吐量的指标向外扩展(即水平伸缩)。于是单节点的服务器已经无法满足需求,又随着人员的增多以及项目的多职责,于是我们谈论最多的话题就是拆分。拆分一般分为水平拆分和垂直拆分,这里的拆分并不单指数据库或缓存,主要是一种分而治之的思想和逻辑
然而在拆分后的系统最大的问题就是一致性问题:对于这么多具有单一功能的模块,或者同一功能池中的多个节点,如何保证它们的信息、状态一致且有序地工作呢?
1.下订单和扣库存
即下订单和扣库存如何保持一致。如果先下订单,扣库存失败,则会超卖;如果下订单失败,扣库存成功,那么会导致少卖。
2.同步调用超时
服务化的系统间调用经常会因为网络问题导致系统间调用超时,A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了功能。
3.异步回调超时
和2案例类似,不过这是一个受理模式的场景,使用了异步回调返回处理结构,系统A同步调用系统B,B受理后则返回成功受理,然后系统B处理后异步通知系统A处理结果。在过程中,如果系统A迟迟没有收到B的回调结果,则两个系统的状态是不一致的。
4.掉单
在分布式系统中,两个系统协作处理一个流程,如果一个系统中存在一个请求(通常指订单),另一个系统不存在,则会导致掉单
5.系统间状态不一致
和4例类似,不同的是两个系统间都存在请求,但是请求的状态不一致
6.缓存和数据库不一致
在大规模和高并发的互联网系统里,由于对响应和吞吐量有这高要求,数据库难以抗住读流量,通常会增加一层缓存,那么缓存和数据库之间的数据如何保持一致性?是要保持强一致还是弱一致?
7.本地缓存节点不一致
一个服务池上的多个节点为了满足较高的性能需求,需要使用本地缓存,这样每个节点都有一份缓存数据的复制,如果数据要更新,则被更新时各个节点的更新是有先后顺序的,在更新的瞬间,在某个时间窗口内的各个节点的数据是不一致的。
8.缓存数据结构不一致
某系统需要在缓存中暂存某种类型的数据,该数据由多个数据元素组成,其中某个数据元素要从数据库获取,如果一部分数据元素获取失败,则由于城乡处理不正确,仍然将不完全的数据存入缓存中,在缓存使用者使用时很可能因为数据的不完整性导致异常。
针对前面抛出的一致性问,会逐个进行分析并提出解决方案,最后形成通用的设计模式。
下面先介绍一下几个重要的理论,后面的解决方案都是基于这些理论的:
ACID:
A:Atomicity,原子性。
C:Consistency,一致性。
I:Isolation,隔离性。
D:durability,持久性。
具有ACID特性的数据库支持强一致性,强一致性代表数据库本身不会出现不一致,每个事物都是原子的,要么失败要么成功,事物间是隔离的互相完全不受影响,而且最终状态是持久落盘的。典型关系型数据库代表:Mysql,Oracle。
CAP理论:
C:Consistency一致性。在分布式系统中所有的数据再同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据。
A:Availability可用性,好的响应性能。完全的可用性指的是在任何故障模型下,服务都会在有限的时间内处理完成并进行响应。
P:Patition tolerance分区容忍性。尽管网络上的部分消息丢失,但系统仍然可继续工作。
CAP原理证明,任何分布式系统只可同时满足以上两点,无法三者兼顾。由于关系型数据库是单点无复制的,因此不具有分区容忍性,但是具备一致性和可用性,而分布式的服务化系统都需要满足分区容忍性,那么我们必须在一致性和可用性之间进行权衡。如果 子网络上有消息丢失,也就是出现了网络分区,则复制操作可能会被延后,如果这时我们的使用放等待复制完成再返回,则可能导致在有限时间内无法返回,就失去了可用性;而如果使用方不等待复制完成,而在主分片写完后直接返回,则失去了一致性。
BASE:
BASE思想解决了CAP提出的分布式系统的一致性和可用性不可兼得的问题。它满足CAP理论,通过牺牲强一致性获得可用性,一般应用于服务化系统的应用层或者大数据处理系统中,通过达到最终一致性来尽量满足业务的绝大多数需求。
BA:Basically Available,基本可用。
S:Soft State,软状态,状态可以在一段时间内不同步
E:Eventually Consistent,最终一致性,在一定的时间内,最终达成一致即可。
BASE思想实现的系统由于不保证强一致性,所以系统的处理请求过程中可以存在短暂的不一致,在短暂的时间窗口内,请求处于临时状态中,系统在进行每步操作时,通过记录每个临时状态,在出现故障时可以从这些中间状态继续处理未完成的请求或退回原始状态,最终达成一致状态。
一致性模型
数据的一致性模型可以分成以下 3 类:
强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的。
弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。
最终一致:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。
分布式一致性协议:
国际开放标准组织OpenGroup定义了DTS(分布式事务处理模型),模型中包含4个角色:应用程序、事务管理器、资源管理器、通信资源管理器。事务管理器是统管全局的管理者(也称协调者),资源管理器和通信资源管理器是事务的参与者。
JEE规范中定义了TX协议和XA协议,TX协议定义应用程序与事务管理器之间的接口,XA协议则定义了事务管理器与资源管理器之间的接口。在企业级开发中,关系型数据库、JMS服务服务扮演资源管理器角色,而EJB容器扮演事务管理器的角色。
下面我们介绍两阶段提交协议、三阶段提交协议以及阿里巴巴提出的TCC,它们都是根据DTS这一思想演变而来的。
1.两阶段提交:
JEE的XA协议就是根据两阶段提交来保证事务的完整性,并实现分布式服务化的强一致性。
两阶段提交协议分布式事务分为两个阶段,一个是准备阶段,另一个是提交阶段。
2.三阶段提交:
三阶段是二阶段的改进版,它通过超时的机制解决了阻塞的问题。
三阶段提交协议与两阶段提交协议主要有两个不同点:
三阶段和两阶段相比,具有如上优点,但是一旦发生超时,系统仍然会发送不一致,只不过这种情况很少见,好处是至少不会阻塞和永远锁定资源。
二阶段和三阶段,实际上它们能解决案例1中分布式事务的问题,但是遇到极端情况时,系统会产生阻塞或者不一致的问题,需要技术人员解决。两阶段及三阶段方案中都包含多个参与者多个阶段实现一个事务,实现辅助,性能也是一个很大的问题,因此,在高并发系统中,鲜有使用两阶段提交和三阶段提交协议的场景。
3.TCC:
TCC协议将一个任务拆分成Try、Confirm、Cancel三个步骤,正常的流程会先执行try,如果执行没有问题,则再执行Confirm,如果执行过程中处理问题,则执行逆操作Cancel。
TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
与二阶段和三阶段比较:
位于业务服务层而非资源层。
没有单独的准备阶段,Try操作兼备资源操作与准备能力。
Try操作可以灵活选择业务资源的锁定粒度。
缺点:
Canfirm和Cancel的幂等性很难保证。
这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的,如果串行的服务很多,回滚的成本实在太高。
在服务化系统中,一个功能被拆分成多个子功能,一个流程会有多个系统的服务组合实现,如果使用两阶段提交协议和三阶段提交协议,则确实能解决系统间的一致性问题。除了这两个协议的自身问题,其实现也比较复杂、成本比较高,最重要的是性能不好,相比来看,TCC协议更简单且更容易实现,但是TCC协议由于每个事物都需要执行Try,再执行Confirm,略显臃肿。其实,现实系统的底线是仅仅需要达到最终一致性,而不需要实现专业的、复杂的一致性协议。实现最终一致性有一些非常有效、简单的模式,下面就介绍这些模式及其应用场景。
1.查询模式:
服务提供一个查询接口,用来向外部输出操作执行的状态。使用方可以通过查询接口得知服务操作状态,然后根据不同的状态来做不同的处理。
为了实现查询,每个服务操作都需要有一个唯一的标识,也可以使用此次服务操作对应的资源ID,例如:订单号、用户ID。
对于案例2-案例5,我们都可以使用查询模式来解决
2.补偿模式:
如果操作处于不正常的状态,则我们需要修正操作中有问题的子操作,这可能需要重新执行未完成的子操作,通过修复使整个分布式系统达到一致。
补偿操作根据发起形式分一下集中:
3.异步确保模式:
此模式是补偿模式的一个典型案例,通常把某类操作从主流程摘除,通过异步的方式进行处理,处理后把结果通过通知系统给使用方。
在实践中将要执行的异步操作封装后持久入库,然后通过定时捞取未完成的任务进行补偿操作来实现异步确保模式,只要定时系统健壮,则任务最终都会被成功执行。
在案例3,若对某个操作迟迟没有收到响应,则通过查询模式、补偿模式和异步确保模式来继续未完成的操作。
4.定期校对模式:
系统在没有达到一致性之前,需要通过补偿操作来达到最终一致性的目的,那如何来发现需要补偿的操作呢?就是定期校对。
在定期校对的一个关键就是分布式系统中需要有一个自始至终唯一的ID。
全局唯一流水ID可以将一个请求在分布式系统中的流转路径聚合,这一块的技术可以查阅谷歌的Dapper论文以及相关的开源实现项目,这里不详细展开。
在分布式系统中构建唯一ID、调用链等基础设施后,我们很容易对系统间的不一致进行核对。通常需要构建第三方的定期核对系统来监控服务之间的健康程度。
到现在为止,我们看到通过查询模式、补偿模式、定期核对模式可以解决案例2,3,4,5的所有问题。
5.可靠消息模式:
前面提到的异步确保模式,为了让异步操作的调用方和被调用方充分解耦,也由于专业的消息队列本身具有可伸缩、可分片、可持久等功能,我们通常通过消息队列实现异步化。对于消息队列,需要保证可靠的消息发送及处理机的幂等性。
当下主流的消息队列都有独立的持久化策略以及消息确认机制来实现可靠的消息发送。
要保证可靠的发生消息,那么就需要有重试机制,有了重试机制后,消息就一定会重复,那么我们需要对重复的消息进行处理。
保证操作的幂等性的常用方法:
6.缓存一致性模式:
使用缓存来保证一致性的最佳实践:
这里的的最佳实践能避免案例6,7,8中的问题
在服务化或者微服务的架构里,传统的整体应用拆分成了多个职责单一的服务,服务之间通过某种网络通信协议相互通信,完成特定的功能。然而,由于网路通信不稳定,我们在设计系统是必须考虑都对网络通信的容错,也就是超时问题的处理。
服务与服务之间的交互模式可以分为以下3类:
1.同步调用模式:
服务1调用服务2,服务1的线程阻塞等待服务2返回处理结果,如果服务2一直不返回结果,则服务1一直等待到超时为止。
同步调用模式适用于短小操作,而不适合后端负载较高的场景。
超时解决方案:
在同步调用模式下,对外的接口会提供服务契约,契约定义了服务的处理结果会通过返回值返回给使用方,对于返回的状态定义分为两种:
我们将第1种定义称为两状态的同步接口,将第2种定义称为三状态的同步接口
1.1 两状态的同步接口:
服务接口处理必需是成功或者失败的,在这种情况下可能发生两种同步调用超时:
一、发生在使用方调用此同步接口的过程中:
针对这个问题,我们需要服务的使用方使用上面提到的查询模式,异步处理查询结果,根据返回的状态做相应操作。但这里还有一个问题,如果查询模式的返回状态是未知或者说超时,则使用方需要使用同一个请求ID进行重试,服务1就必须实现请求处理的幂等性。
二、同步调用超时发送在内部服务1调用服务2的过程中:
服务1对外接口只有两种返回状态:成功或者失败,也就是对使用方来讲,不允许有中间的处理中状态,对于这种服务内部超时的场景,必须使用快速失败的策略:针对超时错误,服务快速返回失败,同时在内部调用服务2的冲正接口,服务2的冲正接口判断之前是否接受到请求,如果接收到请求并做了处理,则应该反向的回滚操作,如果之前没有处理请求,则忽略。
1.2三状态的同步接口:
服务契约规定了三种处理结果:成功、失败和处理中,内部服务的调用超时被视为内部暂时的问题,随后可能被修复,因此,可能在一定的时间窗口内告知使用方在处理中,随后修复问题并补偿执行,达到最大化请求处理成功的目的来提升用户体验。
在这种场景下,我们更倾向于给用户更好的体验,尽最大的努力成果处理用户发来的请求。因此针对服务1调用服务2时超时,我们会分返回给用户处理中的状态,随后系统尽最大努力补偿执行出错的部分,服务1需要通过服务2的查询接口得到最新的请求处理状态,如果服务2没有明确回复,则可以尝试重新发送请求,当然,这里需要服务2也实现了操作的幂等性。
2.接口异步调用模式:
服务1请求服务2受理某项任务,服务2受理后即可返回给服务1其受理结果,如果受理成功,则服务1继续做其他任务,而服务2异步的处理这项任务,直到服务2处理完这项任务后,才反向地通知服务1任务已经完成,服务1再做后续处理。
超时解决方案:
2.1、异步调用接口超时
异步调用接口超时发生在使用方调用服务1的受理接口时,同两状态同步调用接口超时场景是一样的,解决方案同两状态同步调动接口超时一致。
2.2、异步调用内部超时:
这和三状态同步调用内部超时的解决方案一致,不同的是此场景下一旦处理成功,则需要异步回调通知使用方。
2.3、异步调用回调超时
回调超时的问题经常出现,由于使用方可能是公司内部的也可能是外部的,网络环境复杂多变,因此,大多数公司都会开发一个通知子系统,用来专门处理回调通知。
由于服务1通过回调通知使用方,所有服务1需要保证通知一定可送达,如果遇到超时,则服务1复杂重新继续补偿,通常会设计一个通知时间按一定间隔递增的策略,例如:指数回退,直到通知成功为止,通知是否成功以对方的回写状态为准。
3.消息队列异步处理模式:
消息队列异步处理模式利用消息队列作为通信机制,在这种交互模式中,服务1只需将某种事件传递给服务2,而不需要等待服务2返回结果。在这样的场景下,服务1与服务2充分解耦,且具有消峰的功能。
超时解决方案:
3.1消息队列的生产者超时:
对于这种场景,同可靠消息模式
3.2消息队列的消费者超时:
对于消息队列的处理机与消息队列之间的超时或者网络问题,通常可以通过消息队列提供的机制来解决。
一般消息队列会提供如下两种方式:
如果允许丢消息,则使用第(1)种处理方式,但如果我们队消息处理的准确性要求高,则必须采用第(2)种方式。
以上三种交互模式普遍应用于服务化架构中,它们之间没有绝对的好坏,只需要在特定的场景下做出合适的选择。
以上列举了导致不一致的具体问题,并围绕这些问题,提出一致性原理如ACID、CAP和BASE等;以及两阶段、三阶段和TCC一致性协议,总结了实现最终一致性的查询模式、补充模式、异步确保模式、定期校对模式、可靠消息模式和缓存一致性模式等;最后针对服务化系统中同步调用、异步调用、消息队列等应用场景分析了超时的场景和解决方案。
PS:本文主要内容来源于《分布式服务架构》一书。之前一直没有很系统的梳理这方面的知识,此次写文的目的也是为了自己学习巩固这方面的理论知识,如果能对大家带来一些帮助那就不甚荣幸。