«1. Обзор

Spring Cloud обеспечивает балансировку нагрузки на стороне клиента с помощью ленты Netflix. Механизм балансировки нагрузки ленты можно дополнить повторными попытками.

В этом уроке мы собираемся изучить этот механизм повторных попыток.

Во-первых, мы увидим, почему важно, чтобы наши приложения были созданы с учетом этой функции. Затем мы создадим и настроим приложение с помощью Spring Cloud Netflix Ribbon, чтобы продемонстрировать механизм.

2. Мотивация

В облачных приложениях сервис обычно делает запросы к другим сервисам. Но в такой динамичной и нестабильной среде сети могут выйти из строя или службы могут быть временно недоступны.

Мы хотим корректно справляться со сбоями и быстро восстанавливаться. Во многих случаях эти проблемы недолговечны. Если бы мы повторили тот же запрос вскоре после того, как произошел сбой, возможно, он был бы успешным.

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

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

3. Настройка

Чтобы поэкспериментировать с механизмом повторных попыток, нам нужны две службы Spring Boot. Во-первых, мы создадим службу погоды, которая будет отображать сегодняшнюю информацию о погоде через конечную точку REST.

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

3.1. Служба погоды

Давайте создадим очень простую службу погоды, которая иногда будет давать сбой с кодом состояния HTTP 503 (служба недоступна). Мы смоделируем этот периодический сбой, выбрав сбой, когда количество вызовов кратно настраиваемому свойству success.call.divisor:

@Value("${successful.call.divisor}")
private int divisor;
private int nrOfCalls = 0;

@GetMapping("/weather")
public ResponseEntity<String> weather() {
    LOGGER.info("Providing today's weather information");
    if (isServiceUnavailable()) {
        return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
    }
    LOGGER.info("Today's a sunny day");
    return new ResponseEntity<>("Today's a sunny day", HttpStatus.OK);
}

private boolean isServiceUnavailable() {
    return ++nrOfCalls % divisor != 0;
}

у нас есть регистратор сообщений внутри обработчика.

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

3.2. Служба клиента

Наша вторая служба будет использовать ленту Spring Cloud Netflix.

Во-первых, давайте определим конфигурацию клиента Ribbon:

@Configuration
@RibbonClient(name = "weather-service", configuration = RibbonConfiguration.class)
public class WeatherClientRibbonConfiguration {

    @LoadBalanced
    @Bean
    RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

}

Наш HTTP-клиент имеет аннотацию @LoadBalanced, что означает, что мы хотим, чтобы нагрузка балансировалась с помощью Ribbon.

Теперь мы добавим механизм ping для определения доступности службы, а также стратегию балансировки нагрузки с циклическим перебором, определив класс RibbonConfiguration, включенный в аннотацию @RibbonClient выше:

public class RibbonConfiguration {
 
    @Bean
    public IPing ribbonPing() {
        return new PingUrl();
    }
 
    @Bean
    public IRule ribbonRule() {
        return new RoundRobinRule();
    }
}

Далее нам нужно чтобы отключить Eureka из клиента ленты, поскольку мы не используем обнаружение службы. Вместо этого мы используем определенный вручную список экземпляров службы погоды, доступных для балансировки нагрузки.

Итак, давайте также добавим все это в файл application.yml:

weather-service:
    ribbon:
        eureka:
            enabled: false
        listOfServers: http://localhost:8021, http://localhost:8022

Наконец, давайте создадим контроллер и заставим его вызывать серверную службу:

@RestController
public class MyRestController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/client/weather")
    public String weather() {
        String result = this.restTemplate.getForObject("http://weather-service/weather", String.class);
        return "Weather Service Response: " + result;
    }
}

4. Включение механизма повторных попыток ~ ~~ 4.1. Настройка свойств application.yml

Нам нужно поместить свойства службы погоды в файл application.yml нашего клиентского приложения:

В приведенной выше конфигурации используются стандартные свойства ленты, которые нам нужно определить для включения повторных попыток:

weather-service:
  ribbon:
    MaxAutoRetries: 3
    MaxAutoRetriesNextServer: 1
    retryableStatusCodes: 503, 408
    OkToRetryOnAllOperations: true

MaxAutoRetries – количество повторных попыток неудачного запроса на одном и том же сервере (по умолчанию 0). OkToRetryOnAllOperations — когда для этого свойства установлено значение true, повторяются все типы HTTP-запросов, а не только GET (по умолчанию)

    Мы собираемся повторить неудачный запрос, когда клиентская служба получит 503 (служба недоступна) или 408 (тайм-аут запроса) код ответа.

4.2. Требуемые зависимости

Spring Cloud Netflix Ribbon использует Spring Retry для повторения неудачных запросов.

Мы должны убедиться, что зависимость находится в пути к классам. В противном случае неудачные запросы не будут повторяться. Мы можем опустить версию, так как она управляется Spring Boot:

«

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

«4.3. Логика повтора на практике

Наконец, давайте посмотрим на логику повтора на практике.

По этой причине нам нужно два экземпляра нашего погодного сервиса, и мы запустим их на портах 8021 и 8022. Конечно, эти экземпляры должны соответствовать списку listOfServers, определенному в предыдущем разделе.

Кроме того, нам нужно настроить свойство success.call.divisor для каждого экземпляра, чтобы убедиться, что наши смоделированные сервисы дают сбой в разное время:

successful.call.divisor = 5 // instance 1
successful.call.divisor = 2 // instance 2

Далее, давайте также запустим клиентский сервис на порту 8080 и вызовем:

http://localhost:8080/client/weather

Посмотрим на консоль метеослужбы:

weather service instance 1:
    Providing today's weather information
    Providing today's weather information
    Providing today's weather information
    Providing today's weather information

weather service instance 2:
    Providing today's weather information
    Today's a sunny day

Итак, после нескольких попыток (4 на экземпляре 1 и 2 на экземпляре 2) мы получили корректный ответ.

5. Конфигурация политики отсрочки

Когда в сеть поступает больше данных, чем она может обработать, возникает перегрузка. Чтобы облегчить это, мы можем настроить политику отсрочки.

По умолчанию задержки между повторными попытками нет. Ниже Spring Cloud Ribbon используется объект Spring Retry NoBackOffPolicy, который ничего не делает.

Однако мы можем переопределить поведение по умолчанию, расширив класс RibbonLoadBalancedRetryFactory:

@Component
private class CustomRibbonLoadBalancedRetryFactory 
  extends RibbonLoadBalancedRetryFactory {

    public CustomRibbonLoadBalancedRetryFactory(
      SpringClientFactory clientFactory) {
        super(clientFactory);
    }

    @Override
    public BackOffPolicy createBackOffPolicy(String service) {
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000);
        return fixedBackOffPolicy;
    }
}

Класс FixedBackOffPolicy обеспечивает фиксированную задержку между повторными попытками. Если мы не устанавливаем период отсрочки, значение по умолчанию равно 1 секунде.

В качестве альтернативы мы можем установить ExponentialBackOffPolicy или ExponentialRandomBackOffPolicy:

@Override
public BackOffPolicy createBackOffPolicy(String service) {
    ExponentialBackOffPolicy exponentialBackOffPolicy = 
      new ExponentialBackOffPolicy();
    exponentialBackOffPolicy.setInitialInterval(1000);
    exponentialBackOffPolicy.setMultiplier(2); 
    exponentialBackOffPolicy.setMaxInterval(10000);
    return exponentialBackOffPolicy;
}

Здесь начальная задержка между попытками составляет 1 секунду. Затем задержка удваивается для каждой последующей попытки, не превышающей 10 секунд: 1000 мс, 2000 мс, 4000 мс, 8000 мс, 10000 мс, 10000 мс…

Кроме того, ExponentialRandomBackOffPolicy добавляет случайное значение к каждому периоду ожидания без превышает следующее значение. Таким образом, это может дать 1500 мс, 3400 мс, 6200 мс, 9800 мс, 10000 мс, 10000 мс…

Выбор того или иного зависит от того, сколько у нас трафика и сколько различных клиентских сервисов. Эти стратегии, от фиксированных до случайных, помогают нам добиться лучшего распределения всплесков трафика, что также означает меньшее количество повторных попыток. Например, для многих клиентов фактор случайности помогает избежать одновременного обращения нескольких клиентов к службе при повторной попытке.

6. Заключение

В этой статье мы узнали, как повторить неудачные запросы в наших приложениях Spring Cloud, используя ленту Spring Cloud Netflix. Мы также обсудили преимущества, которые дает этот механизм.

Далее мы продемонстрировали, как работает логика повторных попыток в приложении REST, поддерживаемом двумя службами Spring Boot. Spring Cloud Netflix Ribbon делает это возможным благодаря использованию библиотеки Spring Retry.

Наконец, мы увидели, как настроить различные типы задержек между повторными попытками.

Как всегда, исходный код этого руководства доступен на GitHub.