服务治理之重试篇
一、背景
什么是重试?
一种保障机制,why not try again!
无论是单体服务模块化的调用,或是微服务当道的今天服务间的相互调用。一次业务请求包含了太多的链条环扣,每一扣的失败都会导致整个请求的失败。因此需要保障每个环节的可用性。
二、动态策略配置
1、基本配置项
涉及重试,我们所需要关心的几点基本包括:什么时候重试?重试多少次?每次重试的间隔?
也即:重试异常、最大重试次数、重试间隔。
1)重试异常:
其实拿重试异常作为“什么时候重试?”的结论也不太完整。异常是一个通常的触发点,比如发生rpc超时了,此时需要触发重试机制以再次请求获取结果。但是有时,我们也会关注返回结果是否符合预期,比如,我们去请求某个状态,但是返回的和我们预期的不符(通常发成此种情况,一方面可能是数据层面的一致性问题,或者服务层面,服务提供方存在异常处理或者降级策略等),我们就需要去再次尝试获取。此处对于此类不再展开讨论。
2)最大重试次数:
最大,我们知道这是一个上限控制,重试也需要有终止条件(类似递归的终止),无论你的重试切入点是在入口,或者下游的某个链条,我们需要明确的是整个服务的【基本响应时间】要求必须得到保障。
重试是需要消耗额外时间的,包括每次的间隔及重试请求的耗时,因此必须综合考量配置。
3)重试间隔:
上面一点,我们已经提到重视间隔时间的概念,即,每次重试请求之间的间隔。
为什么会需要这个间隔呢?直接连续重试不行吗?其实,也是可以的,但是可能不合理。
间隔的存在涉及分散服务压力的需要,把请求平摊到更长的时间段内,减小下游服务的压力,比如我们在第一点中提到的,如果是因为下游服务触发降级导致的非预期结果重试,那么提供必要的间隔时间以供下游服务恢复服务能力则是必须的。
当然,重试间隔也可以有多种策略,比如每次在一个数值范围内随机间隔时间、逐渐递增间隔时间或者只是简单地固定长度间隔时间,可以根据实际的业务情景进行定制化的处理。
2、配置中心选择
其实此处,我们只是需要一种机制,即,配置的存储和配置变更的及时发现。任何实现都是可以的。
成熟的配置中心如 spring-cloud-config、apollo、nacos 等。或者基于 zookeeper、redis等,加上自己实现监听。
此处,我们简单介绍基于apollo配置中心。
详细可以参考:Apollo(阿波罗)配置中心Java客户端使用指南使用指南
如下,基于注解配置相应的监听 Listner,监听重试策略配置key变动
interestedKeys 需要监听的配置key。
3、配置
如下针对不同策略,添加不同的配置,以 name 区分:
[ { "name": "ht", //策略名称 "max": 2, //最大重试次数 "dur": 200, //重试间隔 "ex": "xxx.XXXException" //需要重试的异常 } ]
4、策略创建
策略的创建时机主要分为两部分,
一是服务启动时的初始化,此时需要拉取配置中心的配置进行写略的初始创建存储;
二是配置变更,监听获取到配置变化时对策略的重新初始化创建替换。
三、重试框架
目前流行的的包含或者专于重试实现的框架可能比较多,限于认知,仅就如下调研的几个做简要入门介绍:
1、guava-retrying
docs:https://github.com/rholder/guava-retrying
guava-retrying是基于Guava核心库的。
基本组成部分如下图:
Retryer:重试的入口和实际执行者。
StopStrategy:重试终止策略,也即什么时候停止重试。
WaitStrategy:间隔策略,确定每次重试间隔时间。
Attempt:代表每次重试请求,记录请求数据及结果。
基本依赖:
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>{version}</version>
</dependency>
完整的重试配置如下:
基于内存存储不同策略的重试器 RETRYERS
/**
* dynamic retry config
*/
@Slf4j
@Configuration
public class MyRetryConfig {
//retry apollo config
private static final String RETRY_RULES_CONFIG = "retry_rules";
//retry duration min 100 ms
private static long RETRY_DURATION_MIN = 100;
//retry duration max 500 ms
private static long RETRY_DURATION_MAX = 500;
//retry min attempts 1
private static int RETRY_ATTEMPTS_MIN = 1;
//retry max attempts 3
private static int RETRY_ATTEMPTS_MAX = 3;
//retry default config
private static String RETRY_DEFAULT_KEY = "default";
//retry default
private static Retryer RETRY_DEFAULT = RetryerBuilder.newBuilder()
.withWaitStrategy(WaitStrategies.fixedWait(RETRY_DURATION_MIN, TimeUnit.MILLISECONDS)) //retry duration
.withStopStrategy(StopStrategies.stopAfterAttempt(RETRY_ATTEMPTS_MAX)) //max retry times
.build();
//retryer
private static Map<String, Retryer> RETRYERS = new HashMap<>();
@PostConstruct
public void init() {
String retryConfig = ApolloConfig.getValue(RETRY_RULES_CONFIG, StringUtils.EMPTY);
try {
RETRYERS.clear();
MyRetry[] config = Optional.ofNullable(JJsonUtil.jsonToBeanArray(retryConfig, MyRetry[].class)).orElse(new MyRetry[0]);
for (MyRetry myRetry : config) {
RETRYERS.put(myRetry.getName(), buildRetryer(myRetry));
}
log.info("retry config init, config: {}", retryConfig);
} catch (IOException e) {
log.warn("init retry config failed");
}
}
/**
* apollo retry config listener
*
* listening key: RETRY_RULES_CONFIG
*
* @param changeEvent
*/
@ApolloConfigChangeListener(interestedKeys = {RETRY_RULES_CONFIG})
private void apolloConfigChangeEvent(ConfigChangeEvent changeEvent) {
log.info("retry config changed, reconfig retry: {}", changeEvent.getChange(RETRY_RULES_CONFIG));
init();
}
/**
* config and build retryer
*
* @param myRetry
* @return
*/
private Retryer buildRetryer(MyRetry myRetry) {
//suitable max
int max = NumUtils.getLimitedNumber(myRetry.getMax(), RETRY_ATTEMPTS_MIN, RETRY_ATTEMPTS_MAX);
//suitable duration
long duration = NumUtils.getLimitedNumber(myRetry.getDur(), RETRY_DURATION_MIN, RETRY_DURATION_MAX);
return buildRetryer(max, duration, parseRetryConfigEx(myRetry.getEx()), myRetry.getName());
}
/**
* retry trace exceptions config => Class
*
* @param config
* @return
*/
private Class<? extends Throwable>[] parseRetryConfigEx(String config) {
String [] exs = Optional.ofNullable(StringUtils.split(config, "|")).orElse(ArrayUtils.EMPTY_STRING_ARRAY);
List<Class<? extends Throwable>> exClazz = new ArrayList<>();
for (String ex : exs) {
try {
exClazz.add((Class<? extends Throwable>) Class.forName(ex));
} catch (ClassNotFoundException e) {
log.warn("parse retry ex config failed, config: {}, e: {}", ex, e.getMessage());
}
}
return exClazz.toArray(new Class[0]);
}
/**
* config and build retryer
*
* @param maxAttempts
* @param duration
* @param errorClasses
* @return
*/
public Retryer buildRetryer(int maxAttempts, long duration, Class<? extends Throwable>[] errorClasses, String name) {
RetryerBuilder builder = RetryerBuilder.newBuilder()
.withWaitStrategy(WaitStrategies.fixedWait(duration, TimeUnit.MILLISECONDS)) //retry dueation
.withStopStrategy(StopStrategies.stopAfterAttempt(maxAttempts)); //max retry times
//trace exceptions
for (Class<? extends Throwable> errorClass : errorClasses) {
builder.retryIfExceptionOfType(errorClass);
}
//retry listener
builder.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
log.info("retry attempt, times: {}, duration: {}", attempt.getAttemptNumber(), attempt.getDelaySinceFirstAttempt());
}
});
return builder.build();
}
/**
* get or default
*
* @param retryer
* @return
*/
public static Retryer getRetryer(String retryer) {
return RETRYERS.getOrDefault(StringUtils.defaultIfEmpty(retryer, RETRY_DEFAULT_KEY), RETRY_DEFAULT);
}
}
重试工具类:
基于默认策略或者指定策略的重试包装调用:
@Slf4j
public class RetryUtils {
private RetryUtils(){}
//default retry
public static <T> T callWithRetry(Callable<T> callable) throws Exception {
return callWithRetry(null, callable);
}
//custom retry
public static <T> T callWithRetry(String retryer, Callable<T> callable) throws Exception {
return (T) MyRetryConfig.getRetryer(retryer).call(callable);
}
}
调用:
List<Object> list = RetryUtils.callWithRetry(() -> xxxService.getXXXs(args));
2、spring-retry
docs:https://github.com/spring-projects/spring-retry
spring-retry 我们基于 RetryTemplate,使用方式和 guava-retrying 类似。spring-retry 支持基于注解的方式,此处不再展开讨论。
基本依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>{version}</version>
</dependency>
完整的配置:
/**
* dynamic retry config
*/
@Slf4j
@Configuration
public class MyRetryConfig {
//retry apollo config
private static final String RETRY_RULES_CONFIG = "retry_rules";
//retry duration min 100 ms
private static long RETRY_DURATION_MIN = 100;
//retry duration max 500 ms
private static long RETRY_DURATION_MAX = 500;
//retry min attempts 1
private static int RETRY_ATTEMPTS_MIN = 1;
//retry max attempts 3
private static int RETRY_ATTEMPTS_MAX = 3;
//retry default config
private static String RETRY_DEFAULT_KEY = "default";
//retry default
private static RetryTemplate RETRY_DEFAULT = RetryTemplate.builder()
.fixedBackoff(RETRY_DURATION_MIN) //retry duration
.maxAttempts(RETRY_ATTEMPTS_MAX) //max retry times
.build();
private static Map<String, RetryTemplate> RETRYERS = new HashMap<>();
@PostConstruct
public void init() {
String retryConfig = ApolloConfig.getValue(RETRY_RULES_CONFIG, StringUtils.EMPTY);
try {
RETRYERS.clear();
MyRetry[] config = Optional.ofNullable(JJsonUtil.jsonToBeanArray(retryConfig, MyRetry[].class)).orElse(new MyRetry[0]);
for (MyRetry myRetry : config) {
RETRYERS.put(myRetry.getName(), buildRetryTemplate(myRetry));
}
log.info("retry config init, config: {}", retryConfig);
} catch (IOException e) {
log.warn("init retry config failed");
}
}
/**
* apollo retry config listener
*
* listening key: RETRY_RULES_CONFIG
*
* @param changeEvent
*/
@ApolloConfigChangeListener(interestedKeys = {RETRY_RULES_CONFIG})
private void apolloConfigChangeEvent(ConfigChangeEvent changeEvent) {
log.info("retry config changed, reconfig retry: {}", changeEvent.getChange(RETRY_RULES_CONFIG));
init();
}
/**
* config and build retryTemplate
*
* @param myRetry
* @return
*/
private RetryTemplate buildRetryTemplate(MyRetry myRetry) {
//suitable max
int max = NumUtils.getLimitedNumber(myRetry.getMax(), RETRY_ATTEMPTS_MIN, RETRY_ATTEMPTS_MAX);
//suitable duration
long duration = NumUtils.getLimitedNumber(myRetry.getDur(), RETRY_DURATION_MIN, RETRY_DURATION_MAX);
return buildRetryTemplate(max, duration, parseRetryConfigEx(myRetry.getEx()), myRetry.getName());
}
/**
* retry trace exceptions config => Class
*
* @param config
* @return
*/
private Class<? extends Throwable>[] parseRetryConfigEx(String config) {
String [] exs = Optional.ofNullable(StringUtils.split(config, "|")).orElse(ArrayUtils.EMPTY_STRING_ARRAY);
List<Class<? extends Throwable>> exClazz = new ArrayList<>();
for (String ex : exs) {
try {
exClazz.add((Class<? extends Throwable>) Class.forName(ex));
} catch (ClassNotFoundException e) {
log.warn("parse retry ex config failed, config: {}, e: {}", ex, e.getMessage());
}
}
return exClazz.toArray(new Class[0]);
}
/**
* config and build retryTemplate
*
* @param maxAttempts
* @param duration
* @param errorClasses
* @return
*/
public RetryTemplate buildRetryTemplate(int maxAttempts, long duration, Class<? extends Throwable>[] errorClasses, String name) {
RetryTemplateBuilder builder = RetryTemplate.builder()
.maxAttempts(maxAttempts) //max retry times
.fixedBackoff(duration); //retry dueation
//trace exceptions
for (Class<? extends Throwable> errorClass : errorClasses) {
builder.retryOn(errorClass);
}
//retry listener
builder.withListener(new RetryListenerSupport(){
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.info("retry: {}", context);
}
});
return builder.build();
}
/**
* get or default
*
* @param retryTemplate
* @return
*/
public static RetryTemplate getRetryTemplate(String retryTemplate) {
return RETRYERS.getOrDefault(StringUtils.defaultIfEmpty(retryTemplate, RETRY_DEFAULT_KEY), RETRY_DEFAULT);
}
}
重试工具类:
@Slf4j
public class RetryUtils {
private RetryUtils(){}
//default retry
public static <T> T callWithRetry(RetryCallback<T, Exception> callback) throws Exception {
return callWithRetry(null, callback);
}
//custom retry
public static <T> T callWithRetry(String retryer, RetryCallback<T, Exception> callback) throws Exception {
return (T) MyRetryConfig.getRetryTemplate(retryer).execute(callback);
}
}
调用:
List<Object> list = RetryUtils.callWithRetry(context -> xxxService.getXXXs(args));
3、resilience4j-retry
Resilience4j 是一个轻量级的容错框架,提供包括熔断降级,流控及重试等功能。
详细参考文档:https://resilience4j.readme.io/docs/retry
基本依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>{version}</version>
</dependency>
完整配置:
此处使用 RetryRegistry 作为策略注册管理中心。
/**
* Resilience4j config
*/
@Slf4j
@Configuration
public class MyRetryConfig {
private static final String RETRY_RULES_CONFIG = "retry_rules";
//retry duration min 100 ms
private static long RETRY_DURATION_MIN = 100;
//retry min attempts 1
private static int RETRY_ATTEMPTS_MIN = 1;
//retry max attempts 3
private static int RETRY_ATTEMPTS_MAX = 3;
@Resource
private RetryRegistry retryRegistry;
@PostConstruct
public void init() {
initRetry(false);
}
/**
* init retry
*
* @param reInit config change reinit
*/
private void initRetry(boolean reInit) {
String retryConfig = ApolloConfig.getValue(RETRY_RULES_CONFIG, StringUtils.EMPTY);
try {
MyRetry[] config = Optional.ofNullable(JJsonUtil.jsonToBeanArray(retryConfig, MyRetry[].class)).orElse(new MyRetry[0]);
for (MyRetry myRetry : config) {
if (reInit) {
retryRegistry.replace(myRetry.getName(), Retry.of(myRetry.getName(), parseRetryConfig(myRetry)));
} else {
retryRegistry.retry(myRetry.getName(), parseRetryConfig(myRetry));
}
}
log.info("r4jConfigEvent, init retry: {}", retryConfig);
} catch (IOException e) {
log.warn("init retry config failed");
}
}
/**
* apollo retry config listener
*
* listening key: RETRY_RULES_CONFIG
*
* @param changeEvent
*/
@DependsOn(value = "retryRegistry")
@ApolloConfigChangeListener(interestedKeys = {RETRY_RULES_CONFIG})
private void apolloConfigChangeEvent(ConfigChangeEvent changeEvent) {
log.info("retry config changed, reconfig retry: {}", changeEvent.getChange(RETRY_RULES_CONFIG));
initRetry(true);
}
/**
* retry config => RetryConfig
*
* @param myRetry
* @return
*/
private RetryConfig parseRetryConfig(MyRetry myRetry) {
//suitable max
int max = NumUtils.getLimitedNumber(myRetry.getMax(), RETRY_ATTEMPTS_MIN, RETRY_ATTEMPTS_MAX);
//suitable duration
long duration = NumUtils.getLimitedNumber(myRetry.getDur(), RETRY_DURATION_MIN, RetryConfig.DEFAULT_WAIT_DURATION);
return configRetryConfig(max, duration, parseRetryConfigEx(myRetry.getEx()));
}
/**
* retry exception config => Class
*
* @param config
* @return
*/
private Class<? extends Throwable>[] parseRetryConfigEx(String config) {
String [] exs = Optional.ofNullable(StringUtils.split(config, "|")).orElse(ArrayUtils.EMPTY_STRING_ARRAY);
List<Class<? extends Throwable>> exClazz = new ArrayList<>();
for (String ex : exs) {
try {
exClazz.add((Class<? extends Throwable>) Class.forName(ex));
} catch (ClassNotFoundException e) {
log.warn("parse retry ex config failed, config: {}, e: {}", ex, e.getMessage());
}
}
return exClazz.isEmpty() ? null : exClazz.toArray(new Class[0]);
}
/**
* process retry config
*
* @param maxAttempts
* @param duration
* @param errorClasses
* @return
*/
public RetryConfig configRetryConfig(int maxAttempts, long duration, Class<? extends Throwable>[] errorClasses) {
return RetryConfig
.custom()
.maxAttempts(maxAttempts) //max retry times
.waitDuration(Duration.ofMillis(duration)) //retry duration
.retryExceptions(errorClasses) //tracing ex, if null trace all
.build();
}
}
结合注解使用:
@Retry(name = "xxx") //策略名称
切面会根据配置的策略名称从 RetryRegistry 查询获取相应的策略。
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=wxyihmw3bpxh
- JDK容器学习之Queue:ConcurrentLinkedQueue
- JDK容器学习之Queue: PriorityQueue
- React Native导航器之react-navigation使用
- Nginx 路由转发配置笔记
- React Native控件之ListView
- Java学习之深拷贝浅拷贝及对象拷贝的两种方式
- [周末课程]什么是“页面业务流程”分析思维导图?如何编写页面假JSON数据? &下一个前端组件“日历”
- Java并发学习之玩转线程池
- Java & PhantomJs 实现html输出图片
- 干货 | React Native实践之携程Moles框架
- Java并发学习之ThreadLocal使用及原理介绍
- ibeacon蓝牙技术简介
- Java并发学习之定时任务的几种玩法
- [视频直播]本周日先行者视频“React多级菜单
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- CentOS7安装配置 Redis的方法步骤
- Linux下Oracle如何导入导出dmp文件详解
- Linux中samba服务器的搭建教程
- linux环境搭建图数据库neo4j的讲解
- Linux编程之ICMP洪水攻击
- linux搭建squid代理服务器的完整步骤
- 使用wget递归镜像网站
- 整理Linux中字符串的相关操作技巧
- Ubuntu 16.04下无法安装.deb的解决方法
- 关于linux中系统输入输出的管理详解
- Linux下IP设置脚本的实例及遇到问题解决办法
- Linux与Windows文件互传(VMWare)
- 如何测试Linux下tcp最大连接数限制详解
- Linux中利用Vim对文件进行密码保护的方法详解
- Linux中禁止用户修改/重置密码