Путь к обновлению Spring Cloud — Хокстон — 5. Реализация повторной попытки вызова микросервиса

Spring Cloud

Ведущие реализаторы верны

Мы продолжаем использоватьresilience4jРеализовать повтор, согласно предыдущей статьеSpring Cloud Upgrade Road — Hoxton — 4. Используйте Resilience4j для обеспечения изоляции и прерывания цепи на уровне экземпляра, мы загрузилиRetryReqistryЭта основная конфигурацияBean.

RetryСвязанная конфигурация:create-and-configure-retry

Наша конфигурация здесь:

resilience4j.retry:
  configs:
    default:
      maxRetryAttempts: 2
      waitDuration: 1
      retryExceptions:
        - java.lang.Exception
    service-provider2:
      maxRetryAttempts: 4

Обратитесь к нашему предыдущемуLoadBalancerConfig

//对于非返回200的接口,抛出异常
if (execute.status() != HttpStatus.OK.value()) {
    throw new ResponseWrapperException(execute.toString(), execute);
}

Таким образом, пока все исключения настроены на повторную попытку, она также будет повторяться для возвратов, отличных от 200.

Чтобы реализовать повторную попытку, она должна быть до того, как сработает балансировщик нагрузки, потому чтоSpring-CloudТам может быть много связующего кода вFeignBlockingLoadBalancerClientМетод может быть не очень масштабируемым, здесь мы используем фасетный метод для реализации повторной попытки.

CustomizedCircuitBreakerAspect

//配置哪些包下的FeignClient进行重试,必须含有@FeignClient注解
@Around("execution(* com.github.hashjang.hoxton..*(..)) && @within(org.springframework.cloud.openfeign.FeignClient)")
public Object feignClientWasCalled(final ProceedingJoinPoint pjp) throws Throwable {
    boolean isGet = false;
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    FeignClient annotation = signature.getMethod().getDeclaringClass().getAnnotation(FeignClient.class);
    String serviceName = annotation.value();
    if (StringUtils.isBlank(serviceName)) {
        serviceName = annotation.name();
    }

    //查看是否是GET请求
    RequestMapping requestMapping = signature.getMethod().getAnnotation(RequestMapping.class);
    if (requestMapping != null &&
            (requestMapping.method().length == 0 ||
                    Arrays.asList(requestMapping.method()).contains(RequestMethod.GET))
    ) {
        isGet = true;
    }
    GetMapping getMapping = signature.getMethod().getAnnotation(GetMapping.class);
    if (getMapping != null) {
        isGet = true;
    }
    Retry retry;
    try {
        retry = retryRegistry.retry(serviceName, serviceName);
    } catch (ConfigurationNotFoundException e) {
        retry = retryRegistry.retry(serviceName);
    }
    if (!isGet) {
        //非GET请求,只有在断路器打开的情况下,才会重试
        retry = Retry.of(serviceName, RetryConfig.from(retry.getRetryConfig()).retryExceptions().retryOnException(throwable -> {
            Throwable cause = throwable.getCause();
            if (cause instanceof CallNotPermittedException) {
                //对于断路器,不区分方法,都重试,因为没有实际调用
                log.info("retry on circuit breaker is on: {}", cause.getMessage());
                return true;
            }
            return false;
        }).build());
    }
    //对于GET请求,启用重试机制
    Supplier<Object> objectSupplier = Retry.decorateSupplier(retry, () -> {
        try {
            return pjp.proceed();
        } catch (Throwable throwable) {
            ReflectionUtils.rethrowRuntimeException(throwable);
            return null;
        }
    });
    return Try.ofSupplier(objectSupplier).get();
}

Spring Cloud Gateway реализует повторную попытку

Spring Cloud Gateway по умолчанию имеет свои собственные повторные попытки, иresilience4jизRetryОн несовместим с механизмом Reactor Spring Cloud Gateway, поэтому вам нужно написать дополнительный связующий код.Для простоты Spring Cloud Gateway по умолчанию имеет собственную повторную попытку. Используйте эту повторную попытку для достижения повторной попыткиFilterПодключен к Spring Cloud Gateway.

Повторная попытка Spring Cloud GatewayFilterпройти черезRetryGatewayFilterFactoryРеализация, мы хотим вступить в силу для каждого вызова микросервиса и сделать егоGlobalFilter.И эта повторная попытка должна быть до того, как балансировщик нагрузки выберет экземпляр, поэтому эта повторная попытка должна быть вRouteToRequestUrlFilterиLoadBalancerClientFilterПеред (эти двое отвечают заlb:Балансировщик нагрузки запроса маршрутизации для получения URL-адреса перезаписи экземпляра).

Мы хотим реализовать разные конфигурации для разных микросервисов, поэтому создаем классы конфигурацииApiGatewayRetryConfig:

@Data
@ConfigurationProperties(prefix = "spring.cloud.gateway")
public class ApiGatewayRetryConfig {
    private Map<String, RetryGatewayFilterFactory.RetryConfig> retry;

    public RetryGatewayFilterFactory.RetryConfig getDefault() {
        return retry.computeIfAbsent("default", key -> new RetryGatewayFilterFactory.RetryConfig());
    }
}

один из нихRetryConfigСодержит следующие свойства, которые мы используем:

Значение по умолчанию элемента конфигурации указывает максимальное количество повторных попыток3, за исключением того, какие коды ответов повторять при вызове seriesSERVER_ERROR, по умолчанию все коды ответов 5XX пусты, для каких кодов состояния повторять попытку, это конкретный HttpStatus и серия Only можно указать один methodGET, для которого HttpMethods нужно повторить. exceptionsList.of(IOException.class, TimeoutException.class) Для исключений, которые нужно повторить, по умолчанию используется исключение IO и исключение времени ожидания. Конфигурация backoffbackOff, которая определяет, как повторить попытку позже — firstBackoff5[мс] Интервал первой попытки — maxBackoff Максимальный интервал повтора — factor2 каждый интервал повтора = firstBackoff * (коэффициент ^ (times - 1)), максимальное значение maxBackoff-basedOnPreviousValuetrue основано на отсрочке последнего запроса, если да, сохраните последнее время отсрочки, следующий раз с этого времени отсрочки начните как интервал первого повтора

Наша конфигурация:

spring:
  cloud:
    gateway:
      # 这是我们自定义的重试配置:ApiGatewayRetryConfig,
      retry:
        default:
          retries: 1
          series: SERVER_ERROR,CLIENT_ERROR
          methods: GET
          # 对于断路器打开也会重试
          exceptions: io.github.resilience4j.circuitbreaker.CallNotPermittedException, org.springframework.cloud.gateway.support.TimeoutException, java.io.IOException
          backoff:
            basedOnPreviousValue: true
            factor: 2
            firstBackoff: 100ms
            maxBackoff: 500ms