【稳定性123】Http Retrying重试机制

背景:

项目UI与数据存储系统交互时遇到断连(例如:返回503 Service Unavailable),导致控件响应失灵。为解决处理503以及加强重试机制,引入Guava Retrying。

 

 

参考:

https://www.jianshu.com/p/2e3cfc509d56

https://www.jianshu.com/p/a289dde63043

https://www.jianshu.com/p/557eb67bb3d8

 

重试机制适用场景

对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。

远程调用超时网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。

比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性

在很多业务场景中,为了排除系统中的各种不稳定因素,以及逻辑上的错误,并最大概率保证获得预期的结果,重试机制都是必不可少的。尤其是调用远程服务,在高并发场景下,很可能因为服务器响应延迟或者网络原因,造成我们得不到想要的结果,或者根本得不到响应。这个时候,一个优雅的重试调用机制,可以让我们更大概率保证得到预期的响应。


“提高系统稳定性”
 
 
 

Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。

在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。

但是Srping Retry必须是基于异常来进行控制。

定义参数:
  • 重试条件:遇到 RuntimeException
  • 重试次数:3
  • 重试策略:重试的时候等待 5S, 后面时间依次变为原来的 2 倍数。
  • 熔断机制:全部重试失败,则调用 recover() 方法。

 

 

 

guava-retrying 是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。该扩展包比较简单,大约包含了10个方法和类。

guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。

Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callablecall() 方法。

guava-retrying默认的阻塞策咯是通过Thread.sleep来实现的,也就是说通过让当前线程休眠来实现阻塞功能,这或许不是一种很好的选择;

guava-retrying功能强大,基本能满足我们常用的操作;如果不满足当前各种已有的策咯,可以选择分别继承WaitStrategyStopStrategyBlockStrategy来自定义自己的实现;

在实际使用过程种,我们可能会经常要调整重试次数、重试时间等策咯,所以我们可以将重试策咯的配置进行参数化保存,达到动态调节的目的;另外在使用的时候,也可以封装成util工具类供大家使用。
 
 
 
 1 Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
 2         .retryIfException()
 3         .retryIfResult(aBoolean -> Objects.equals(aBoolean, false))
 4         .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()))
 5         .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
 6         .withStopStrategy(StopStrategies.stopAfterAttempt(5))
 7         .withRetryListener(new RetryListener() {
 8             @Override
 9             public <V> void onRetry(Attempt<V> attempt) {
10                 System.out.print("retry time=" + attempt.getAttemptNumber());
11             }
12         }).build();
13 try {
14     retryer.call(() -> {
15         // 逻辑处理
16         return null;
17     });
18 } catch (Exception e) {
19     System.out.println("exception:" + e);
20 }

具体接口及相应的策咯:

  • newBuilder:创建RetryerBuilder对象,通过该类进行构建各种重试策咯;
  • retryIfException:抛出异常时重试,但抛出error不会重试;另外该方法还包含一个重载的方法,可以自定义针对异常的实现;
  • retryIfRuntimeException:见名知义,抛出RuntimeException时重试;
  • retryIfExceptionOfType:抛出指定异常类型时重试;
  • retryIfResult:根据具体的返回值选择重试;
  • withRetryListener:在重试的时候进行事件监听,这中间我们可以记录下错误日志什么的;可以注册多个事件监听器,会按照注册顺序依次调用;
  • withWaitStrategy:重试等待策略,核心策咯之一;
  • withStopStrategy:重试停止策略,核心策咯之一;
  • withBlockStrategy:重试阻塞策略,也就是两次重试的时间间隔的实现方式;
  • withAttemptTimeLimiter:单次任务执行时长限制(如果单次任务执行超时,则终止执行当前任务);
  • build:通过newBuilder构建了各种重试策咯,构建完成,还需要通过build方法借助Retryer来执行;

主要接口

序号接口描述备注
1 Attempt 一次执行任务  
2 AttemptTimeLimiter 单次任务执行时间限制 如果单次任务执行超时,则终止执行当前任务。NoAttemptTimeLimit,FixedAttemptTimeLimit。
3 BlockStrategies 任务阻塞策略 通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么),默认策略为:BlockStrategies.THREAD_SLEEP_STRATEGY
4 RetryException 重试异常  
5 RetryListener 自定义重试监听器 可以用于异步记录错误日志
6 StopStrategy 停止重试策略 StopAfterDelayStrategy, NeverStopStrategy, StopAfterAttemptStrategy
7 WaitStrategy 等待时长策略 (控制时间间隔),返回结果为下次执行时长。FixedWaitStrategy,RandomWaitStrategy,IncrementingWaitStrategy,ExponentialWaitStrategy,FibonacciWaitStrategy,ExceptionWaitStrategy,CompositeWaitStrategy。
8 Attempt 一次执行任务
9 Attempt 一次执行任务
 

一个完备的重试实现,要很好地解决如下问题:

  1. 什么条件下重试
  2. 什么条件下停止
  3. 如何停止重试
  4. 停止重试等待多久
  5. 如何等待
  6. 请求时间限制
  7. 如何结束
  8. 如何监听整个重试过程

并且,为了更好地封装性,重试的实现一般分为两步:

  1. 使用工厂模式构造重试器
  2. 执行重试方法并得到结果

一个完整的重试流程可以简单示意为:

graph LR
    A((Start)) -->|build| B(Retryer)
    B --> C{need call?}
    C -->|continue| D[call]
    D --> Z[call count++]
    Z --> C
    C -->|finished| E[result]
    E --> F((success))
    E --> G((failed ))
 
 
1. 配置依赖
 
Maven:
<!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying -->
<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>
Gradle:

compile("com.github.rholder:guava-retrying:2.0.0") {
  exclude group: 'com.google.guava', module: 'guava'
}

 
2.实现Callable
Callable的call方法中是你自己实际的业务调用。
Callable<Boolean> callable = new Callable<Boolean>() {
    public Boolean call() throws Exception {
        return true; // do something useful here
    }
};
 
3.通过RetryerBuilder构造Retryer
 
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
        .retryIfResult(Predicates.<Boolean>isNull())
        .retryIfExceptionOfType(IOException.class)
        .retryIfRuntimeException()
        .withStopStrategy(StopStrategies.stopAfterAttempt(3))
        .build();
 
 
4.使用重试器执行你的业务
 
retryer.call(callable);
 

下面是完整的参考实现。

 1 public Boolean test() throws Exception {
 2     //定义重试机制
 3     Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
 4             //retryIf 重试条件
 5             .retryIfException()
 6             .retryIfRuntimeException()
 7             .retryIfExceptionOfType(Exception.class)
 8             .retryIfException(Predicates.equalTo(new Exception()))
 9             .retryIfResult(Predicates.equalTo(false))
10 
11             //等待策略:每次请求间隔1s
12             .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
13 
14             //停止策略 : 尝试请求6次
15             .withStopStrategy(StopStrategies.stopAfterAttempt(6))
16 
17             //时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
18             .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
19 
20             .build();
21 
22     //定义请求实现
23     Callable<Boolean> callable = new Callable<Boolean>() {
24         int times = 1;
25 
26         @Override
27         public Boolean call() throws Exception {
28             log.info("call times={}", times);
29             times++;
30 
31             if (times == 2) {
32                 throw new NullPointerException();
33             } else if (times == 3) {
34                 throw new Exception();
35             } else if (times == 4) {
36                 throw new RuntimeException();
37             } else if (times == 5) {
38                 return false;
39             } else {
40                 return true;
41             }
42 
43         }
44     };
45     //利用重试器调用请求
46    return  retryer.call(callable);
47 }

 

 
 
 
附加:
HTTP 503
 

一、什么是HTTP Error 503 ?

web服务器不能处理HTTP请求,可能是临时超载或者是服务器进行维护。这意味着你需要忍耐一下,等待服务器的临时处理。在这种状态下,一些服务器可以简单的拒绝socket连接,否则会发生内容不一致的错误。

 

二、503错误在HTTP周期的流程:

1、从站点获得IP地址;

2、通过IP地址打开socket连接;

3、通过socket连接写入HTTP数据流;

4、等待响应,返回的数据流。该数据流包含由HTTP协议决定的状态代码值。然后解析数据流状态代码和其它信息。

 

三、出现 HTTP Error 503 的原因:

1、站点遭到攻击,在超过限制时报503错误,待攻击停止就可以恢复了;

2、站点规模较大,并发请求过多,这种建议修改优化程序或需要升级更高类型主机;

3、程序有错误,在短时间内产生多次工作进程崩溃,会因IIS7的快速故障防护功能而关闭程序池;

4、站点提供下载,当带宽超过限制时会报错,需停止下载功能,或者升级主机解决。

 
posted @ 2020-08-21 12:42  CathyGao2018  阅读(441)  评论(0编辑  收藏  举报