专栏目录
17.Eureka的实例配置 18.Eureka的客户端核心设计和配置 1. 背景 2.微服务框架需要考虑的问题 3.Eureka Server 与 API 网关要考虑的问题 4.maven依赖回顾以及项目框架结构 5.所有项目的parent与spring-framework-common说明 6.微服务特性相关的依赖说明 19.Eureka的服务端设计与配置 7.从Bean到SpringCloud 8.理解 NamedContextFactory 9.如何理解并定制一个Spring Cloud组件 10.使用Log4j2以及一些核心配置 11.Log4j2 监控相关 12.UnderTow 简介与内部原理 13.UnderTow 核心配置 14.UnderTow AccessLog 配置介绍 15.UnderTow 订制 16.Eureka架构和核心概念 20. 启动一个 Eureka Server 集群 21.Spring Cloud LoadBalancer简介 22.Spring Cloud LoadBalancer核心源码 23.订制Spring Cloud LoadBalancer 24.测试Spring Cloud LoadBalancer 25.OpenFeign简介与使用 26.OpenFeign的组件 27.OpenFeign的生命周期-创建代理 28.OpenFeign的生命周期-进行调用 29.Spring Cloud OpenFeign 的解析(1) 30. FeignClient 实现重试 31. FeignClient 实现断路器以及线程隔离限流的思路 32. 改进负载均衡算法 33. 实现重试、断路器以及线程隔离源码 34.验证重试配置正确性 35. 验证线程隔离正确性 36. 验证断路器正确性 37. 实现异步的客户端封装配置管理的意义与设计 38. 实现自定义 WebClient 的 NamedContextFactory 39. 改造 resilience4j 粘合 WebClient 40. spock 单元测试封装的 WebClient(上) 40. spock 单元测试封装的 WebClient(下) 41. SpringCloudGateway 基本流程讲解(1) 41. SpringCloudGateway 基本流程讲解(2) 42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷 43.为何 SpringCloudGateway 中会有链路信息丢失 44.避免链路信息丢失做的设计(1) 44.避免链路信息丢失做的设计(2) 45. 实现公共日志记录

30. FeignClient 实现重试

干货满满张哈希
• 阅读 258

30. FeignClient 实现重试

本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

需要重试的场景

微服务系统中,会遇到在线发布,一般的发布更新策略是:启动一个新的,启动成功之后,关闭一个旧的,直到所有的旧的都被关闭。Spring Boot 具有优雅关闭的功能,可以保证请求处理完再关闭,同时会拒绝新的请求。对于这些拒绝的请求,为了保证用户体验不受影响,是需要重试的。

云上部署的微服务,对于同一个服务,同一个请求,很可能不会所有实例都同时异常,例如:

  1. Kubernetes 集群部署的实例,可能同一个虚拟机 Node 在闲时部署了多个不同微服务实例,当压力变大时,就需要迁移和扩容。这时候由于不同的微服务压力不同,当时处于哪一个 Node 也说不定,有的可能处于压力大的,有的可能处于压力小的。对于同一个微服务,可能并不会所有实例位于的 Node 压力都大。
  2. 云上部署一般会跨可用区部署,如果有一个可用区异常,另一个可用区还可以继续提供服务。
  3. 某个业务触发了 Bug,导致实例一直在 GC,但是这种请求一般很不常见,不会发到所有实例上。

这时候,就需要我们对请求进行无感知的重试。

重试需要考虑的问题

  1. 重试需要重试与之前不同的实例,甚至是不处于同一个虚拟机 Node 的实例,这个主要通过 LoadBalancer 实现,可以参考之前的 LoadBalancer 部分。后面的文章,我们还会改进 LoadBalancer
  2. 重试需要考虑到底什么请求能重试,以及什么异常能重试:
    • 假设我们有查询接口,和没有做幂等性的扣款接口,那么很直观的就能感觉出查询接口是可以重试的,没有做幂等性的扣款接口是不能重试的
    • 业务上不能重试的接口,对于特殊的异常(其实是表示请求并没有发出去的异常),我们是可以重试的。虽然是没有做幂等性的扣款接口,但是如果抛出的是原因是 Connect Timeout 的 IOException,这样的异常代表请求还没有发出去,是可以重试的
  3. 重试策略:重试几次,重试间隔。类比多处理器编程模式中的 Busy Spin 策略会造成很大的总线通量从而降低性能这个现象,如果失败立刻重试,那么在某一个实例异常导致超时的时候,会在同一时间有很多请求重试到其他实例。最好加上一定延迟。

使用 resilience4j 实现 FeignClient 重试

FeignClient 本身带重试,但是重试策略相对比较简单,同时我们还想使用断路器以及限流器还有线程隔离,resilience4j 就包含这些组件。

原理简介

Resilience4J 提供了 Retryer 重试器,官方文档地址:https://resilience4j.readme.io/docs/retry

从配置上就能理解其中的原理,但是官方文档配置并不全面,如果想看所有的配置,最好还是通过源码:

RetryConfigurationProperties.java

//重试间隔,默认 500ms
@Nullable
private Duration waitDuration;

//重试间隔时间函数,和 waitDuration 只能设置一个,默认就是 waitDuration
@Nullable
private Class<? extends IntervalBiFunction<Object>> intervalBiFunction;

//最大重试次数,包括本身那次调用
@Nullable
private Integer maxAttempts;

//通过抛出的异常判断是否重试,默认是只要有异常就会重试
@Nullable
private Class<? extends Predicate<Throwable>> retryExceptionPredicate;

//通过结果判断是否重试,默认是只要获取到结果就不重试
@Nullable
private Class<? extends Predicate<Object>> resultPredicate;

//配置抛出这些异常以及子类则会重试
@SuppressWarnings("unchecked")
@Nullable
private Class<? extends Throwable>[] retryExceptions;

//配置抛出这些异常以及子类则不会重试
@SuppressWarnings("unchecked")
@Nullable
private Class<? extends Throwable>[] ignoreExceptions;

//启用 ExponentialBackoff 延迟算法,初次重试延迟时间为 waitDuration,之后每次重试延迟时间都乘以 exponentialBackoffMultiplier,直到 exponentialMaxWaitDuration
@Nullable
private Boolean enableExponentialBackoff;

private Double exponentialBackoffMultiplier;

private Duration exponentialMaxWaitDuration;

//启用随机延迟算法,范围是 waitDuration - randomizedWaitFactor*waitDuration ~ waitDuration + randomizedWaitFactor*waitDuration
@Nullable
private Boolean enableRandomizedWait;

private Double randomizedWaitFactor;

@Nullable
private Boolean failAfterMaxAttempts;

引入 resilience4j-spring-boot2 的依赖,就可以通过 Properties 配置的方式去配置 Retryer 等所有 resilience4j 组件,例如:

application.yml

resilience4j.retry:
  configs:
    default:
      ## 最大重试次数,包括第一次调用
      maxRetryAttempts: 2
      ## 重试等待时间
      waitDuration: 500ms
      ## 启用随机等待时间,范围是 waitDuration - randomizedWaitFactor*waitDuration ~ waitDuration + randomizedWaitFactor*waitDuration
      enableRandomizedWait: true
      randomizedWaitFactor: 0.5
    test-client1:
      ## 最大重试次数,包括第一次调用
      maxRetryAttempts: 3
      ## 重试等待时间
      waitDuration: 800ms
      ## 启用随机等待时间,范围是 waitDuration - randomizedWaitFactor*waitDuration ~ waitDuration + randomizedWaitFactor*waitDuration
      enableRandomizedWait: true
      randomizedWaitFactor: 0.5  

这样,我们就可以通过如下代码,获取到配置对应的 Retryer:

@Autowired
RetryRegistry retryRegistry;
//读取 resilience4j.retry.configs.test-client1 下的配置,构建 Retry,这个 Retry 命名为 retry1
Retry retry1 = retryRegistry.retry("retry1", "test-client1");
//读取 resilience4j.retry.configs.default 下的配置,构建 Retry,这个 Retry 命名为 retry1
//不指定配置名称即使用默认的 default 下的配置
Retry retry2 = retryRegistry.retry("retry2");

引入 resilience4j-spring-cloud2 的依赖,就相当于引入了 resilience4j-spring-boot2 的依赖。并在其基础上,针对 spring-cloud-config 的动态刷新 RefreshScope 机制,增加了兼容。

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-cloud2</artifactId>
</dependency>

使用 resilience4j-feign 给 OpenFeign 添加重试

官方提供了粘合 OpenFeign 的依赖库,即 resilience4j-feign

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-feign</artifactId>
</dependency>

接下来,我们使用这个依赖,给 OpenFeign 添加重试,首先启用 OpenFeign Client 并指定默认配置:

OpenFeignAutoConfiguration

@EnableFeignClients(value = "com.github.jojotech", defaultConfiguration = DefaultOpenFeignConfiguration.class)

在这个默认配置中,通过覆盖默认的 Feign.Builder 的方式粘合 resilience4j 添加重试:

@Bean
public Feign.Builder resilience4jFeignBuilder(
        List<FeignDecoratorBuilderInterceptor> feignDecoratorBuilderInterceptors,
        FeignDecorators.Builder builder
) {
    feignDecoratorBuilderInterceptors.forEach(feignDecoratorBuilderInterceptor -> feignDecoratorBuilderInterceptor.intercept(builder));
    return Resilience4jFeign.builder(builder.build());
}

@Bean
public FeignDecorators.Builder defaultBuilder(
        Environment environment,
        RetryRegistry retryRegistry
) {
    String name = environment.getProperty("feign.client.name");
    Retry retry = null;
    try {
        retry = retryRegistry.retry(name, name);
    } catch (ConfigurationNotFoundException e) {
        retry = retryRegistry.retry(name);
    }

    //覆盖其中的异常判断,只针对 feign.RetryableException 进行重试,所有需要重试的异常我们都在 DefaultErrorDecoder 以及 Resilience4jFeignClient 中封装成了 RetryableException
    retry = Retry.of(name, RetryConfig.from(retry.getRetryConfig()).retryOnException(throwable -> {
        return throwable instanceof feign.RetryableException;
    }).build());

    return FeignDecorators.builder().withRetry(
            retry
    );
}

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

30. FeignClient 实现重试

评论区
推荐文章

暂无数据