本系列代码地址:https://github.com/JoJoTec/sp…
须要重试的场景
微服务零碎中,会遇到 在线公布,个别的公布更新策略是:启动一个新的,启动胜利之后,敞开一个旧的,直到所有的旧的都被敞开。Spring Boot 具备优雅敞开的性能,能够保障申请解决完再敞开,同时会回绝新的申请。对于这些回绝的申请,为了保障用户体验不受影响,是须要重试的。
云上部署的微服务,对于同一个服务,同一个申请,很可能不会所有实例都同时异样,例如:
- Kubernetes 集群部署的实例,可能同一个虚拟机 Node 在闲时部署了多个不同微服务实例,当压力变大时,就须要迁徙和扩容。这时候因为不同的微服务压力不同,过后处于哪一个 Node 也说不定,有的可能处于压力大的,有的可能处于压力小的。对于同一个微服务,可能并不会所有实例位于的 Node 压力都大。
- 云上部署个别会跨可用区部署,如果有一个可用区异样,另一个可用区还能够持续提供服务。
- 某个业务触发了 Bug,导致实例始终在 GC,然而这种申请个别很不常见,不会发到所有实例上。
这时候,就须要咱们对申请进行无感知的重试。
重试须要思考的问题
- 重试须要重试与 之前不同的实例,甚至是不处于同一个虚拟机 Node 的实例,这个次要通过 LoadBalancer 实现,能够参考之前的 LoadBalancer 局部。前面的文章,咱们还会改良 LoadBalancer
-
重试须要思考到底什么申请能重试,以及什么异样能重试:
- 假如咱们有查问接口,和没有做幂等性的扣款接口,那么很直观的就能感觉出 查问接口是能够重试的,没有做幂等性的扣款接口是不能重试的。
- 业务上不能重试的接口,对于非凡的异样(其实是示意申请并没有收回去的异样),咱们是能够重试的。尽管是没有做幂等性的扣款接口,然而如果抛出的是起因是 Connect Timeout 的 IOException,这样的异样代表申请还没有收回去,是能够重试的。
- 重试策略:重试几次,重试距离。类比多处理器编程模式中的 Busy Spin 策略会造成很大的总线通量从而升高性能这个景象,如果失败立即重试,那么在某一个实例异样导致超时的时候,会在同一时间有很多申请重试到其余实例。最好加上肯定提早。
应用 resilience4j 实现 FeignClient 重试
FeignClient 自身带重试,然而重试策略绝对比较简单,同时咱们还想应用断路器以及限流器还有线程隔离,resilience4j 就蕴含这些组件。
原理简介
Resilience4J 提供了 Retryer 重试器,官网文档地址:https://resilience4j.readme.i…
从配置上就能了解其中的原理,然而 官网文档配置并不全面,如果想看所有的配置,最好还是通过源码:
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: