在微服务世界中,为了满足客户请求,一个微服务可能需要与其他微服务对话。 我们应该最小化这种对其他微服务的直接依赖,但是在某些情况下这是不可避免的。 如果微服务已关闭或无法正常运行,则问题可能会扩展到上游服务。 Netflix创建了实施电路断路器模式的 Hystrix库,以解决此类问题。 我们可以使用Spring Cloud Netflix Hystrix断路器来保护微服务免于级联故障。
使用Spring Boot和Spring Cloud的微服务
- 第1部分:MicroServices:Spring Boot和Spring Cloud概述
- 第2部分:MicroServices:使用Spring Cloud Config和Vault进行配置管理
- 第3部分:MicroServices:Spring Cloud Service注册和发现
- 第4部分:微服务:使用Netflix Hystrix的Spring Cloud Breaker
在这篇文章中,我们将学习:
- 使用@HystrixCommand实现断路器模式
- 如何传播ThreadLocal变量
- 使用Hystrix仪表板监控断路器
实施Netflix Hystrix断路器模式
从目录服务中,我们正在库存服务上调用REST端点以获取产品的库存水平。 如果库存服务中断了怎么办? 如果库存服务花费的时间太长而导致响应速度变慢,该怎么办呢? 我们希望有一个timeout并实现一些后备机制。
将Hystrix起动器添加到目录服务。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
要启用断路器, 请在目录服务入口点类上添加@EnableCircuitBreaker批注。
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批注。
让我们创建InventoryServiceClient.java ,它将调用清单服务REST端点并通过回退实现应用@HystrixCommand 。
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);
}
}
import lombok.Data;
@Data
public class ProductInventoryResponse {
private String productCode;
private int availableQuantity;
}
我们已经在使用@HystrixCommand( fallbackMethod =“ getDefaultProductInventoryByCode”)进行REST调用的位置注释了该方法,以便如果在特定时间限制内未收到响应,则该调用将超时并调用配置的fallback方法。 后备方法应在同一类中定义,并且应具有相同的签名。 在后备方法getDefaultProductInventoryByCode()中,我们将availableQuantity设置为50 , 显然,此行为取决于业务需求 。
我们可以通过使用@HystrixProperty批注配置属性来自定义@HystrixCommand默认行为。
@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文件中配置它们,而不是在代码中配置这些参数值,如下所示。
hystrix.command.getProductInventoryByCode.execution.isolation.thread.timeoutInMilliseconds=2000
hystrix.command.getProductInventoryByCode.circuitBreaker.errorThresholdPercentage=60
请注意,我们使用方法名称作为commandKey ,这是默认行为。 我们可以自定义commandKey名称,如下所示:
@HystrixCommand(commandKey = "inventory-by-productcode", fallbackMethod = "getDefaultProductInventoryByCode")
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
...
}
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的方法将在不同的线程上执行,因为默认的execution.isolation.strategy是ExecutionIsolationStrategy.THREAD 。 所以,ThreadLocal的变量,我们调用@HystrixCommand方法不会@HystrixCommand方法中可用之前设置。
使ThreadLocal变量可用的一种选择是使用execution.isolation.strategy = SEMAPHORE 。
@HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode",
commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
}
)
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
...
}
如果设置该属性execution.isolation.strategy 信号量 ,然后猬将使用信号量,而不是线程来限制并发线程父的那调用命令的数量。 您可以在https://github.com/Netflix/Hystrix/wiki/How-it-Works#isolation上了解有关隔离如何工作的更多信息。
使ThreadLocal变量在Hystrix命令方法中可用的另一种方法是实现我们自己的HystrixConcurrencyStrategy 。
假设您要传播一些CorrelationId设置为ThreadLocal变量。
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 。
@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);
}
}
}
}
现在,您可以在调用Hystrix命令之前设置CorrelationId ,并在Hystrix命令中访问CorrelationId。
ProductService.java
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
@HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode")
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
...
log.info("CorrelationID: "+ MyThreadLocalsHolder.getCorrelationId());
}
这只是如何将数据传播到Hystrix命令的一个示例。 同样,我们可以传递当前HTTP请求中可用的任何数据,例如,使用诸如RequestContextHolder等Spring组件。
Jakub Narloch写了一篇很好的文章,介绍如何传播请求上下文,甚至还创建了一个Spring Boot启动程序。 请查看他的博客https://jmnarloch.wordpress.com/2016/07/06/spring-boot-hystrix-and-threadlocals/和GitHub Repo https://github.com/jmnarloch/hystrix-context-spring- boot-starter 。
使用Hystrix仪表板监控断路器
将Hystrix启动器添加到目录服务后,我们可以使用Actuator端点http:// localhost:8181 / actuator / hystrix.stream来获取电路状态作为事件流,假设目录服务在8181端口上运行。
Spring Cloud还提供了一个不错的仪表板来监视Hystrix命令的状态。
使用Hystrix Dashboard starter创建一个Spring Boot应用程序,并使用@EnableHystrixDashboard注释主入口点类。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
假设我们在8788端口上运行Hystrix仪表板,然后转到http:// localhost:8788 / hystrix查看仪表板。
现在,在Hystrix仪表板主页中,输入http:// localhost:8181 / actuator / hystrix.stream作为流URL,并给Catalog Service作为标题,然后单击Monitor Stream按钮。
现在调用目录服务REST端点,该端点在内部调用清单服务REST端点,您可以看到Circuit状态以及成功调用的次数和发生失败的次数等。
不必为每个服务都有单独的仪表板,我们可以使用Turbine在单个仪表板中提供所有服务的统一视图。 有关更多详细信息,请参见http://cloud.spring.io/spring-cloud-static/Finchley.M7/single/spring-cloud.html#_turbine 。
您可以在https://github.com/sivaprasadreddy/spring-boot-microservices-series上找到本文的源代码。