如果这是第二次看到我的文章,欢迎订阅我的公众号(跨界架构师)哟~
本文长度为4229字,建议阅读11分钟。
这是本系列中既「数据一致性」后的第二章节——「高可用」的完结篇。
前面几篇中z哥跟你聊了聊做「高可用」的意义,以及如何做「负载均衡」和「高可用三剑客」(熔断、限流、降级,文末会附上前文连接:))。这次,我们来聊一聊在保证对外高可用的同时,憋出的“内伤”该如何通过「补偿」机制来自行消化。
一、「补偿」机制的意义?
以电商的购物场景为例:
客户端 ---->购物车微服务 ---->订单微服务 ----> 支付微服务。
这种调用链非常普遍。
那么为什么需要考虑补偿机制呢?
正如之前几篇文章所说,一次跨机器的通信可能会经过DNS 服务,网卡、交换机、路由器、负载均衡等设备,这些设备都不一定是一直稳定的,在数据传输的整个过程中,只要任意一个环节出错,都会导致问题的产生。
而在分布式场景中,一个完整的业务又是由多次跨机器通信组成的,所以产生问题的概率成倍数增加。
但是,这些问题并不完全代表真正的系统无法处理请求,所以我们应当尽可能的自动消化掉这些异常。
可能你会问,之前也看到过「补偿」和「事务补偿」或者「重试」,它们之间的关系是什么?
你其实可以不用太纠结这些名字,从目的来说都是一样的。就是一旦某个操作发生了异常,如何通过内部机制将这个异常产生的「不一致」状态消除掉。
题外话:在Z哥看来,不管用什么方式,只要通过额外的方式解决了问题都可以理解为是「补偿」,所以「事务补偿」和「重试」都是「补偿」的子集。前者是一个逆向操作,而后者则是一个正向操作。
只是从结果来看,两者的意义不同。「事务补偿」意味着“放弃”,当前操作必然会失败。
事务补偿
「重试」则还有处理成功的机会。这两种方式分别适用于不同的场景。
重试
因为「补偿」已经是一个额外流程了,既然能够走这个额外流程,说明时效性并不是第一考虑的因素,所以做补偿的核心要点是:宁可慢,不可错。
因此,不要草率的就确定了补偿的实施方案,需要谨慎的评估。虽说错误无法100%避免,但是抱着这样的一个心态或多或少可以减少一些错误的发生。
二、「补偿」该怎么做?
做「补偿」的主流方式就前面提到的「事务补偿」和「重试」,以下会被称作「回滚」和「重试」。
我们先来聊聊「回滚」。相比「重试」,它逻辑上更简单一些。
「回滚」
Z哥将回滚分为2种模式,一种叫「显式回滚」(调用逆向接口),一种叫「隐式回滚」(无需调用逆向接口)。
最常见的就是「显式回滚」。这个方案无非就是做2个事情:
首先要确定失败的步骤和状态,从而确定需要回滚的范围。一个业务的流程,往往在设计之初就制定好了,所以确定回滚的范围比较容易。但这里唯一需要注意的一点就是:如果在一个业务处理中涉及到的服务并不是都提供了「回滚接口」,那么在编排服务时应该把提供「回滚接口」的服务放在前面,这样当后面的工作服务错误时还有机会「回滚」。
其次要能提供「回滚」操作使用到的业务数据。「回滚」时提供的数据越多,越有益于程序的健壮性。因为程序可以在收到「回滚」操作的时候可以做业务的检查,比如检查账户是否相等,金额是否一致等等。
由于这个中间状态的数据结构和数据大小并不固定,所以Z哥建议你在实现这点的时候可以将相关的数据序列化成一个json,然后存放到一个nosql类型的存储中。
「隐式回滚」相对来说运用场景比较少。它意味着这个回滚动作你不需要进行额外处理,下游服务内部有类似“预占”并且“超时失效”的机制的。例如:
电商场景中,会将订单中的商品先预占库存,等待用户在 15 分钟内支付。如果没有收到用户的支付,则释放库存。
下面聊聊可以有很多玩法,也更容易陷入坑里的「重试」。
「重试」
「重试」最大的好处在于,业务系统可以不需要提供「逆向接口」,这是一个对长期开发成本特别大的利好,毕竟业务是天天在变的。所以,在可能的情况下,应该优先考虑使用「重试」。
不过,相比「回滚」来说「重试」的适用场景更少一些,所以我们第一步首先要判断,当前场景是否适合「重试」。比如:
-
下游系统返回「请求超时」、「被限流中」等临时状态的时候,我们可以考虑重试
-
而如果是返回“余额不足”、“无权限”等明确无法继续的业务性错误的时候就不需要重试了
-
一些中间件或者rpc框架中返回Http503、404等没有何时恢复的预期的时候,也不需要重试
如果确定要进行「重试」,我们还需要选定一个合适的「重试策略」。主流的「重试策略」主要是以下几种。
策略1.立即重试。有时故障是候暂时性,可能是因网络数据包冲突或硬件组件流量高峰等事件造成的。在此情况下,适合立即重试操作。不过,立即重试次数不应超过一次,如果立即重试失败,应改用其它的策略。
策略2.固定间隔。应用程序每次尝试的间隔时间相同。 这个好理解,例如,固定每 3 秒重试操作。(以下所有示例代码中的具体的数字仅供参考。)
策略1和策略2多用于前端系统的交互式操作中。
策略3.增量间隔。每一次的重试间隔时间增量递增。比如,第一次0秒、第二次3秒、第三次6秒,9、12、15这样。
return (retryCount - 1) * incrementInterval;
使得失败次数越多的重试请求优先级排到越后面,给新进入的重试请求让道。
策略4.指数间隔。每一次的重试间隔呈指数级增加。和增量间隔“殊途同归”,都是想让失败次数越多的重试请求优先级排到越后面,只不过这个方案的增长幅度更大一些。
return 2 ^ retryCount;
策略5.全抖动。在递增的基础上,增加随机性(可以把其中的指数增长部分替换成增量增长。)。适用于将某一时刻集中产生的大量重试请求进行压力分散的场景。
return random(0 , 2 ^ retryCount);
策略6.等抖动。在「指数间隔」和「全抖动」之间寻求一个中庸的方案,降低随机性的作用。适用场景和「全抖动」一样。
var baseNum = 2 ^ retryCount;return baseNum + random(0 , baseNum);
3、4、5、6策略的表现情况大致是这样。(x轴为重试次数)
为什么说「重试」有坑呢?
正如前面聊到的那样,出于对开发成本考虑,你在做「重试」的时候可能是复用的常规调用的接口。那么此时就不得不提一个「幂等性」问题。
如果实现「重试」选用的技术方案不能100%确保不会重复发起重试,那么「幂等性」问题是一个必须要考虑的问题。哪怕技术方案可以确保100%不会重复发起重试,出于对意外情况的考量,尽量也考虑一下「幂等性」问题。
幂等性:不管对程序发起几次重复调用,程序表现的状态(所有相关的数据变化)与调用一次的结果是一致的话,就是保证了幂等性。
这意味着可以根据需要重复或重试操作,而不会导致意外的影响。对于非幂等操作,算法可能必须跟踪操作是否已经执行。
所以,一旦某个功能支持「重试」,那么整个链路上的接口都需要考虑幂等性问题,不能因为服务的多次调用而导致业务数据的累计增加或减少。
满足「幂等性」其实就是需要想办法识别重复的请求,并且将其过滤掉。思路就是:
-
给每个请求定义一个唯一标识。
-
在进行「重试」的时候判断这个请求是否已经被执行或者正在被执行,如果是则抛弃该请求。
第1点,我们可以使用一个全局唯一id生成器或者生成服务(可以扩展阅读,分布式系统中的必备良药 —— 全局唯一单据号生成)。 或者简单粗暴一些,使用官方类库自带的Guid、uuid之类的也行。
然后通过rpc框架在发起调用的客户端中,对每个请求增加一个唯一标识的字段进行赋值。
第2点,我们可以在服务端通过Aop的方式切入到实际的处理逻辑代码之前和之后,一起配合做验证。
大致的代码思路如下。
【方法执行前】if(isExistLog(requestId)){ //1.判断请求是否已被接收过。 对应序号3 var lastResult = getLastResult(); //2.获取用于判断之前的请求是否已经处理完成。 对应序号4 if(lastResult == null){ var result = waitResult(); //挂起等待处理完成 return result; } else{ return lastResult; } } else{ log(requestId); //3.记录该请求已接收 } //do something.. 【方法执行后】 logResult(requestId, result); //4.将结果也更新一下。
如果「补偿」这个工作是通过MQ来进行的话,这事就可以直接在对接MQ所封装的SDK中做。在生产端赋值全局唯一标识,在消费端通过唯一标识消重。
三、「重试」的最佳实践
再聊一些Z哥积累的最佳实践吧(划重点:)),都是针对「重试」的,的确这也是工作中最常用的方案。
「重试」特别适合在高负载情况下被「降级」,当然也应当受到「限流」和「熔断」机制的影响。当「重试」的“矛”与「限流」和「熔断」的“盾”搭配使用,效果才是最好。
需要衡量增加补偿机制的投入产出比。一些不是很重要的问题时,应该「快速失败」而不是「重试」。
过度积极的重试策略(例如间隔太短或重试次数过多)会对下游服务造成不利影响,这点一定要注意。
一定要给「重试」制定一个终止策略。
当回滚的过程很困难或代价很大的情况下,可以接受很长的间隔及大量的重试次数,DDD中经常被提到的「saga」模式其实也是这样的思路。不过,前提是不会因为保留或锁定稀缺资源而阻止其他操作(比如1、2、3、4、5几个串行操作。由于2一直没处理完成导致3、4、5没法继续进行)。
四、总结
这篇我们先聊了下做「补偿」的意义,以及做补偿的2个方式「回滚」和「重试」的实现思路。
然后,提醒你要注意「重试」的时候需要考虑幂等性问题,并且z哥也给出了一个解决思路。
最后,分享了几个z哥总结的针对「重试」的最佳实践。
希望对你有所帮助。
Question:
你之前有哪些时候是通过自己人工来做「补偿」的经历吗?欢迎吐槽~
z哥自己就有多次熬到半夜才把“意外”造成的混乱清理干净,刻骨铭心啊。
相关文章:
作者:Zachary
出处:https://www.cnblogs.com/Zachary-Fan/p/compensation.html
关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎关注我的公众号(跨界架构师)哦~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
相关推荐
分布式系统设计模式是指在分布式系统中,为了解决如何划分服务、如何部署服务以及如何组织服务间通信等问题而采用的一些通用方案和策略。这些模式能够在不同的分布式环境和应用场景中应用,以期达到系统设计的最优解...
分布式神经网络框架——分布式神经网络
《分布式系统原理与范型》作为一本系统介绍分布式系统基本原理与实践应用的书籍,涵盖了分布式系统设计与实现的核心理念。分布式系统指的是由多个可以独立运行的计算单元构成的系统,这些计算单元通过通信网络相互...
【分布式应用解决方案——linkbase】 分布式链接库,或者称为linkbase,是搜索引擎核心组件之一,尤其是在百度搜索引擎中扮演着至关重要的角色。linkbase用于存储大量的链接数据,包括互联网上的网页链接,其性能和...
系统和更现代化的业务流程连接起来,《SOA实践指南》都阐明了SOA如何满足你的需 要。 目录 第1章:动机 1.1 大型分布式系统的特征 1.2 魔术总线故事 1.3 魔术总线故事给我们的启示 1.4 soa历史 1.5 ...
这种模式的核心是通过分布式系统实现生产异常的实时监控、精准预测和快速响应,以解决传统以制造执行系统(MES)为核心的车间运行模式存在的缺陷。文章《分布式自主协同制造——一种智能车间运行新模式》详细介绍了...
基于Spark视域下的分布式大数据算法分析——以计算机维修实验室管理系统为例.pdf
该书不仅对分布式系统的基础理论做了详尽阐述,还提供了大量实践案例,旨在帮助读者深入理解分布式系统的原理和技术。 #### 二、核心概念 1. **分布式系统定义**:分布式系统是由多个相互连接的计算机组成的一个...
分布式系统工程实践 分布式系统是一种由多个独立的计算单元组成,通过网络互相连接并协作完成任务的系统。这类系统通常具有高度的内聚性和透明性,其核心目标是提供高性能、高可用性以及可扩展性。分布式系统中,...
分布式应用解决方案——linkbase是软件开发领域中针对大型搜索引擎如百度的一种关键技术,它涉及链接数据的存储、管理和处理,以优化网页抓取效率和搜索结果质量。linkbase的发展经历了三个阶段,分别是基于主域分环...
专题研究 DDTP:分布式数据传输协议——个人信息可携带权的中国路径倡议
【分布式系统学习——GFS谷歌文件系统Paper翻译1】 谷歌文件系统(Google File System, GFS)是一个专为大规模分布式数据处理设计的可扩展的分布式文件系统。它基于普通的、价格适中的硬件设备,旨在在容错性、性能...
例如,Google的搜索索引、Facebook的照片存储和推荐系统,以及Netflix的流媒体服务都是分布式系统设计的典范。 总结来说,分布式系统设计原理与实践涉及的内容广泛且深入,涵盖数据存储、计算、服务架构、通信协议...
1. 分布式系统基础:首先,书籍可能会介绍分布式系统的概念,包括其基本特征、优势以及面临的挑战,如数据一致性、容错性、网络延迟等。 2. 对象存储模型:分布式对象存储通常基于对象模型,每个对象包含数据和元...
开源分布式版本控制工具 —— Git 之旅 Git 是一个开源的分布式版本控制软件,由 Linus Torvalds 于 2005 年开发,旨在解决 Linux 内核维护工作中的繁琐事务。Git 的设计思想是分布式代码库与文件快照,相比于传统...
### Hadoop分布式文件系统(HDFS):关键技术与实践 #### 摘要 Hadoop分布式文件系统(HDFS)是Hadoop项目的核心组件之一,旨在为大规模数据集提供高效可靠的存储解决方案。HDFS的设计原则强调了数据的分布式存储与...
北邮 邹华 老师的分布式计算环境课件,第一章第2部分,讲述中间件
在分布式系统中,Spring可以用于编写服务端的业务逻辑,并且Spring框架支持分布式开发中的多种模式和最佳实践。 Android是一种广泛使用的移动操作系统,由Google主导开发。Android应用开发通常采用Java语言,以及...