31

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxNDEwNjk5OQ%3D%3D&%3Bmid=2650408273&%3Bidx=1&%3Bsn=c0ebaf48b0261ec4b01bc10099608230
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

QFbIfyE.gif

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

什么是重试

6rAVv26.png!web

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

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

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

  • 彩票没中(结果不符合预期)

  • 上次没带钱(条件不符合)

  • 彩票门店没开门(结果异常)

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

VFFFJj2.png!web

什么是正确的重试

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

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

▐   固定循环次数方式

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

yUzmM3e.png!web

比如:

fu2MbaJ.png!web

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

▐   带固定 delay 的方式

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

3UJNFvQ.png!web

比如:

7B7fuqb.png!web

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

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

▐  带随机delay的方式:

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

aemyaav.png!web

比如:

iEVnM3i.png!web

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

f2yMZri.png!webAVruiaB.png!web

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

  • 如果依赖的底层服务持续地失败,改方法依然会进行固定次数的尝试,并不能起到很好的保护作用

  • 对结果是否符合预期,是否需要进行重试依赖于异常

  • 无法针对异常进行精细化的控制,如只针部分异常进行重试。

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

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

NzuAJve.png!web

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

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

和断路器结合

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

2Q3Aryn.png!web

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

AF3Uzy7.png!web

在应用断路器时,需要对下游资源的每次调用都通过断路器,对代码具备一定的结构侵入性。常见的有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。不断提升消费这的体验和活跃,提升商家服务的效率和能力,促进商家业务增长。

联系电话:18651806651

邮箱:postbox:: [email protected]

✿  拓展阅读

QBNRFfM.png!web

e2QZFri.png!web

EFnAfaE.png!web

作者| 虎鸣

编辑| 橙子君

出品| 阿里巴巴新零售淘系技术

veE3IrM.jpg!web

uyQNNnZ.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK