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

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

什么是重试


image.png

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

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

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

  • 彩票没中(结果不符合预期)
  • 上次没带钱(条件不符合)
  • 彩票门店没开门(结果异常)

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

image.png


什么是正确的重试

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

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


▐ 固定循环次数方式

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

image.png

比如:

image.png

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


▐ 带固定 delay 的方式

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

image.png

比如:

image.png

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


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


▐ 带随机delay的方式:

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

image.png

比如:

image.png



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

image.png

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

  • 如果依赖的底层服务持续地失败,改方法依然会进行固定次数的尝试,并不能起到很好的保护作用
  • 对结果是否符合预期,是否需要进行重试依赖于异常
  • 无法针对异常进行精细化的控制,如只针部分异常进行重试。

▐ 可进行细粒度控制的重试

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

image.png

一般这个时候,代码已经相对来说比较复杂了,个人推荐使用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);

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


和断路器结合

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

image.png

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

image.png

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

// Given
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
// When I decorate my function
CheckedFunction0<String> decoratedSupplier = CircuitBreaker
        .decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");

又或者

def callWithCircuitBreakerCS[T](body: Callable[CompletionStage[T]]): CompletionStage[T]

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


重试的一些其他实现

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

对失败做出反应

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


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

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


回弹性:

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


小结

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


淘系 IM 消息平台

我们负责阿里新零售领域 IM 消息平台的建设,通过 IM即时通讯产品(push、聊天机器人、单聊、群聊、消息号和聊天室)构建连接消费者和商家的沟通和触达渠道,我们每天服务上亿消费者和数百万商家,处理百亿级的消费规模,支撑了直播互动、客服服务、商家群运营、品牌资讯、营销推送等电商领域 BC 互通的业务场景;同时,我们在消费者的购物体验上不断探索创新——直播、AR试用、游戏互动,为新的购物玩法提供灵活稳定的基础设施,实现阿里电商生态重要支点,为上百家APP 提供安全、稳定、标准化的电商组件SDK。不断提升消费这的体验和活跃,提升商家服务的效率和能力,促进商家业务增长。

邮箱 :limin.llm@alibaba-inc.com


——————————————————————————————————————

本篇内容来自于阿里巴巴淘系技术部虎鸣。

相关文章
|
4月前
|
消息中间件 Kafka Python
Producer的错误处理与重试机制
【8月更文第29天】在分布式系统中,消息传递是核心组件之一,它通常通过消息队列(如 Kafka、RabbitMQ 或其他)来实现。当生产者尝试将消息发送到消息队列时,可能会遇到各种类型的故障,例如网络中断、服务器不可用等。为了确保消息的可靠传递,需要实现有效的错误处理和重试机制。
130 2
|
API
出现连接超时错误的原因
出现连接超时错误的原因
596 2
|
数据采集 网络协议 Java
HTTP调用:你考虑到超时、重试、并发了吗?
今天,我们一起聊聊进行 HTTP 调用需要注意的超时、重试、并发等问题。
347 0
|
7月前
curl --retry 5 --retry-delay 2 -o curl 超时与重试
curl --retry 5 --retry-delay 2 -o curl 超时与重试
60 0
|
Java Maven Spring
spring-retry实现方法请求重试
spring-retry实现方法请求重试
82 0
|
分布式计算 Spark
SPARK outputDeterministicLevel的作用--任务全部重试或者部分重试
SPARK outputDeterministicLevel的作用--任务全部重试或者部分重试
85 0
|
应用服务中间件
高并发下hystrix熔断超时及concurrent.RejectedExecutionException: Rejected command because thread-pool queueSiz...
高并发下hystrix熔断超时及concurrent.RejectedExecutionException: Rejected command because thread-pool queueSiz...
286 0
|
设计模式 存储 运维
Java之Retry重试机制详解
Java之Retry重试机制详解
613 0
Java之Retry重试机制详解
|
负载均衡 Java 测试技术
聊聊openfeign的超时和重试
聊聊openfeign的超时和重试
1833 0