本微服务教程将继续展示如何设置断路器以解决微服务依赖项中的潜在问题。抽丝剥茧 细说架构那些事——【优锐课】
上篇文章说到,微服务系列详解—Part 3:Spring Cloud Service注册和发现
在微服务世界中,为了满足客户请求,一个微服务可能需要与其他微服务对话。我们应该最小化这种对其他微服务的直接依赖,但是在某些情况下,这是不可避免的。如果微服务已关闭或无法正常运行,则问题可能会扩展到上游服务。Netflix创建了Hystrix库,实现了Circuit Breaker模式来解决此类问题。我们可以使用Spring Cloud Netflix Hystrix断路器来保护微服务免于级联故障。
在这篇文章中,我们将学习:
• 使用@HystrixCommand实现断路器模式
• 如何传播ThreadLocal变量
• 使用Hystrix仪表板监控断路器
从目录服务中,我们正在库存服务上调用REST端点以获取产品的库存水平。如果库存服务中断了怎么办?如果库存服务的响应时间太长,从而降低了所有依赖它的服务的速度,该怎么办?我们希望有一些超时时间并实现后备机制。
将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
你可以在此处找到所有可用的配置选项。
默认情况下,具有@HystrixCommand的方法将在不同的线程上执行,因为默认的execution.isolation.strategy为ExecutionIsolationStrategy.THREAD。因此,在调用@HystrixCommand方法之前设置的ThreadLocal变量在@HystrixCommand方法中不可用。
使ThreadLocal变量可用的一种选择是使用execution.isolation.strategy = SEMAPHORE。
@HystrixCommand(fallbackMethod = "getDefaultProductInventoryByCode",
commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
}
)
public Optional<ProductInventoryResponse> getProductInventoryByCode(String productCode)
{
...
}
如果将属性execute.isolation.strategy设置为SEMAPHORE,则Hystrix将使用信号量而不是线程来限制调用该命令的并发父线程的数量。
使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组件。
将Hystrix启动器添加到目录服务后,假设目录服务在8181端口上运行,则可以使用Actuator端点http://localhost:8181/actuator/hystrix.stream作为事件流获取电路状态。
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在单个仪表板中提供所有服务的统一视图。
感谢阅读!
下篇文章:微服务系列详解-Part5:Spring Cloud Zuul代理作为API网关
另外还有一些资源分享给各位,需要的可以免费领取~