请先阅读之前的内容:
- Spring Cloud 学习笔记 - No.1 服务注册发现
- Spring Cloud 学习笔记 - No.2 服务消费 Ribbon & Feign
- Spring Cloud 学习笔记 - No.3 分布式配置 Config
在微服务架构中,各单元应用间通过服务注册与订阅的方式互相依赖。
由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,最终导致自身服务的瘫痪,进一步甚至出现故障的蔓延最终导致整个系统的瘫痪。
Spring Cloud Netflix Hystrix 介绍
https://spring.io/guides/gs/circuit-breaker/
针对上述问题,在 Spring Cloud Hystrix 中实现了线程隔离、断路器等一系列的服务保护功能,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。
一个需要断路器的场景
场景描述:在之前的例子中,我们的 eureka-consumer
调用 eureka-client
来提供加法服务,eureka-client
启动了两个实例,端口分别是 2001 和 2002。
对于 eureka-client
提供的加法服务,可能会出现如下的情况:
-
两个
eureka-client
实例都挂了,即出现了故障,例如我们手动关闭这两个实例,然后调用eureka-consumer
的consumer
接口 http://127.0.0.1:3001/consumer,会出现如下的结果:
eureka-client
服务没有挂,但是延时较大,例如我们可以手动增加 5 秒的延时:
修改eureka-client
的CalculatorController.java
类:
@GetMapping("/add")
public Integer add(@RequestParam Integer operand1, @RequestParam Integer operand2) throws InterruptedException {
Thread.sleep(5000L);
return operand1 + operand2;
}
调用 eureka-consumer
的 consumer
接口 http://127.0.0.1:3001/consumer,会出现如下的结果:
从后台日志中可以看出调用超时:
因为 Feign 默认的 read timeout 就是5秒,参见 https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html
这样会有一个问题:每一个请求都至少需要执行5秒,这样会出现请求堆积的情况。
我们希望:如果在某一个时间窗口内,超过一定数量的请求发生了超时,我们启动某个机制,使得后来的请求直接返回,不需要再次等待5秒。这就是断路器。
Hystrix 服务降级
我们利用之前创建的服务消费者 eureka-consumer
。
首先在 pom.xml
中增加如下的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
注意,如果是 Finchley 版本的 Spring Cloud,需要再添加如下依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
否则,启动时会报如下的错误:
ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.0.3.RELEASE:run (default-cli) on project eureka-consumer: An exception occurred while running. null: InvocationTargetException: Error creating bean with name 'hystrixCommandAspect' defined in class path resource [org/springframework/cloud/netflix/hystrix/HystrixCircuitBreakerConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect]: Factory method 'hystrixCommandAspect' threw exception; nested exception is java.lang.NoClassDefFoundError: org/aspectj/lang/JoinPoint: org.aspectj.lang.JoinPoint -> [Help 1]
随后在应用主类中使用 @EnableCircuitBreaker
或 @EnableHystrix
注解开启 Hystrix 的使用:
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
public class EurekaConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(EurekaConsumerApplication.class, args);
}
}
随后改造服务消费方式,新增 ConsumerService
类,然后将在 ConsumerController
中的逻辑迁移过去。最后,在为具体执行逻辑的函数上增加 @HystrixCommand
注解来指定服务降级方法:
@Service
public class ConsumerService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000"),
@HystrixProperty(name = "execution.isolation.strategy",value = "THREAD")},
fallbackMethod = "fallback")
public String consumer() {
// 调用加法服务
String url = "http://eureka-client/add";
UriComponentsBuilder builder = UriComponentsBuilder
.fromUriString(url)
// Add query parameter
.queryParam("operand1", 1)
.queryParam("operand2", 2);
return restTemplate.getForObject(builder.toUriString(), Integer.class)+"";
}
public String fallback() {
return "fallback";
}
}
@RestController
public class ConsumerController {
private final static Logger logger = LoggerFactory.getLogger(ConsumerController.class);
@Autowired
private ConsumerService consumerService;
@GetMapping("/consumer")
public String consumer() {
return consumerService.consumer();
}
}
此时,再次调用 eureka-consumer
的 consumer
接口 http://127.0.0.1:3001/consumer,会出现如下的结果:
Hystrix 依赖隔离
Hystrix 会为每一个 Hystrix 命令创建一个独立的线程池,这样就算某个在 Hystrix 命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。
在上面的示例中,我们使用了@HystrixCommand
来将某个函数包装成了 Hystrix 命令,这里除了定义服务降级之外,Hystrix 框架就会自动的为这个函数实现调用的隔离。所以,依赖隔离、服务降级在使用时候都是一体化实现的。
Hystrix 断路器
在上面的示例中,当我们把服务提供者 eureka-client
中加入了模拟的时间延迟之后,在服务消费端的服务降级逻辑因为 Hystrix 命令调用依赖服务超时,触发了降级逻辑,但是即使这样,受限于 Hystrix 超时时间的问题,我们的调用依然很有可能产生堆积。
断路器的三个重要参数:
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
- 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格根据熔断。默认为20,意味着在10秒内,如果该 Hystrix 命令的调用此时不足20次,即时所有的请求都超时或其他原因失败,断路器都不会打开。
- 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,如果在这30次调用中,有16次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开。
断路器未打开之前,对于之前那个示例的情况就是每个请求都会在当 Hystrix 超时之后返回 fallback,每个请求时间延迟就是近似 Hystrix 的超时时间,如果设置为5秒,那么每个请求就都要延迟5秒才会返回。
断路器打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回 fallback。
通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,那么原来的主逻辑要如何恢复呢?对于这一问题,Hystrix 也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
Hystrix 监控面板
在上面的示例中,断路器是根据一段时间窗内的请求情况来判断并操作断路器的打开和关闭状态的。
而这些请求情况的指标信息都是 HystrixCommand
和 HystrixObservableCommand
实例在执行过程中记录的重要度量信息,它们除了 Hystrix 断路器实现中使用之外,对于系统运维也有非常大的帮助。
这些指标信息会以“滚动时间窗”与“桶”结合的方式进行汇总,并在内存中驻留一段时间,以供内部或外部进行查询使用,Hystrix Dashboard 就是这些指标内容的消费者之一。
hystrix-dashboard 监控面板
可以通过如下的 Spring Assistant
插件来创建项目 hystrix-dashboard
,添加 Hystrix Dashboard
等作为依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
为应用主类加上 @EnableHystrixDashboard
,启用 Hystrix Dashboard 功能:
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
application.properties
文件配置如下:
spring.application.name=hystrix-dashboard
server.port=5001
最后通过命令 mvn spring-boot:run
启动该项目,访问 http://127.0.0.1:5001/hystrix 可以看到:
Hystrix Dashboard 共支持三种不同的监控方式,依次为:
- 默认的集群监控:通过 http://turbine-hostname:port/turbine.stream 开启,实现对默认集群的监控。
- 指定的集群监控:通过 http://turbine-hostname:port/turbine.stream?cluster=[clusterName] 开启,实现对clusterName集群的监控。
- 单体应用的监控:通过 http://hystrix-app:port/hystrix.stream 开启,实现对具体某个服务实例的监控。
我们这里先看第三种:单体应用的监控,例如我们想监控上面的例子 eureka-consumer
,它对应的监控数据接口为 http://127.0.0.1:3001/actuator/hystrix.stream。
注意:
- 对于 Finchley 版本的 Spring Cloud,接口是
/actuator/hystrix.stream
,其他版本的接口是/hystrix.stream
- 对于 Finchley 版本的 Spring Cloud,需要在
application.properties
中添加如下配置:
management.endpoints.web.exposure.include=*
随后通过如下的方式创建一个针对 eureka-consumer
的监控面板
我们可以在监控信息的左上部分找到两个重要的图形信息:一个实心圆和一条曲线。
- 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色、黄色、橙色、红色递减。该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,我们就可以在大量的实例中快速的发现故障实例和高压力实例。
- 曲线:用来记录2分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。
此时的系统架构如下:图片引自 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/
Hystrix 监控数据聚合
上面的例子中,只能实现对服务 单个实例 的数据展现,在生产环境我们的服务是肯定需要做高可用的,那么对于 多个实例 的情况,我们就需要将这些度量指标数据进行聚合。
这里需要另外一个工具:Turbine。
此时的系统架构如下:图片引自 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/
可以通过如下的 Spring Assistant
插件来创建项目 turbine
,添加 Hystrix Dashboard
等作为依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
为应用主类加上 @EnableTurbine
注解开启 Turbine:
@SpringBootApplication
@EnableTurbine
@EnableDiscoveryClient
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
在 application.properties
加入eureka和turbine的相关配置:
spring.application.name=turbine
server.port=6001
management.port=6002
eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/
turbine.app-config=eureka-consumer
turbine.cluster-name-expression="default"
turbine.combine-host-port=true
management.endpoints.web.exposure.include=urbine.stream
-
turbine.app-config
参数指定了需要收集监控信息的服务名; -
turbine.cluster-name-expression
参数指定了集群名称为default
,当我们服务数量非常多的时候,可以启动多个 Turbine 服务来构建不同的聚合集群,而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在 Hystrix 仪表盘中用来定位不同的聚合集群,只需要在 Hystrix Stream 的 URL 中通过cluster
参数来指定; -
turbine.combine-host-port
参数设置为true
,可以让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以host
来区分不同的服务,这会使得在本地调试的时候,本机上的不同服务聚合成一个服务来统计。
最后通过命令 mvn spring-boot:run
启动该项目,访问 http://127.0.0.1:6001/turbine.stream 可以看到 ping 的结果:
我们可以将 http://127.0.0.1:6001/turbine.stream 配置到 Hystrix Dashboard 中:
我们也可以通过消息代理收集聚合,此时的架构如下:图片引自 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/
具体的使用,参见 http://blog.didispace.com/spring-cloud-starter-dalston-5-2/
引用:
程序猿DD Spring Cloud基础教程
Spring Cloud构建微服务架构:服务容错保护(Hystrix服务降级)【Dalston版】
Spring Cloud构建微服务架构:服务容错保护(Hystrix依赖隔离)【Dalston版】
Spring Cloud构建微服务架构:服务容错保护(Hystrix断路器)【Dalston版】
Spring Cloud构建微服务架构:Hystrix监控面板【Dalston版】
Spring Cloud构建微服务架构:Hystrix监控数据聚合【Dalston版】
Spring Cloud Dalston中文文档