浅聊异常测试系列-幂等测试

原创
06/16 10:00
阅读数 635

阿里QA导读:幂等是一个出现频率极高的词汇,今天这篇文章为我们详细的介绍了幂等以及幂等测试

前言



        是否你有过在收一笔款子的时候突然传来了连续两声XXX到账一千元的经历呢,这时候你可能乐呵了,背后的开发测试同学可就头疼喽,因为这就有可能就是幂等被击穿所产生的故障。

        分布式架构体系中很关键的一点就是业务上下游数据一致性,事务一致性,表里如一。针对数据一致性我们在系统设计上可能使用XTS分布式事务保障,但是相对来说实现和维护成本较高,可能对大多数业务来说并不适用。当下大部分业务通常使用的一些实现手段,例如事件单框架,异步命令框架等方式,通过异步调度,任务重试等机制来保证业务的最终的正确性和一致性。而这类架构的实现过程中最核心的评估内容之一,就是幂等。

        幂等这个词在我们日常工作中出现的频率极高,是一个基础老生常谈的课题,但在我们不同的架构,不同的业务间的实现方案和手段又是千变万化的。在我们的业务处理过程中,每一阶段每一步骤都需要考虑异常阻断后的重试逻辑,处理不当的话可能会出现非常严重的故障问题。幂等处理不好,轻则影响客户,重则造成资损。

        所以今天聊一聊异常测试系列之幂等测试,新同学看了了解一下幂等基本概念,老同学也可以温故而知新也

1.幂等定义


 官方描述

幂等测试是指对同一个系统操作多次执行后产生的结果是相同的测试过程。即使对于重复执行相同操作而言,系统不会因此而产生任何不同的结果。

一句话描述一下,同一请求任意多次执行所产生的影响均与一次执行的影响相同。

解读

        一个具有幂等性的服务,要求无论重复请求在多么极端的情况下发生,都要表里如一,此时必须满足

  • 对外:返回完全相同的结果

  • 对内:不重复受理,自身状态不再发生任何改变

幂等通常分为:业务幂等、系统幂等

  • 系统幂等:指针对系统同一请求的处理做幂等,聚焦同一业务请求通过幂等号进行的幂等控制。

  • 业务幂等:指基于一定业务特性或要求生成的幂等实现方式,比如“先查再处理”,“业务上不允许调用多次”,“业务限额控制”等等。

为什么要幂等?

在分布式的场景下,可能出现重复提交,或网络异常阻塞的重试导致系统的重复操作。

若作为一个上游A应用调用下游B应用时出现超时的情况后A应用无法进行后续处理。但是又不知道下游状态,B应用是根本没收到请求,还是收到了已经处理完了,还是收到了处理失败了,上游A只能一脸懵逼的重试了。

假设在放款的场景下,若已经处理成功且没有幂等的情况下,这时候就可能会出现前言里提到的xxx到账一千元两次的情况了。所以通过进行幂等测试,可以检测系统在重复操作时是否具有正确的处理逻辑,防止数据出现错误或系统崩溃等问题。

2.幂等性的特性


原子性

即用于控制幂等性的逻辑必须要和业务处理逻辑在一个事务内,他们是需要做到同时成功或者同时失败。否则非常容易出故障,典型就是通过TAIR或者缓存来控制幂等性。

传递性

即A接口具备幂等性,那么他依赖的第三方服务也需要具备幂等性。否则重复请求的情况下,可能无法返回同样的结果。

3.典型幂等场景 


需要保证幂等的范围
  • 业务接口的处理逻辑

  • 消息的处理逻辑

  • 内部的调度/补充任务执行逻辑

符合幂等的典型场景

  • 第一次有处理有执行某个操作,第二次重试后执行操作的影响与第一次完全一致(接口调用,内部处理完全一致)

  • 第一次有处理有执行某个操作,第二次重试没有执行某个操作(例如常见的先查询后操作的情况,查询确认通过后就不再重复执行)

  • 第一次处理没有执行某个操作,第二次重试又执行某个操作(常见与有缓存的地方,缓存失效后会重新执行)

不符合幂等的典型场景

  • 被测接口返回:

  • 首次请求的终态返回值与重复请求的返回值不一致(任意一个字段多了、少了、变了)

  • 重复请求,接口报了业务异常:校验失败,唯一键冲突等

  • 系统内部处理:

  • 重复请求,重新做了一笔业务:数据库中落了两笔单据等

  • 重复请求,DB的数据不一致:金额出现了累加,单据号、状态发生了变化等

  • 幂等ID为空的情况业务处理成功了

  • 幂等ID与DB的唯一键或主键不匹配:并发场景下会击穿幂等

  • 系统内部存在先查询判断后调用的处理,重复请求会被查询拦截。并发场景下,若查询接口被击穿,重复外调请求可能会击穿幂等。

  • 外调其他接口:

  • 重复请求,调用其他外部RPC接口的参数与首次不一致(幂等ID不一致、其他参数不一致等)

  • 重复请求,调用其他外部RPC接口的返回结果与首次不一致(调用结果、返回明细不一致等)

  • 重复请求,对外发送的消息内容与首次不一致:幂等ID不一致、其他参数不一致等

  • 业务链路幂等:

  • 上下游针对幂等ID的理解有误,在各自应用都幂等的情况下链路上不幂等(如幂等键理解不一致,参数requestID、orderId等等)

  • 外调的接口被重复请求后,对应接口应用内部重复受理请求,导致对应应用的幂等被击穿。

  • 无法直接判断是否幂等的场景(无法拿到结果,需要重试)

  • 第一次处理返回业务处理中、DB抖动、RPC调用超时等非业务异常,第二次重试的时候返回终态结果

  • 第一次处理返回终态结果,第二次处理返回业务处理中、DB抖动、RPC调用超时等非业务异常


4.核心关注点

对于业务链路

(此处没有明确方案,只有一些建议思考点)

  • 任何一个请求都可能被重复调用包括前端请求)

  • 需要通盘考虑整体业务的幂等设计以及幂等键的选取策略(必须全局唯一,重复请求不变化

  • 任何一个写操作处理都可能被异常中断(断网、重启、超时等),导致系统停留在某个中间状态。

  • 需要考虑所有中间状态下系统的重试策略和方式。包括但不限于以下场景:

  • 异常中断,下游实际处理成功(幂等场景)

    • 异常中断,下游实际处理失败(幂等场景)

    • 异常中断,下游实际未处理(非幂等场景)

  • 在同一状态机下,任何业务操作都可能被重复调用(读操作、写操作、业务判断等)

  • 需要考虑重复请求的场景下所有的业务操作是否会被错误拦截处理。

对于服务方

  • 明确接口幂等性,不同要求关注点不同

  • 弱幂等:针对请求的处理只要幂等号一致,就算同一笔请求,幂等成功

  • 强幂等:在弱幂等的情况下,还要求所有的参数都一致才算幂等成功,否则算幂等失败。

  • 多次被请求,不重复受理

  • 不能落多次单据,有且只能有一份业务数据

  • 幂等ID组合必须是DB或其他分布式锁的唯一键,防止击穿。

  • 业务结果(状态、金额、业务日期等),不再发生任何变化。

  • 多次被请求,返回结果保持一致,不存在二义性

对于调用方

  • 一定考虑所调用下游服务的幂等性明确幂等号的组成(幂等号可能由一个或多个入参组成)

  • 强幂等:多次请求,所有参数必须保持一致

  • 弱幂等,多次请求,幂等号必须保持一致,不发生变化

  • 幂等号的设计思路,必须全局唯一(跨用户、跨请求、跨机房)

  • 对下游服务的返回结果做合适处理

  • 多次请求,返回成功,正常处理(推荐方式)

  • 多次请求,返回明确幂等异常,识别异常作为成功处理(不推荐这种方式,一旦错误码存在二义性,容易出现严重问题;另外增加调用方的学习成本)

  • 多次请求,返回其他未经确认的异常,默认不做任何处理

  • 先查后写的设计模式,必须明确查询接口是针对业务写操作请求本身的结果查询,并且返回明确的处理状态(这种模式下,也必须考虑后续写接口本身的幂等,防止查询击穿

  • 返回处理成功,正常处理,认为已幂等

  • 返回请求不存在,正常处理,可以继续写操作

  • 返回其他未经确认的异常,默认不做任何处理

  • 没有DB的应用在做幂等处理时需要严格依赖有DB应用的幂等服务。

强幂等弱幂等

识别应用采用是强幂等还是弱幂等

  • 弱幂等:针对请求的处理只要幂等ID一致,就算同一笔请求,幂等成功

  • 强幂等:在弱幂等的情况下,还要求所有的参数都一致才算幂等成功,否则算幂等失败。其中强幂等的判断各个应用针对强幂等的判断又可能采用不同的策略:

  • 使用对象判断相等:将DB已有的数据反序列化对象,与传入的参数对象做equals判断

  • 使用字符串判断相等:将新传入的参数序列化为字符串,与DB已经存在的数据字符串做equals判(涉及到序列化配置,属于高风险模式

  • 仅判断部分字段:选择入参中的部分字段做equals判断

补充说明下,应用采用强弱幂等模式,完全是应用自身基于风险的考量,不存在孰优孰劣:

  • 强幂等:

  • 优点:可以完全最大程度降低资损风险:比如第一次请求是付款10元,第二次重试的时候因系统原因对金额进行了累加,变成了付款20元,此时如果下游是强幂等则会进行拦截报错,制止后续的业务处理,否则可能后续就会按照20元进行业务处理,导致有10元的资损发生

  • 缺点:技术框架要求较高:需要保证任意情况的重试均不会有字段值变更(比如new Date()就不能直接采用),且针对返回结果要识别幂等失败这种特殊场景。

  • 弱幂等:与强幂等刚好相反,对技术框架要求低一些,但无法拦截重试金额变化的问题

幂等checklist(供参考)

checkList来源:小瑕,泽火

  1. 幂等键的设计

    1. SEQ超过最大值重置击穿幂等

    2. 幂等号分段设计(如分库分表),导致分区间幂等重复

    3. 多参数组合幂等键,会因为业务场景变更击穿幂等

    4. 幂等键包含允许为null的字段,未有效拦截,导致幂等问题

    5. 生成幂等号和远程调用,放在一个事务里面,导致异常重试幂等号重新生成

  1. 幂等键的存储

    1. 幂等键未持久化(如放内存,重启会丢失。常见有效的方案:DB唯一性约束、分布式锁redis、tair、zookeeper等)

    2. 幂等键存储介质本身不具备唯一性约束能力,单纯依靠查询做幂等(查询时并发单据还未写入,查询不到流水导致重复写入)

    3. DB幂等设计未考虑分库分表击穿幂等

  1. 幂等传递

    1. 上下游对字段理解不一致导致幂等击穿(*双方幂等键设计必须满足第1大类)

    2. 上下游对异常重试机制理解不一致导致幂等被击穿(*进一步分析见第6大类)

    3. 跟外部机构约定不一致导致幂等被击穿(约定是否有明确文字信息)

    4. 上游调用方在未通知情况下对幂等键约定修改(修改是否有明确文字信息)

    5. 下游服务方没有对请求参数进行一致性校验,导致重试逻辑击穿幂等(两次幂等键一样,但第二次的参数变了,覆盖第一次请求)

    6. 下游服务方对完全相同两次重试请求的受理结果不一致(第一业务成功,第二次业务失败)

  1. 业务变更或切流引入幂等风险

    1. 异步化泄洪,当正常的业务逻辑被打乱,部分环节延迟,幂等有被击穿的风险

    2. 新老系统并存业务逻辑不兼容,导致幂等被击穿(*进一步分析见第5大类)

    3. 历史数据迁移、批刷,对幂等的影响

    4. failover切库设计

    5. 数据订正,冲破幂等控制

  1. 变更兼容性幂等风险

    1. DB结构变更(唯一键、唯一索引),与代码上线时间不一致,可能存在幂等风险

    2. 代码发布,集群分批生效,可能存在幂等风险

    3. 新业务配置推送,配置分批逐渐生效,前后业务逻辑幂等机制不一致导致幂等被击穿

    4. 新业务预发/灰度验证单据,被线上调度捞起,可能存在幂等风险

  1. 重试场景的幂等考虑

RPC远程调用:

  • 请求外部服务调用超时(重试幂等键不变)

  • 请求调用服务连接拒绝(重试幂等键不变)

  • 请求外部找不到服务(重试幂等键不变)

  • 重复请求外部服务幂等(两次请求参数不变)

  • 处理外部请求并发(上下游幂等机制一致)

  • 重复处理外部调用幂等(两次的受理结果一致)

  • 其他未返回明确结果的异常(由谁触发重试、重试的幂等如何控制)

消息相关:

  • 处理外部消息重投幂等

  • 处理外部消息乱序

  • 对外发送消息失败

  • 对外发送消息幂等

  • 其他未返回明确结果的异常

XTS相关:

  • XTS事务悬挂

  • XTS回滚

  • XTS空回滚

  • XTS事务提交失败

  • XTS事务回滚失败

  • XTS的幂等性

  • 其他未返回明确结果的异常

外部机构:

  • 第三方机构报文重发

  • 文件重给

  • 文件内容重复

离线数据加工:

  • 数据丢失

  • 数据重复

  • 数据错行

  • 数据生成延迟

5.经典幂等问题分析


幂等号不唯一,导致幂等击穿
  • 简单描述:某业务场景,幂等字段包含了时区字段,欧洲时区切换后,机构发来的时区发生变化(幂等号不唯一了),导致幂等击穿,重复入账。

  • 细节:幂等要素即账单幂等要素,通常是机构标识+日期+流水号+金额,重点!!除此之外还加了当前机构的所在时区。如果时区正确的话正常来说幂等逻辑都是生效的。但是,账单落户的时候未使用标准时区表述方式(Rurope/London)而采用了与格林威治时间差值(+01:00)。由于欧美地区存在冬夏令时机制,同一时区的不同时段与格林威治时间差值不一致。也就是说在冬夏时令切换后,十月份最后一个周日凌晨一点。。。账单在不同时间段到达导致同一笔账单幂等键不一致,导致幂等被击穿。

  • 修复方案:把判断幂等的条件里 timeZone去掉,避免幂等号变化情况。

  • 问题反思:幂等号的设计思路,必须全局唯一(跨用户、跨请求、跨机房),在任何情况下都不能发生变化

幂等ID与DB唯一键不一致,导致幂等被击穿,重复受理

  • 经典case:某退款业务重复放款

  • 一句话描述:核心业务表的主键是自增长ID,而对业务字段没有增加唯一索引控制,重试场景下幂等被击穿。

  • case详情:

  • 故障背景:退款系统有过owner移交,代码一直都存在该问题,属于历史遗留问题。最近退款业务量暴增,增加了该问题发生几率。

  • 一句话原因:系统在处理退款业务的时候,未针对退款业务做好幂等性处理,导致在并发场景下,会出现数据重复落地。

  • 详细原因描述:a系统调b系统退款,a系统保存和数据库未做幂等控制,导致退款订单数据重复落地。通过查看系统调用日志,只有一次调用,经判断为判断为sofa框架rpc网络重发导致(ws服务器调用时基于http协议会默认重发3次)。网络重发问题不可控,应从落地数据方面控制。

  • 修复方案:新建一张幂等表,解决退款业务的幂等性问题

  • 问题反思:幂等ID组合必须是DB或其他分布式锁的唯一键,防止被击穿。

重复请求被业务规则错误拦截,导致上下游不一致

  • 经典case:断网演练触发某业务提前收款分布式一致性问题,导致额度有误

  • 一句话描述:首次请求网络失败但实际异步恢复成功,重复请求被额度查询的业务规则校验拦截,错误终止流程,导致上下游数据不一致。

  • 修复方案:异步重试时,不再依赖查询额度判断,直接发起异步重试

  • 问题反思:重试流程的设计过程中,需要评估业务规则校验的影响,避免被错误拦截。建议的一些方案:

  • 通过状态机等方式控制,重试过程中,不再反复的业务检查,直接重试业务操作。

  • 如必须做业务检查,不可跳过,则在业务检查前增加幂等检查

    6.幂等测试的构造(用BKLIGHT准没错)


    都讲到这了 大家了解了幂等可能产生的问题了吧?那么如何构建幂等测试呢

    你还在remote+debug构建超时么,那就过时了,不如试试BKLIGHT平台

    BKLIGHT不仅只做能做幂等测试方案构造,更有MCOK,异常测试等等一系列的能力,使用简单10分钟上手。


    构建幂等测试A方案(页面为MOCK,异常注入平台,仅供参考):

    方案1 :被测系统模拟调用超时实际成功(推荐方案)

    注入方式:被测应用+rpc注入+调用异常+模拟timeout

    注入效果:调用超时,但实际下游业务处理成功

    注意点:

    • 被测系统A->B,期望测试A调用B幂等场景的返回处理,用这种方式,注入点应该在被测系统A上

    • 一定要选择“调用结束后抛异常”,否则白测!!!!

    方案2 下游系统B设置延迟返回,造成被测应用A超时

    注入方式:被调系统B+内存注入+调用延时+调用结束后延迟

    注入效果:处理成功,但返回延时,上游调用方会超时

    注意点:

    • 被测系统A->B,期望测试A调用B幂等场景的返回处理,用这种方式,注入点应该在被调系统B上

    • 一定要选择“调用结束后延时”,否则白测!!!!

    不建议理由:注入点不是被测应用,比较变扭,需要去下游应用找实现类注入,很容易搞错。

    推荐方案3:就用BKLIGHT平台的自动幂等测试功能

    原理

    对一个幂等点(方法)发起两次调用,拦截一个线程内所有持久化或者对外发送的数据,自动评估数据的幂等性。

    能发现和不能发现的问题

    能发现的问题

    • 第一次处理的终态返回值与第二次请求的返回值不一致:任意一个字段多了、少了、变了

    • 第二次处理的时候重新做了一笔业务:数据库中落了两笔单据,放了两笔款,发了两条短信等

    • 第二次处理的时候报了业务异常:校验失败,唯一键冲突等

    • 第二次处理的时候调用其他外部RPC接口的参数与第一次不一致:幂等ID不一致、其他参数不一致等

    • 第二次处理的时候对外发送的消息内容与第一次不一致:幂等ID不一致、其他参数不一致等

    • 第二次处理的时候DB的数据不一致:金额出现了累加,单据号、状态发生了变化等

    不能发现的问题

    • 不在一个线程内处理的幂等逻辑的情况。

    • 重试的情况下不会执行逻辑(第一次做了,第二次没做,两次流量关联不上)的情况:比如内部的幂等控制采用的是先查询再操作,即如果查询到已经处理完了就不在处理了的情况下,这部分处理逻辑因为不会进行重试,就没有对比数据,所以无法发现。

    • 重试的情况下额外执行的逻辑(第一次没做,第二次做了,两次流量关联不上)的情况:此情况在有缓存或者判断逻辑的情况下误报率太高,就去掉校验了,当做正常情况处理。

    • 重试情况下必须不能执行的逻辑(第一次做了,第二次绝对不能做,有点业务规则)的情况:基于原理来看,第二次只要相同就幂等,不会判断是否应该做,比如对客短信发送,第一次成功后,重试就一定不能发,这个时候无法发现。

    • 幂等ID为null的情况:如果应用自身幂等ID为null的时候也能做业务,那么上游在重试的时候,因为调用外部参数没变,符合幂等逻辑,但是下游业务还是会做两笔。这种情况无法发现。

    • 其他基于内存数据,而不是DB数据做业务的情况:基于其原理,重试执行的时候两次的入参,上下文等都是同一个对象,如果第一次操作后有修改,则会反应到第二次调用中来,可能对幂等有影响。

    7.鸣谢


    以上很多想法都是来自大佬们的指导和启发

    特别鸣谢

    曾铣,小瑕,鸣野,术赤,林力,添亦,路月,如虎等

    联合作者:

    蚂蚁集团-财富保险事业群-财保技术部-质量与技术风险-联觉

    网商银行-网商银行-信息科技部-质量与技术风险-添亦

    蚂蚁消金-信息科技部-消金质量-林力



    关注阿里巴巴技术质量阅读更多





    本文分享自微信公众号 - 阿里巴巴技术质量(AlibabaTechQA)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    展开阅读全文
    加载中
    点击引领话题📣 发布并加入讨论🔥
    打赏
    0 评论
    0 收藏
    0
    分享
    返回顶部
    顶部