写点什么

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

评论

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

【Meetup预告】OpenMLDB+OneFlow:链接特征工程到模型训练,加速机器学习模型开发

第四范式开发者社区

人工智能 机器学习 数据库 AI 特征平台

数据库故障容错之系统时钟故障

CnosDB

时序数据库 开源社区 CnosDB 工程师有话说 CnosDB Tech Talk

Starfish Os打造的元宇宙生态,跟MetaBell的合作只是开始

鳄鱼视界

Bootstrap警告和轮播插件详解【前端Bootstrap框架】

恒山其若陋兮

7月月更

算法题每日一练---第12天:算式900

知心宝贝

程序员 算法 前端 后端 7月月更

小程序助力智能家居生态平台

Geek_99967b

物联网,

SpringBoot 整合 Swagger 自动生成在线API文档

宁在春

springboot swagger 7月月更

【函数式编程实战】(十) 优雅的处理代码中的时间类

小明Java问道之路

Lambda java8 Stream API 7月月更 签约计划第三季

数据中台建设(三):数据中台架构介绍

Lansonli

数据中台 7月月更

网络安全漏洞分析与漏洞复现

网络安全学海

网络安全 安全 渗透测试 WEB安全 漏洞挖掘

SpringBoot整合Minio 项目中使用自己文件存储服务器

宁在春

springboot Minio 7月月更

机器学习如何做到疫情可视化——疫情数据分析与预测实战

是Dream呀

人工智能 机器学习 爬虫 数据可视化 疫情分析

C# 之 方法参数传递机制

陈言必行

7月月更

leetcode 452. Minimum Number of Arrows to Burst Balloons 用最少数量的箭引爆气球(中等)

okokabcd

LeetCode 数据结构与算法 贪心算法

计算机视觉中Python如何实现图像操作与处理

迷彩

Python 计算机视觉 图像处理 Pillow 7月月更

C# 窗体应用使用对象绑定 DataGridView 数据绑定

IC00

C# 7月月更

SRv6初登场

穿过生命散发芬芳

7月月更 SRv6

浏览器视频帧操作方法 requestVideoFrameCallback() 简介

devpoint

html5 视频处理 requestAnimationFrame 7月月更

融云 IM & RTC 能力上新盘点

融云 RongCloud

Linux环境快速搭建elasticsearch6.5.4集群和Head插件

程序员欣宸

Java elasticsearch 7月月更

Prometheus 运维工具 Promtool (四)TSDB 功能

耳东@Erdong

Prometheus 7月月更 签约计划第三季 Promtool

Redis设计规范

知识浅谈

redis' redis 精讲

Starfish Os X MetaBell战略合作,元宇宙商业生态更进一步

股市老人

《我的Vivado实战—单周期CPU指令分析》

攻城狮杰森

cpu 计算机组成原理 7月月更 vivado 计算机科学与技术

汽车智能应用生态的下一个趋势:车载小程序

Geek_99967b

车联网 物联网,

MySQL进阶--存储过程以及自定义函数

Java学术趴

7月月更

一文读懂Plato Farm的ePLATO,以及其高溢价缘由

股市老人

算法题每日一练---第11天:第39级台阶

知心宝贝

程序员 算法 前端 后端 7月月更

鲜衣怒马散尽千金,Vue3.0+Tornado6前后端分离集成Web3.0之Metamask钱包区块链虚拟货币三方支付功能

刘悦的技术博客

Python 区块链 Vue 加密货币 虚拟货币

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