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

30. FeignClient 实现重试

unknown
• 阅读 746

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 实现重试

点赞
收藏
评论区
推荐文章

暂无数据

unknown
unknown
Lv1
男 · rrrr · rrrrrrrr
rrrrr
文章
0
粉丝
17
获赞
0
热门文章

暂无数据