服务治理之重试篇

一、背景

什么是重试?

一种保障机制,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 区分:

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

四、附加订阅

posted @ 2020-09-04 14:52  WindWant  阅读(822)  评论(1编辑  收藏  举报
文章精选列表