【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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

评论

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

浅谈Go语言反射

海风极客

Go 反射 11月月更

【Node.JS 】创建基本的web服务器

坚毅的小解同志

node.js 11月月更

Baklib知识分享|文档生命周期:确保您的文档产出效率

Baklib

Node.JS 】http的概念及作用

坚毅的小解同志

node.js 11月月更

支持向量机-线性SVM用于分类的原理

烧灯续昼2002

Python 机器学习 算法 sklearn 11月月更

【Node.JS 练习】时钟案例

坚毅的小解同志

11月月更

Spring事务的底层原理

千锋IT教育

融云 IM 和 RTC 服务,「助攻」智能物流等客户打通链路、完善生态

融云 RongCloud

IM RTC

云原生社区 Meetup 长沙站开始报名!

腾源会

云原生

筑道与寻术:华为云与汽车产业的时代问答

脑极体

【Node.JS 练习】考试成绩整理

坚毅的小解同志

node.js 11月月更

【Node.JS】写入文件内容

坚毅的小解同志

node.js 11月月更

java就业方向有哪些?

小谷哥

Java反射(完)类加载和反射获取信息

浅辄

Java 反射 11月月更

【Node.js练习】根据不同的url响应不同的html内容

坚毅的小解同志

node.js 11月月更

【Node.JS 】服务器相关的概念

坚毅的小解同志

11月月更

【Node.JS 】path路径模块

坚毅的小解同志

node.js 11月月更

【Node.JS】buffer类缓冲区

坚毅的小解同志

node.js 11月月更

西安java培训学习有哪些方法?

小谷哥

【愚公系列】2022年11月 微信小程序-页面生命周期

愚公搬代码

11月月更

java并发编程挑战与原理剖析

想要飞的猪

synchronized volatile原理

一文搞懂Go1.18泛型新特性

海风极客

Go 11月月更

融云推送服务:独享推送通道,更高并发能力,应用运营必备

融云 RongCloud

互联网 消息

我服了!SpringBoot升级后这服务我一个星期都没跑起来!(下)

艾小仙

Java spring springbot

Linux 系统目录结构

二哈侠

Linux tar 11月月更 Linux目录

【Node.JS 】服务器相关的概念

坚毅的小解同志

kubernetes1.15极速部署prometheus和grafana

程序员欣宸

Kubernetes Prometheus 11月月更

【Node.js】模块化学习

坚毅的小解同志

node.js 11月月更

【Node.js练习】web服务器案例

坚毅的小解同志

node.js 11月月更

【Node.JS】事件的绑定与触发

坚毅的小解同志

node.js 11月月更

Hive 与 HBase 之间的区别和联系

千锋IT教育

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