12 | 异常重试:在约定时间内安全可靠地重试

你好,我是何小锋。上一讲我讲解了在RPC框架中如何设计自适应的负载均衡,其关键点就是调用端收集服务端每个节点的指标数据,再根据各方面的指标数据进行计算打分,最后根据每个节点的分数,将更多的流量打到分数较高的节点上。

今天我们就继续下一个话题,讲讲RPC框架中的异常重试机制。

为什么需要异常重试?

我们可以考虑这样一个场景。我们发起一次RPC调用,去调用远程的一个服务,比如用户的登录操作,我们会先对用户的用户名以及密码进行验证,验证成功之后会获取用户的基本信息。当我们通过远程的用户服务来获取用户基本信息的时候,恰好网络出现了问题,比如网络突然抖了一下,导致我们的请求失败了,而这个请求我们希望它能够尽可能地执行成功,那这时我们要怎么做呢?

我们需要重新发起一次RPC调用,那我们在代码中该如何处理呢?是在代码逻辑里catch一下,失败了就再发起一次调用吗?这样做显然不够优雅吧。这时我们就可以考虑使用RPC框架的重试机制。

RPC框架的重试机制

那什么是RPC框架的重试机制呢?

这其实很好理解,就是当调用端发起的请求失败时,RPC框架自身可以进行重试,再重新发送请求,用户可以自行设置是否开启重试以及重试的次数。

那这个机制是如何实现的呢?

还是挺简单的。我们可以回想下[第 11 讲],通过这一讲我们了解到,调用端在发起RPC调用时,会经过负载均衡,选择一个节点,之后它会向这个节点发送请求信息。当消息发送失败或收到异常消息时,我们就可以捕获异常,根据异常触发重试,重新通过负载均衡选择一个节点发送请求消息,并且记录请求的重试次数,当重试次数达到用户配置的重试次数的时候,就返回给调用端动态代理一个失败异常,否则就一直重试下去。

RPC框架的重试机制就是调用端发现请求失败时捕获异常,之后触发重试,那是不是所有的异常都要触发重试呢?

当然不是了,因为这个异常可能是服务提供方抛回来的业务异常,它是应该正常返回给动态代理的,所以我们要在触发重试之前对捕获的异常进行判定,只有符合重试条件的异常才能触发重试,比如网络超时异常、网络连接异常等等。

了解了RPC框架的重试机制,那用户在使用异常重试时需要注意哪些问题呢?

比如我刚才提的那个调用场景,当网络突然抖动了一下导致请求超时了,但这个时候调用方的请求信息可能已经发送到服务提供方的节点上,也可能已经发送到服务提供方的服务节点上,那如果请求信息成功地发送到了服务节点上,那这个节点是不是就要执行业务逻辑了呢?是的。

那如果这个时候发起了重试,业务逻辑是否会被执行呢?会的。

那如果这个服务业务逻辑不是幂等的,比如插入数据操作,那触发重试的话会不会引发问题呢?会的。

综上,我们可以总结出:在使用RPC框架的时候,我们要确保被调用的服务的业务逻辑是幂等的,这样我们才能考虑根据事件情况开启RPC框架的异常重试功能。这一点你要格外注意,这算是一个高频误区了。

通过上述讲解,我相信你已经非常清楚RPC框架的重试机制了,这也是现在大多数RPC框架所采用的重试机制。

那看到这儿,你觉得这个机制完善了吗?有没有想到连续重试对请求超时时间的影响?继续考虑这样一个场景:我把调用端的请求超时时间设置为5s,结果连续重试3次,每次都耗时2s,那最终这个请求的耗时是6s,那这样的话,调用端设置的超时时间是不是就不准确了呢?

如何在约定时间内安全可靠地重试?

我刚才讲到,连续的异常重试可能会出现一种不可靠的情况,那就是连续的异常重试并且每次处理的请求时间比较长,最终会导致请求处理的时间过长,超出用户设置的超时时间。

解决这个问题最直接的方式就是,在每次重试后都重置一下请求的超时时间。

当调用端发起RPC请求时,如果发送请求发生异常并触发了异常重试,我们可以先判定下这个请求是否已经超时,如果已经超时了就直接返回超时异常,否则就先重置下这个请求的超时时间,之后再发起重试。

那么解决了因多次异常重试引发的超时时间失效的问题,这个重试机制是不是就完全可靠了呢?

我们接着考虑,当调用端设置了异常重试策略,发起了一次RPC调用,通过负载均衡选择了节点,将请求消息发送到这个节点,这时这个节点由于负载压力较大,导致这个请求处理失败了,调用端触发了重试,再次通过负载均衡选择了一个节点,结果恰好仍选择了这个节点,那么在这种情况下,重试的效果是否受影响了呢?

当然有影响。因此,我们需要在所有发起重试、负载均衡选择节点的时候,去掉重试之前出现过问题的那个节点,以保证重试的成功率。

那我们现在再完整地回顾一下,考虑了业务逻辑必须是幂等的、超时时间需要重置以及去掉有问题的服务节点后,这样的异常重试机制,还有没有可优化的地方呢?

我刚才讲过,RPC框架的异常重试机制,是调用端发送请求之后,如果发送失败会捕获异常,触发重试,但并不是所有的异常都会触发重试的,只有RPC框架中特定的异常才会如此,比如连接异常、超时异常。

而像服务端业务逻辑中抛回给调用端的异常是不能重试的。那么请你想一下这种情况:服务端的业务逻辑抛给调用端一个异常信息,而服务端抛出这个异常是允许调用端重新发起一次调用的。

比如这个场景:服务端的业务逻辑是对数据库某个数据的更新操作,更新失败则抛出个更新失败的异常,调用端可以再次调用,来触发服务端重新执行更新操作。那这个时候对于调用端来说,它接收到了更新失败异常,虽然是服务端抛回来的业务异常,但也是可以进行重试的。

那么在这种情况下,RPC框架的重试机制需要怎么优化呢?

RPC框架是不会知道哪些业务异常能够去进行异常重试的,我们可以加个重试异常的白名单,用户可以将允许重试的异常加入到这个白名单中。当调用端发起调用,并且配置了异常重试策略,捕获到异常之后,我们就可以采用这样的异常处理策略。如果这个异常是RPC框架允许重试的异常,或者这个异常类型存在于可重试异常的白名单中,我们就允许对这个请求进行重试。

所有可能出现的问题,我们排查了一圈下来之后,一个可靠的重试机制就诞生了,如下图所示:

总结

今天我们讲解了RPC框架的重试机制,还有如何在约定时间内进行安全可靠地重试。

这个机制是当调用端发起的请求失败时,如果配置了异常重试策略,RPC框架会捕捉异常,对异常进行判定,符合条件则进行重试,重新发送请求。

在重试的过程中,为了能够在约定的时间内进行安全可靠地重试,在每次触发重试之前,我们需要先判定下这个请求是否已经超时,如果超时了会直接返回超时异常,否则我们需要重置下这个请求的超时时间,防止因多次重试导致这个请求的处理时间超过用户配置的超时时间,从而影响到业务处理的耗时。

在发起重试、负载均衡选择节点的时候,我们应该去掉重试之前出现过问题的那个节点,这样可以提高重试的成功率,并且我们允许用户配置可重试异常的白名单,这样可以让RPC框架的异常重试功能变得更加友好。

另外,在使用RPC框架的重试机制时,我们要确保被调用的服务的业务逻辑是幂等的,这样才能考虑是否使用重试,这一点至关重要。

课后思考

请你思考一下,在整个RPC调用的流程中,异常重试发生在哪个环节?

欢迎留言分享你的答案,也欢迎你把文章分享给你的朋友,邀请他加入学习。我们下节课再见!

精选留言

  • shen

    2020-03-16 00:46:37

    每看一节内容就会对照下dubbo怎么实现的,理论和实践结合,老师后面可以给一个demo然后类比下dubbo相关的设计讲解下
  • Darren

    2020-03-17 22:51:46

    其实重试也是需要时间间隔一直调整的,不然会影响服务方的性能;我们之前的处理是,重试的次数大于服务方实例的时候,我们会动态调整重试的间隔时间,举个例子:加入当前服务有3个实例,客户方重试的次数是10,那么前3次是失败就重试(不停的换下一个节点),从第四次开始,有延迟,第一次1S,第二次2S,第三次4S,以2的幂次方增加重试的等待时间,保证服务调用方不能被重试把QPS或者TPSzhan man占满。
    作者回复

    考虑的很周全

    2020-03-24 07:56:46

  • Reason

    2020-03-16 10:31:18

    按照老师所讲的内容,我认为异常重试机制发生在,客户端调用时,并且重试代码块包含的内容是集群处理(服务发现和负载均衡),以及请求调用;并且包含异步响应的结果获取。
    所以应该是在动态代理发起invoke,紧接着的一步
    作者回复

    很不错。

    2020-03-17 20:47:23

  • 小可

    2020-03-29 21:21:56

    1.保证被重试的业务服务是幂等的,
    2.超时重试前重置计时
    3.针对业务返回的异常,设置重试异常名单
    4.重试时负载均衡选取节点时要剔除前一次访问的节点
    作者回复

    关键点就是这些。

    2020-03-30 08:50:41

  • 2020-05-16 11:49:19

    异常重试的要点:
    第一重试的业务服务是幂等的(这个我认为,更细致一点,如果能判定连接未建立,请求未打到服务端,非幂等的也可以重试,毕竟请求压根就没到服务端)

    第二重试时注意重置超时时间,不要超过业务设置的超时时间,目前组内的RPC这点貌似就没做

    第三针对业务返回的异常,有些也需要重试,需要考虑怎么重试,比如:设置异常重试白名单,我们之前还使用过服务端告知的情况,在响应信息中给出是否可重试的建议,在服务端判断有些情况可以重试,就建议上游重试

    第四重试时,在通过负载均衡选择重试节点时踢出异常的节点
  • tttw

    2020-03-29 23:34:11

    failsafe failfast failover failback
    作者回复

    很不错!

    2020-03-30 08:49:58

  • 忆水寒

    2020-04-23 12:54:50

    这个异常机制其实就像网络socket连接的时候发生的异常一样,我们可以采用避退策略。也就是第一次失败,延迟2秒再试第二次。假如第二次再失败,延迟4秒。直道重试次数达到上限。
    当然了,在RPC场景下,我们也可以在前几次不断的路由切换,切换到不同的服务提供节点。
    作者回复

    rpc都是实时业务,退避好像不合适啊

    2020-04-30 22:11:52

  • 每天晒白牙

    2020-03-16 07:45:17

    个人感觉异常重试还是主要放到远程调用服务端这块
    作者回复

    那如果是由于网络问题呢,调用端没收到响应,服务端就没法处理了吧?

    2020-03-17 09:53:51

  • 高源

    2020-03-16 08:28:22

    老师讲了这些,能不能加入演示例子,原理清楚了,还差在动手能力啊😊
    作者回复

    每个框架的实现都有区别,你可以找一些开源框架,阅读源码,一一印证下,看他们的实现哪些可以借鉴,哪些需要改进。

    2020-03-17 10:09:40

  • 楼下小黑哥

    2020-03-16 22:21:04

    >>否则我们需要重置下这个请求的超时时间,防止因多次重试导致这个请求的处理时间超过用户配置的超时时间,从而影响到业务处理的耗时。

    这段超时时间重置不是很理解,A--->B--->C,B 调用 C 超时时间 10s,重试次数为 2.
    如果 B 调用 C 耗费 5s 失败,然后重试。这时重试的超时不是只剩下 5 s 了吗?
    如果又将这次超时时间重置为 10s,假如这次调用成功了,消耗了 9S,那么 B 总体耗费了 14 S,但是 A 设置超时时间假如 12 s,这时 A 不是已经超时断开了吗?

    作者回复

    重置超时时间是指,将10秒置为5秒,很大开源的rpc框架是不会修改超时时间的。

    2020-03-17 20:51:41

  • ant

    2020-04-15 16:43:52

    老师,“保证被重试的业务服务是幂等的”,这就要求了服务提供方必须能支持重复请求,而这就需要业务部门在开发可提供的每一个服务时候都要注意到这一点,那么我们是否能在服务具体逻辑之前增加一层呢,比如每一次请求带有唯一id,这一层逻辑负责统计唯一id的执行情况以及是否完成了回调,重复收到的请求,是否能把保存下来的结果直接返回呢。
    作者回复

    方案没问题,但需要考虑团队研发整体接受度

    2020-04-19 12:27:53

  • 魔曦

    2020-03-28 09:05:47

    异常重试主要有客户端的重试,每个业务层也会有重试,通过幂等,白名单,摘除认为有问题的机器,重试次数来保证业务可用
    作者回复

    rpc可以做到话,尽量下沉

    2020-03-30 20:22:25

  • answer宫

    2020-03-16 19:08:29

    老师您好,文中提到"调用端收集服务端每个节点的指标数据,再根据各方面的指标数据进行计算打分,最后根据每个节点的分数,将更多的流量打到分数较高的节点上。",如果每个调用端都要单独检测服务端节点的各项指标,这也是一个不小的性能损耗把?这个检测要每个调用端自己实现吗?这个开发量也不少啊,请老师解惑
    作者回复

    这个应该是rpc框架实现,方便使用方使用,至于性能开销,就要看计算的算法了,不要小看计算机的能力

    2020-03-17 10:04:06

  • 好运来

    2020-03-16 13:26:55

    有demo代码能够结合所讲内容会更容易理解
  • 轨迹

    2020-10-28 09:18:34

    重置超时时间,意思是,如果调用方设置超时时间是3秒,第一次调用2秒发生异常,重试的时候会重置超时时间为3秒然后进行重试,重试3次都2秒异常,这样实际的调用时间不就是2+2+2+2,8秒了吗?是这个意思吗?
  • 夜空中最亮的星

    2020-03-17 08:18:48

    rpc重试要业务层面负责幂等性
    作者回复

    如果要重试,服务端的逻辑一定要幂等

    2020-03-17 10:02:05

  • hello

    2020-03-16 10:59:43

    老师,您好!请教您一个问题,文中“重试后都重置一下请求的超时时间”,这个操作是如何实现的(APP->A->B->C->D,C在调用D过程中超时了,是通过什么机制重置超时时间让APP端能感知到的)?
    作者回复

    调用端感知,超时后再发重试请求的时候,调用端自己去调整超时时间。

    2020-03-17 10:05:24

  • ivan

    2022-12-08 23:27:23

    重试策略要配置在路由策略里面吗?
  • TableBear

    2022-10-31 11:14:22

    请问一下:超时时间重置是不是遇到可重试异常的时候,将超时时间重置为当前超时时间减去执行遇到异常的时间?
  • hillwater

    2022-10-11 21:26:13

    阿里内部hsf不支持重试,因为太容易出bug了