写点什么

如何正确地实现重试 (Retry)

  • 2020-06-25
  • 本文字数:3006 字

    阅读完需:约 10 分钟

如何正确地实现重试(Retry)

在日常的编码过程中,无论是和本地服务相关的本机资源交互,还是和本地服务相关的远程资源甚至是远程服务进行交付,都可能会遇到失败(异常),这时候,我们最常见的做法就是重试,本文将和大家介绍一下如何正确实现重试。

什么是重试


重试:即从新尝试,以观察结果是否符合预期。


to try (something) again to see if it is successful, working, or satisfactory。


在生活中,以买彩票为例,再次尝试购买彩票有以下几种情况:


  • 彩票没中(结果不符合预期)

  • 上次没带钱(条件不符合)

  • 彩票门店没开门(结果异常)


图形化的表述,可以简化为:


什么是正确的重试

和任何的锲而不舍都需要向着现实低头一样,“重试”也需要有终止条件(即有条件的重试),想象一样买彩票的场景,如果屡次不中,一直尝试不停歇,那不是得破产吗?


在日常的编码中,我们最常见的做法也是如此,即指定一个重试次数的上限,然后单次请求达到上限后返回。但是这样做了就没有问题了吗?答案当然是否定的。

固定循环次数方式

这是最常见的版本,样板方法为:



比如:



这种方式的问题在于: 不带 backoff 的重试,对于下游来说会在失败发生时进一步遇到更多的请求压力,继而进一步恶化。

带固定 delay 的方式

在失败之后,进行固定间隔的 delay, delay 的方式按照是方法本身是异步还是同步的,可以通过定时器或则简单的 Thread.sleep 实现,样板方法为:



比如:



这种方式的问题在于: 虽然这次带了固定间隔的 backoff,但是每次重试的间隔固定,此时对于下游资源的冲击将会变成间歇性的脉冲;特别是当集群都遇到类似的问题时,步调一致的脉冲,将会最终对资源造成很大的冲击,并陷入失败的循环中。


想想一下,一群鼓手,协调一致地击鼓时所产生的效果。

带随机 delay 的方式:

和 2 中固定间隔的 delay 不一样,现在采用随机 backoff 的方式,即具体的 delay 时间,在一个最小值和最大值之间浮动,样板代码如下:



比如:



或则一个类似的异步版本:




这种方式的问题在于:虽然现在解决了 backoff 的时间集中的问题,对时间进行了随机打散,但是依然存在下面的问题:


  • 如果依赖的底层服务持续地失败,改方法依然会进行固定次数的尝试,并不能起到很好的保护作用

  • 对结果是否符合预期,是否需要进行重试依赖于异常

  • 无法针对异常进行精细化的控制,如只针部分异常进行重试。

可进行细粒度控制的重试

比如可以针对特定的异常来说,其样板代码为:



一般这个时候,代码已经相对来说比较复杂了,个人推荐使用 resilience4j-retry 或则 spring-retry 等库来进行组合,减少自己编写时维护成本,比如以 resilience4j-retry 为例,其可以使用配置代码对重试策略进行细粒度的控制,比如:


RetryConfig config = RetryConfig.custom()  .maxAttempts(2)  .waitDuration(Duration.ofMillis(1000))  .retryOnResult(response -> response.getStatus() == 500)  .retryOnException(e -> e instanceof WebServiceException)  .retryExceptions(IOException.class, TimeoutException.class)  .ignoreExceptions(BunsinessException.class, OtherBunsinessException.class)  .build();RetryRegistry registry = RetryRegistry.of(config);Retry retryWithDefaultConfig = registry.retry("name1");CheckedFunction0<String> retryableSupplier = Retry  .decorateCheckedSupplier(retry, helloWorldService::sayHelloWorld);
复制代码


这种方式的问题在于: 虽然可以比较好的控制重试策略,但是对于下游资源持续性的失败,依然没有很好的解决。当持续的失败时,对下游也会造成持续性的压力。一般这种问题的解法,我们日常工作中都是通过一个开关来进行人工断路,另一个比较好的解法是和断路器结合。

和断路器结合

断路器 在每个家庭中都有,但是在软件工程上,看到大家应用的并不多。断路器模式 一般用在当下游资源失败后,但是失败恢复的时间不固定时,自动地进行探索式地恢复尝试,并且在遇到较多失败时,能够快速自动地断开,从而避免失败蔓延的一种模式。



有人将这种模式叫做『熔断器模式』,其实是错误的,能够「熔断」的,那是保险丝,而不是断路器,断路器来自于电气工程,如下图示:



在应用断路器时,需要对下游资源的每次调用都通过断路器,对代码具备一定的结构侵入性。常见的有 Hystrix 或 resilience4j.


// GivenCircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
// When I decorate my functionCheckedFunction0<String> decoratedSupplier = CircuitBreaker .decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");
复制代码


又或者


def callWithCircuitBreakerCS[](body: Callable[CompletionStage[T]]): CompletionStage[T]
复制代码


当断路器处于开断状态时,所有的请求都会直接失败,不再会对下游资源造成冲击,并能够在一段时间后,进行探索式的尝试,如果没有达到条件,可以自动地恢复到之前的闭合状态。

重试的一些其他实现

目前 重试 在 RxJava 、Reactor、Akka-Stream 等中也都有实现,不过所实现的组合子(operator/操作)实现的相对简单,在实践中,如果需要得到很好的效果,还需要配合断路器来进行,从而最大限度地进行保护下游。

对失败做出反应

在反应式宣言中,也有提到,对对失败做出反应,系统在遇到失败时,可以恢复,并隔离失败的组件,而不是不受控的失败。系统是否具备回弹性,对于线上正常安全生产有很大的影响。正确地实现“重试”,只是整个大图中非常小的一环,实际生产中还需要从架构、生产流程、编码细节处理,监控报警等多种手段入手。


失败(和“错误”相对照)

失败是一种服务内部的意外事件, 会阻止服务继续正常地运行。失败通常会阻止对于当前的、 并可能所有接下来的客户端请求的响应。和错误相对照, 错误是意料之中的,并且针各种情况进行了处理( 例如, 在输入验证的过程中所发现的错误), 将会作为该消息的正常处理过程的一部分返回给客户端。而失败是意料之外的, 并且在系统能够恢复至(和之前)相同的服务水平之前,需要进行干预。这并不意味着失败总是致命的(fatal), 虽然在失败发生之后, 系统的某些服务能力可能会被降低。错误是正常操作流程预期的一部分, 在错误发生之后, 系统将会立即地对其进行处理, 并将继续以相同的服务能力继续运行。失败的例子有:硬件故障、 由于致命的资源耗尽而引起的进程意外终止,以及导致系统内部状态损坏的程序缺陷。

回弹性: 系统在出现失败时依然保持即时响应性。这不仅适用于高可用的、 任务关键型系统——任何不具备回弹性的系统都将会在发生失败之后丢失即时响应性。回弹性是通过复制、 遏制、 隔离以及委托来实现的。失败的扩散被遏制在了每个组件内部, 与其他组件相互隔离, 从而确保系统某部分的失败不会危及整个系统,并能独立恢复。每个组件的恢复都被委托给了另一个(外部的)组件, 此外,在必要时可以通过复制来保证高可用性。(因此)组件的客户端不再承担组件失败的处理。

小结

写这篇文章和大家分享,抛砖引玉,大家感兴趣也可以看看自己负责的应用中目前对于重试的处理,以及一些主流的开源框架或者库中的处理。


本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


https://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ==&mid=2650408273&idx=1&sn=c0ebaf48b0261ec4b01bc10099608230&chksm=8396cd49b4e1445f1f7f63aa8ccbad52832f760401acf25037b8b2dcc37ca7bfb5eedcceb0de&scene=27#wechat_redirect


2020-06-25 10:043147

评论

发布
暂无评论
发现更多内容

【算法技术专题】如何用Java实现一致性 hash 算法( consistent hashing )(上)

洛神灬殇

算法 一致性hash 11月日更

监管打压加码!虚拟货币挖矿再遭围堵 “漏网之鱼”当休

CECBC

一起听、一起看、一起唱掀起Z世代青年社交浪潮

声网

人工智能 算法 音视频

在华为云专属月,找到开启互联网第二增长曲线的一把钥匙

脑极体

jodconverter实现在线预览

小鲍侃java

11月日更

这一次,Google 终于对 Web 自动化下手了!

星安果

chrome 自动化

【LeetCode】重新排序得到 2 的幂Java题解

Albert

算法 LeetCode 11月日更

赢在2022,面试官常问的软件测试面试题总结

六十七点五

软件测试 面试题 自动化测试 经验总结 测试工程师

科技热点周刊|马斯克套现 440 亿;苹果推出数字身份证;Meta 与微软合作;华为捐赠欧拉

青云技术社区

云计算 物联网

何止一个惨字形容,水滴Java面试一轮游,壮烈了,问啥啥不会,数据库血崩,我该怎么办?

Java 编程 程序员 面试

浏览器的几种防护策略

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

范学雷的专栏《深入剖析 Java 新特性》

IT蜗壳-Tango

11月日更

linux下清理系统缓存并释放内存

入门小站

Linux

你现在可以在元宇宙里 “打工”了!

CECBC

你不知道的开源分布式存储系统 Alluxio 源码完整解析(上篇)

腾源会

大数据 开源 数据湖

识别AI换脸!百度这项技术夺冠了!

百度大脑

人工智能 百度

架构设计

AHUI

「架构实战营」

NodeJs 深入浅出之旅:V8 内存分配🧙‍♂️

空城机

大前端 Node 11月日更

1 分钟学会 30 种编程语言

AlwaysBeta

在线假单词随机生成器

入门小站

工具

数据同步:教你如何实时把数据从 MySQL 同步到 OceanBase

OceanBase 数据库

数据库 开源 oceanbase 分布式,

新能源汽车补贴没了,行业还能快速发展吗?

石云升

学习笔记 新能源汽车 11月日更

中央银行、不平等和新技术:使用分布式账本、可编程合约和密码学的蓝图

CECBC

华为初面+综合面试(Java技术面)附上面试题,share给大家~

Java 编程 程序员 面试

直接破防了,阿里大咖DDD(领域驱动设计)不破不立,GitHub直接霸榜,今天share给大家~

编程 程序员 领域驱动

验证码

卢卡多多

图片验证码 11月日更

15 K8S之容器安全上下文

穿过生命散发芬芳

k8s 11月日更

请问软件测试和渗透测试的区别是什么?

喀拉峻

网络安全 渗透测试

你不知道的开源分布式存储系统 Alluxio 源码完整解析(下篇)

腾源会

大数据 开源

gitlab registry占用存储过大问题解决

ilinux

一个基于PoS共识算法的区块链案例

Regan Yue

区块链 共识算法 11月日更 细讲区块链

如何正确地实现重试(Retry)_架构_虎鸣_InfoQ精选文章