Статьи

MicroServices. Часть 4. Автоматический выключатель Spring Cloud с использованием Netflix Hystrix

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

Микроуслуги с использованием Spring Boot и Spring Cloud

В этом посте мы собираемся узнать:

  • Реализация шаблона автоматического выключателя с использованием @HystrixCommand
  • Как распространять переменные ThreadLocal
  • Мониторинг автоматических выключателей с помощью панели инструментов Hystrix

Внедрение схемы автоматического выключателя Netflix Hystrix

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

Добавить стартер Hystrix в каталог-сервис.

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

Чтобы включить прерыватель цепи, добавьте аннотацию @EnableCircuitBreaker в класс точки входа службы каталогов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
 
@EnableCircuitBreaker
@SpringBootApplication
public class CatalogServiceApplication {
 
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
 
    public static void main(String[] args) {
        SpringApplication.run(CatalogServiceApplication.class, args);
    }
}

Теперь мы можем использовать аннотацию @HystrixCommand для любого метода, для которого мы хотим применить метод timeout и fallback.

Давайте создадим InventoryServiceClient.java, который будет вызывать конечную точку REST службы инвентаризации и применять @HystrixCommand с резервной реализацией.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.sivalabs.catalogservice.web.models.ProductInventoryResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
 
import java.util.Optional;
 
@Service
@Slf4j
public class InventoryServiceClient {
    private final RestTemplate restTemplate;
 
    @Autowired
    public InventoryServiceClient(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }
 
    @HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode")
    public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
    {
        ResponseEntity<ProductInventoryResponse> itemResponseEntity =
                restTemplate.getForEntity("http://inventory-service/api/inventory/{code}",
                        ProductInventoryResponse.class,
                        productCode);
        if (itemResponseEntity.getStatusCode() == HttpStatus.OK) {
            return Optional.ofNullable(itemResponseEntity.getBody());
        } else {
            log.error("Unable to get inventory level for product_code: " + productCode + ", StatusCode: " + itemResponseEntity.getStatusCode());
            return Optional.empty();
        }
    }
 
    @SuppressWarnings("unused")
    Optional<ProductInventoryResponse> getDefaultProductInventoryByCode(String productCode) {
        log.info("Returning default ProductInventoryByCode for productCode: "+productCode);
        ProductInventoryResponse response = new ProductInventoryResponse();
        response.setProductCode(productCode);
        response.setAvailableQuantity(50);
        return Optional.ofNullable(response);
    }
}
1
2
3
4
5
6
7
import lombok.Data;
 
@Data
public class ProductInventoryResponse {
    private String productCode;
    private int availableQuantity;
}

Мы аннотировали метод, из которого мы делаем вызов REST, с помощью @HystrixCommand ( fallbackMethod = «getDefaultProductInventoryByCode»), чтобы, если он не получит ответ в течение определенного периода времени, вызов был заблокирован по тайм-ауту и ​​вызвал настроенный резервный метод. Резервный метод должен быть определен в том же классе и иметь одинаковую подпись. В альтернативном методе getDefaultProductInventoryByCode () мы устанавливаем значение availableQuantity равным 50 , очевидно, что это поведение зависит от того, чего хочет бизнес .

Мы можем настроить поведение @HystrixCommand по умолчанию, настроив свойства с помощью аннотаций @HystrixProperty .

01
02
03
04
05
06
07
08
09
10
@HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode",
    commandProperties = {
       @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
       @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value="60")
    }
)
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
    ....
}

Вместо настройки этих значений параметров в коде мы можем настроить их в файлах bootstrap.properties/yml следующим образом.

1
2
hystrix.command.getProductInventoryByCode.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.getProductInventoryByCode.circuitBreaker.errorThresholdPercentage=60

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

1
2
3
4
5
@HystrixCommand(commandKey = "inventory-by-productcode", fallbackMethod = "getDefaultProductInventoryByCode")
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
    ...
}
1
2
hystrix.command.inventory-by-productcode.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.inventory-by-productcode.circuitBreaker.errorThresholdPercentage=60

Вы можете найти все доступные варианты конфигурации здесь https://github.com/Netflix/Hystrix/wiki/Configuration .

Как распространять переменные ThreadLocal

По умолчанию методы с @HystrixCommand будут выполняться в другом потоке, потому что по умолчанию execute.isolation.strategyExecutionIsolationStrategy.THREAD . Таким образом, переменные ThreadLocal, которые мы устанавливаем перед вызовом методов @HystrixCommand , не будут доступны в методах @HystrixCommand .

Один из вариантов сделать переменные ThreadLocal доступными — это использовать execute.isolation.strategy = SEMAPHORE .

1
2
3
4
5
6
7
8
9
@HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode",
    commandProperties = {
        @HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
    }
)
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
    ...
}

Если вы установите для свойства execute.isolation.strategy значение SEMAPHORE, Hystrix будет использовать семафоры вместо потоков, чтобы ограничить число одновременных родительских потоков, которые вызывают команду. Вы можете прочитать больше о том, как работает изоляция, здесь https://github.com/Netflix/Hystrix/wiki/How-it-Works#isolation .

Другой вариант сделать переменные ThreadLocal доступными в командных методах Hystrix — это реализовать нашу собственную HystrixConcurrencyStrategy .

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

01
02
03
04
05
06
07
08
09
10
11
public class MyThreadLocalsHolder {
    private static final ThreadLocal<String> CORRELATION_ID = new ThreadLocal();
 
    public static void setCorrelationId(String correlationId) {
        CORRELATION_ID.set(correlationId);
    }
 
    public static String getCorrelationId() {
        return CORRELATION_ID.get();
    }
}

Давайте реализуем нашу собственную HystrixConcurrencyStrategy .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
@Slf4j
public class ContextCopyHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
 
    public ContextCopyHystrixConcurrencyStrategy() {
        HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
    }
 
    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        return new MyCallable(callable, MyThreadLocalsHolder.getCorrelationId());
    }
 
    public static class MyCallable<T> implements Callable<T> {
 
        private final Callable<T> actual;
        private final String correlationId;
 
        public MyCallable(Callable<T> callable, String correlationId) {
            this.actual = callable;
            this.correlationId = correlationId;
        }
 
        @Override
        public T call() throws Exception {
            MyThreadLocalsHolder.setCorrelationId(correlationId);
            try {
                return actual.call();
            } finally {
                MyThreadLocalsHolder.setCorrelationId(null);
            }
        }
    }
}

Теперь вы можете установить CorrelationId перед вызовом команды Hystrix и получить доступ к CorrelationId внутри команды Hystrix.

ProductService.java

01
02
03
04
05
06
07
08
09
10
11
12
public Optional<Product> findProductByCode(String code)
{
    ....
    String correlationId = UUID.randomUUID().toString();
    MyThreadLocalsHolder.setCorrelationId(correlationId);
    log.info("Before CorrelationID: "+ MyThreadLocalsHolder.getCorrelationId());
    Optional<ProductInventoryResponse> responseEntity = inventoryServiceClient.getProductInventoryByCode(code);
    ...
    log.info("After CorrelationID: "+ MyThreadLocalsHolder.getCorrelationId());
    ....
     
}

InventoryServiceClient.java

1
2
3
4
5
6
@HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode")
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
    ...
    log.info("CorrelationID: "+ MyThreadLocalsHolder.getCorrelationId());
}

Это только один пример того, как распространять данные в команду Hystrix. Точно так же мы можем передавать любые данные, доступные в текущем HTTP-запросе, скажем, используя компоненты Spring, такие как RequestContextHolder и т. Д.

Якуб Нарлок написал замечательную статью о том, как распространять контекст запроса, и даже создал стартовую версию Spring Boot. Пожалуйста, посмотрите на его блог https://jmnarloch.wordpress.com/2016/07/06/spring-boot-hystrix-and-threadlocals/ и репозиторий GitHub https://github.com/jmnarloch/hystrix-context-spring- загрузчик

Мониторинг автоматических выключателей с помощью панели инструментов Hystrix

Добавив стартер Hystrix в службу каталогов, мы можем получить состояние каналов в виде потока событий, используя конечную точку привода http: // localhost: 8181 / activator / hystrix.stream , при условии, что служба каталогов работает на порту 8181 .

Spring Cloud также предоставляет удобную панель мониторинга состояния команд Hystrix.
Создайте приложение Spring Boot с помощью Hystrix Dashboard starter и аннотируйте основной класс точки входа с помощью @EnableHystrixDashboard .

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

Допустим, мы запускаем Hystrix Dashboard на порту 8788, а затем перейдите по адресу http: // localhost: 8788 / hystrix, чтобы просмотреть панель мониторинга.

Теперь на домашней странице Hystrix Dashboard введите http: // localhost: 8181 / activator / hystrix.stream в качестве URL-адреса потока, задайте службу каталогов в качестве заголовка и нажмите кнопку «Поток монитора».

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

Вместо того, чтобы иметь отдельную панель мониторинга для каждого сервиса, мы можем использовать Turbine, чтобы обеспечить единое представление всех сервисов в одной панели управления. Для получения дополнительной информации см. Http://cloud.spring.io/spring-cloud-static/Finchley.M7/single/spring-cloud.html#_turbine .

Вы можете найти исходный код этой статьи по адресу https://github.com/sivaprasadreddy/spring-boot-microservices-series

Опубликовано на Java Code Geeks с разрешения Сивы Редди, партнера нашей программы JCG . См. Оригинальную статью здесь: MicroServices — Часть 4. Автоматический выключатель Spring Cloud с использованием Netflix Hystrix

Мнения, высказанные участниками Java Code Geeks, являются их собственными.