Шаблон повтора

Приложение, которое взаимодействует с элементами, работающими в облаке, должно быть чувствительным к временным сбоям, которые могут возникнуть в этой среде. Сбои включают в себя мгновенную потерю сетевого подключения к компонентам и службам, временную недоступность службы или тайм-ауты, возникающие, когда служба занята.

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

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

  • Отмена. Если ошибка указывает на то, что сбой не является временным или его повторение маловероятно, приложение должно отменить операцию и сообщить об исключении.
  • Повторить попытку. Если сообщенная ошибка является необычной или редкой, она могла быть вызвана необычными обстоятельствами, такими как повреждение сетевого пакета во время передачи.
  • Повторите попытку после задержки. Если неисправность вызвана одним из наиболее распространенных сбоев подключения или занятости, сети или службе может потребоваться короткий период времени, чтобы устранить проблемы с подключением или очистить невыполненные задания. Приложение должно подождать подходящее время, прежде чем повторить запрос.

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

Типичные стратегии используют следующие типы интервалов повтора:

  • Экспоненциальная задержка. Приложение ждет короткое время перед первой попыткой, а затем экспоненциально увеличивается время между каждой последующей попыткой. Например, он может повторить операцию через 50 мс, 100 мс, 200 мс и т. Д.
  • Дополнительные интервалы. Приложение ожидает короткое время перед первой попыткой, а затем постепенно увеличивает время между каждой последующей попыткой. Например, он может повторить операцию через 50 мс, 100 мс, 200 мс и т. Д.

  • Регулярные интервалы. Приложение ожидает один и тот же период времени между каждой попыткой. Например, он может повторять операцию каждые 3 секунды.
  • Немедленная повторная попытка. Иногда временный сбой бывает очень коротким, возможно, вызванным событием, например конфликтом сетевых пакетов или скачком в аппаратном компоненте. В этом случае уместна немедленная повторная попытка операции, потому что она может быть успешной, если сбой будет устранен за время, необходимое приложению для сборки и отправки следующего запроса. Однако никогда не должно быть более одной немедленной повторной попытки, и вам следует переключиться на альтернативные стратегии, такие как экспоненциальный откат или резервные действия, если немедленная повторная попытка не удалась.

  • Рандомизация. Любая из описанных выше стратегий повтора может включать рандомизацию, чтобы предотвратить одновременную отправку несколькими экземплярами клиента последующих попыток повтора. Например, один экземпляр может повторить операцию через 3 секунды, 11 секунд, 28 секунд и так далее, в то время как другой экземпляр может повторить операцию через 4 секунды, 12 секунд, 26 секунд и так далее. Рандомизация - это полезный метод, который вы можете комбинировать с другими стратегиями.

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

Код для шаблона повтора в Java

package util;

import java.util.Arrays;
import java.util.function.Supplier;

public final class Util {

    /**
     * Retry to run a function a few times, retry if specific exceptions occur.
     *
     * @param timeoutExceptionClasses what exceptions should lead to retry. Default: any exception
     */
    public static <T> T retry(Supplier<T> function, int maxRetries, Class<? extends Exception>... timeoutExceptionClasses) {
        timeoutExceptionClasses = timeoutExceptionClasses.length == 0 ? new Class[]{Exception.class} : timeoutExceptionClasses;
        int retryCounter = 0;
        Exception lastException = null;
        while (retryCounter < maxRetries) {
            try {
                return function.get();
            } catch (Exception e) {
                lastException = e;
       if(Arrays.stream(timeoutExceptionClasses).noneMatch(tClass ->
                        tClass.isAssignableFrom(e.getClass())
                ))
                    throw lastException instanceof RuntimeException    ?((RuntimeException) lastException) :
                            new RuntimeException(lastException);
                else {
                    retryCounter++;
                    System.err.println("FAILED - Command failed on retry " + retryCounter + " of " + maxRetries);
                    e.printStackTrace();
                    if (retryCounter >= maxRetries) {
                        break;
                    }
                }
            }
        }
        throw lastException instanceof RuntimeException ?
                ((RuntimeException) lastException) :
                new RuntimeException(lastException);
    }

    /** Manual test method */
    public static void main(String... args) throws Exception {
        retry(() -> {
            System.out.println(5 / 0);
            return null;
        }, 5, Exception.class);
    }
}

Схема автоматического выключателя

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

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

Кроме того, если служба очень загружена, сбой в одной части системы может привести к каскадным сбоям. Например, операция, которая вызывает службу, может быть настроена для реализации тайм-аута и ответа с сообщением об ошибке, если служба не отвечает в течение этого периода.

Шаблон прерыватель цепи может предотвратить неоднократные попытки приложения выполнить операцию, которая может потерпеть неудачу. Позволяя ему продолжить работу, не дожидаясь устранения неисправности или не тратя циклы ЦП, пока он определяет, что неисправность является длительной. Шаблон автоматического выключателя также позволяет приложению определять, устранена ли неисправность. Если кажется, что проблема устранена, приложение может попытаться вызвать операцию.

Назначение шаблона прерывателя цепи отличается от шаблона повторной попытки. Шаблон Retry позволяет приложению повторить операцию в ожидании ее успеха. Шаблон «Автоматический выключатель» предотвращает выполнение приложением операции, которая может завершиться ошибкой. Приложение может комбинировать эти два шаблона, используя шаблон повтора для запуска операции через автоматический выключатель. Однако логика повтора должна быть чувствительна к любым исключениям, возвращаемым автоматическим выключателем, и отказываться от попыток повтора, если автоматический выключатель указывает, что отказ не является временным.

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

Прокси-сервер может быть реализован как конечный автомат со следующими состояниями, имитирующими функциональность автоматического выключателя:

  • Закрыто. Запрос от приложения направляется в операцию. Прокси-сервер ведет подсчет количества недавних сбоев, и если вызов операции оказался неудачным, прокси-сервер увеличивает этот счетчик. Если количество недавних сбоев превышает указанный порог в течение заданного периода времени, прокси переводится в открытое состояние. В этот момент прокси запускает таймер тайм-аута, и когда этот таймер истекает, прокси переводится в полуоткрытое состояние. Таймер тайм-аута предназначен для того, чтобы дать системе время для устранения проблемы, вызвавшей сбой, прежде чем приложение сможет снова попытаться выполнить операцию.
  • Открыть. Запрос от приложения немедленно завершается ошибкой, и в приложение возвращается исключение.
  • Half-Open. Ограниченное количество запросов от приложения может пройти и вызвать операцию. Если эти запросы выполнены успешно, предполагается, что неисправность, которая ранее вызвала отказ, была устранена, и автоматический выключатель переходит в замкнутое состояние (счетчик отказов сбрасывается). Если какой-либо запрос терпит неудачу, автоматический выключатель предполагает, что неисправность все еще присутствует, поэтому он возвращается обратно в состояние «Разомкнуто» и перезапускает таймер тайм-аута, чтобы дать системе дополнительный период времени для восстановления после отказа.

Код схемы автоматического выключателя на Java

Вы можете реализовать схему автоматического выключателя с Netflix Hystrix. Следующий код может лучше объяснить решение.

Ниже приведено приложение для получения сведений о школе на основе ее названия.

Pom.xml выглядит следующим образом

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-rest</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
   </dependency>
</dependencies>

<dependencyManagement>
   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-dependencies</artifactId>
         <version>${spring-cloud.version}</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
   </dependencies>
</dependencyManagement>

Основной класс выглядит следующим образом

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard
@EnableCircuitBreaker
public class SpringHystrixSchoolServiceApplication {

   public static void main(String[] args) {
      SpringApplication.run(SpringHystrixSchoolServiceApplication.class, args);
   }
}

Мы помещаем @EnableCircuitBreaker, чтобы указать, что нам нужно включить автоматический выключатель для этого приложения.

Ниже приводится RestController

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.example.springhystrixschoolservice.delegate.StudentService;

@RestController
public class SchoolServiceController {
   
   @Autowired
   StudentService studentService;

   @RequestMapping(value = "/getSchoolDetails/{schoolname}", method = RequestMethod.GET)
   public String getStudents(@PathVariable String schoolname) {
      System.out.println("Going to call student service to get data!");
      return studentService.callStudentServiceAndGetData(schoolname);
   }
   
}

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

import java.util.Date;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

@Service
public class StudentService {
   @Autowired
   RestTemplate restTemplate;
   
   @HystrixCommand(fallbackMethod = "callStudentServiceAndGetData_Fallback")
   public String callStudentServiceAndGetData(String schoolname) {
      System.out.println("Getting School details for " + schoolname);
      String response = restTemplate
            .exchange("http://localhost:8098/getStudentDetailsForSchool/{schoolname}"
            , HttpMethod.GET
            , null
            , new ParameterizedTypeReference<String>() {
         }, schoolname).getBody();

      System.out.println("Response Received as " + response + " -  " + new Date());

      return "NORMAL FLOW !!! - School Name -  " + schoolname + " :::  Student Details " + response + " -  " + new Date();
   }
   
   @SuppressWarnings("unused")
   private String callStudentServiceAndGetData_Fallback(String schoolname) {
      System.out.println("Student Service is down!!! fallback route enabled...");
      return "CIRCUIT BREAKER ENABLED!!!No Response From Student Service at this moment. Service will be back shortly - " + new Date();
   }

   @Bean
   public RestTemplate restTemplate() {
      return new RestTemplate();
   }
}

Если вы присмотритесь, мы используем HystrixCommand и резервный метод для метода callStudentServiceAndGetData. Библиотека Hystrix Netflix обеспечивает реализацию схемы автоматического выключателя. Когда вы применяете автоматический выключатель к методу, Hystrix отслеживает неудачные вызовы этого метода, и, если отказы достигают порогового значения, Hystrix размыкает цепь, чтобы последующие вызовы автоматически завершались ошибкой. Пока канал открыт, Hystrix перенаправляет вызов метода, и они передаются указанному вами резервному методу.

Spring Cloud Netflix Hystrix ищет любой метод, помеченный аннотацией @HystrixCommand, и помещает этот метод в прокси-сервер, подключенный к автоматическому выключателю, чтобы Hystrix мог его контролировать.

Спасибо за чтение! Если вам понравилось, пожалуйста, похлопайте 👏 в ладоши.