速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

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

评论

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

Rust 元宇宙 15 —— 细节和重构

Miracle

rust 元宇宙

技术架构演进的思考

gevin

架构演进

Git 报错:unable to update local ref

liuzhen007

28天写作 12月日更

「从0到1如何快速实现cli工具」

速冻鱼

大前端 cli JavaScrip 签约计划第二季 12月日更

Week1学习总结

guodongq

「架构实战营」

从实习到秋招成为一名安全工程师,我经历了什么

网络安全学海

面试 网络安全 信息安全 渗透测试 WEB安全

架构实战营-模块1-作业

Pyel

「架构实战营」

Spring AOP(一) AOP基本概念

程序员历小冰

spring aop 28天写作 12月日更

微信业务架构图&&“学生管理系统”毕业架构设计

guodongq

「架构实战营」

透过全球首个知识增强千亿大模型,看到中国AI差异化发展之路

脑极体

TypeScript 之 Class(下)

冴羽

JavaScript typescript 翻译 大前端

消费类电子线上问题定位,分析和解决落地

wood

硬件产品 28天写作 线上故障

架构实战营模块1课后作业

墨宝

架构实战 模块一作业

mj4ever

架构实战营

如何验证你的产品创意?

石云升

产品思维 28天写作 12月日更

毕业总结

小智

架构训练营

微信业务架构图&学生管理系统毕业架构设计

Spring

架构实战营

模块一作业

whoami

「架构实战营」

基于云的技术架构设计实践-第5篇

hackstoic

数据分析 云原生 数据可视化 业务分析 签约计划第二季

第一模块作业

Anlumina

「架构实战营」

学习总结

Anlumina

「架构实战营」

第一周作业

lv

日本公司诚招IT开发技术者

马农驾驾驾

Java c++ php Python 日语

GrowingIO Terraform 实践

GrowingIO技术专栏

运维 SRE Terraform 项目实践 资源编排

彻底弄懂死锁

李子捌

Java、 28天写作 12月日更

架构实战营三期--模块一作业

木几丶

架构实战营 #架构实战营

我粗心,有救吗?

Justin

心理学 成长 28天写作

Hoo虎符研究院 | Arweave调研报告

区块链前沿News

Arweave Hoo虎符 虎符交易所 虎符研究院 去中心化存储

我所理解的微服务

gevin

微服务 微服务架构

记录-今年最骄傲的一件事(2)

将军-技术演讲力教练

Python Qt GUI设计:菜单栏、工具栏和状态栏的使用方法(拓展篇—2)

不脱发的程序猿

Python qt GUI设计 Qt Creator 菜单栏、工具栏、状态栏

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