写点什么

如何正确地实现重试 (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:043098

评论

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

Hexo + Github从零搭建个人博客

梁歪歪 ♚

Hexo 博客搭建

企业网站如何快速被搜索引擎收录

源字节1号

阿里6月终于有HC了!耗时两月足足面试13轮成功入职阿里!拿到32*15Offer

Java全栈架构师

Java spring 程序员 面试 程序人生

Flutter 利用 Redux 中间件完成购物清单离线存储

岛上码农

flutter ios 前端 安卓开发 6月月更

华为云AppCube带你5分钟开发微信小程序

乌龟哥哥

6月月更

互联网电商项目天花板,从立项到交付快速落地,真正帮你解决大型互联网项目经验欠缺的短板

Java全栈架构师

程序员 面试 项目 架构设计 程序员进阶

答应我:监听日志文件变化的这三种方法你一定要会!推荐第三种!

Java全栈架构师

Java 程序员 面试 IDEA 代码人生

this和super的用法与区别

写代码两年半

继承 super javase this 6月月更

InfoQ 极客传媒 15 周年庆征文|聊聊 Kafka:Kafka 如何保证一致性

老周聊架构

kafka 架构 云原生 6月月更 InfoQ极客传媒15周年庆

Java设计模式学习总结

梁歪歪 ♚

设计模式

面试官:执行一条 SQL 语句,期间会发生什么?

Java全栈架构师

Java MySQL 数据库 程序员 面试

linux中删除特殊文件

入门小站

Linux

让开发效率飞速提升的跨端开发神器

Geek_99967b

小程序 小程序容器

跨平台方案的比较

Geek_99967b

小程序 小程序容器

使用IDE并不是懒癌表现

Geek_99967b

小程序 小程序容器

运维服务体系构建

阿泽🧸

运维体系 6月月更

Disruptor 高性能堆内队列 系列一

Nick

Java Disruptor 队列 高性能 6月月更

App中快速复用微信登录授权的一种方法

Speedoooo

APP开发 微信授权 微信登录

FinClip2022重要功能汇总

Speedoooo

微信小程序 APP开发 小程序容器 微信登录

InterpreterPattern-解释器模式

梁歪歪 ♚

设计模式

每日一题 | LeetCode 1 两数之和

武师叔

Python 算法 JAV A Leet Code 6月月更

Flutter的整体架构

Geek_99967b

小程序 小程序容器

scp 高效操作之避免 zsh 路径展开

Nick

Linux zsh 6月月更 高效操作 scp

leetcode 51. N-Queens N 皇后(困难)

okokabcd

LeetCode 搜索 算法与数据结构

「技术人生」第8篇:如何画业务大图

阿里巴巴中间件

阿里云 云原生 技术文章

LabVIEW控制Arduino采集热电偶温度数值(进阶篇—2)

不脱发的程序猿

单片机 LabVIEW Arduino VISA 采集热电偶温度数值

PC端实现运营小程序,是否能再创PC时代又一春!

Geek_99967b

小程序 小程序转app

深入浅出-如何安全的传输密码

梁歪歪 ♚

加密

5分钟了解SDN控制平面

穿过生命散发芬芳

SDN网络 6月月更

过去一周区块链热点回顾|BAYC项目具有被无限铸币的风险

区块链前沿News

Hoo

红利、辛苦钱、利润和工资【读书笔记】

FunTester

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