Spring Cloud Hystrix

在微服务架构中,每个单元都在不同的进程中运行,依赖远程调用的方式执行,这就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,线程资源无法释放,最终导致自身服务的瘫痪,甚至导致整个系统的瘫痪。
为了解决这样的问题,因此产生了断路器等一系列的服务保护机制。

Spring Cloud Hystrix中实现了线程隔离、断路器等一系列的服务保护功能。它也是基于Netflix的开源框架 Hystrix实现的,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备了服务降级、服务熔断、线程隔离、请求缓存、请求合并以及服务监控等强大功能。

Hystrix服务降级

创建一个服务消费者hystrix-eureka-consumer-ribbon,pom.xml的dependencies节点中引入spring-cloud-starter-hystrix依赖:

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

主类中使用@EnableCircuitBreaker或@EnableHystrix注解开启Hystrix的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@EnableCircuitBreaker //或@EnableHystrix
@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {
/**
* 通过注解 @LoadBalanced 使用Ribbon负载均衡
* Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
* 这样就可以在Controller中,去掉通过LoadBalancerClient选取实例和拼接URL的步骤,直接通过RestTemplate发起请求。
* 请求的host位置并没有使用一个具体的IP地址和端口的形式,而是采用了服务名的方式组成,不需要具体的IP和端口。因为:
* Spring Cloud Ribbon有一个拦截器,它能够在这里进行实际调用的时候,自动的去选取服务实例,并将实际要请求的IP地址和端口替换这里的服务名,从而完成服务接口的调用。
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

还可以使用Spring Cloud应用中的@SpringCloudApplication注解来修饰应用主类,该注解的具体定义如下,包含了上述的注解。

1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {

}

在服务消费者服务中,新增ConsumerService类,在方法增加@HystrixCommand注解来指定服务降级方法,在controller中直接调用服务方法。

1
2
3
4
5
6
7
8
9
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
46
47
48
49
50
    @Service
class ConsumerService {

@Autowired
RestTemplate restTemplate;

/**
* 注解:@HystrixCommand用来指定服务降级方法
*/
@HystrixCommand(fallbackMethod = "userFallback")
public String[] users() {
String url = "http://provider-hello/users";
return restTemplate.getForObject(url, String[].class);
}

/*@HystrixCommand(fallbackMethod = "fallback")
public String users(String id) {
String url = "http://provider-hello/users/" + id;
return restTemplate.getForObject(url, String.class);
}*/

//如果服务降级了,userFallback
public String[] userFallback() {
String[] arr ={"err"};
return arr;
}
/*public String fallback() {
return "fallback";
}*/
}
```
控制器直接调用:
```java
@RestController
public class ConsumerController {
@Autowired
private ConsumerService consumerService;
@Autowired
private DiscoveryClient discoveryClient;

@RequestMapping(value = "/users", method = RequestMethod.GET)
public String[] users() throws InterruptedException{
return consumerService.users();
}

/*@RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
public String users(@PathVariable String id) {
return consumerService.users(id);
}*/
}

浏览正确输出值,为了验证服务降级,对代码做以下修改:

1
2
3
4
5
6
public String[] users() throws InterruptedException{
//为了出发服务降级,返回结果为arr
Thread.sleep(6000L);
System.out.println(Arrays.toString(discoveryClient.getServices().toArray()));
return consumerService.users();
}

由于延迟了,就返回fallback

依赖隔离

Hystrix通过实现线程池的隔离,它会为每一个Hystrix命令创建一个独立的线程池,这样就算某个在Hystrix命令包装下的依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的服务。通过对依赖服务的线程池隔离实现,可以带来如下优势:

  • 应用自身得到完全的保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的额其余部分。
  • 可以有效的降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响到应用其他的请求。
  • 当依赖的服务从失效恢复正常后,它的线程池会被清理并且能够马上恢复健康的服务,相比之下容器级别的清理恢复速度要慢得多。
  • 当依赖的服务出现配置错误的时候,线程池会快速的反应出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时,我们可以在不影响应用功能的情况下通过实时的动态属性刷新(后续会通过Spring Cloud Config与Spring Cloud Bus的联合使用来介绍)来处理它。
  • 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,此时线程池的监控指标信息会反映出这样的变化。同时,我们也可以通过实时动态刷新自身应用对依赖服务的阈值进行调整以适应依赖方的改变。
    每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步的访问。

总之,通过对依赖服务实现线程池隔离,让我们的应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。还可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。

线程池隔离的方案如此多的好处,使用者可能会担心为每一个依赖服务都分配一个线程池是否会过多地增加系统的负载和开销。但是Netflix在设计Hystrix的时候,认为隔离所带来的好处大于线程池上的开销。

如何使用

使用了@HystrixCommand来将某个函数包装成了Hystrix命令,这里除了定义服务降级之外,Hystrix框架还自动为这个方法实现了(框架内部)调用的隔离。依赖隔离、服务降级是一体化实现的。

Hystrix中除了使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销要比线程池的小,但是不能设置超时和实现异步访问,只有在依赖服务是足够可靠的情况下才使用信号量。
在HystrixCommand和HystrixObservableCommand中2处支持信号量的使用:

  • 命令执行:如果隔离策略参数execution.isolation.strategy设置为SEMAPHORE,Hystrix会使用信号量替代线程池来控制依赖服务的并发控制。
  • 降级逻辑:当Hystrix尝试降级逻辑时候,它会在调用线程中使用信号量。
    信号量的默认值为10,也可以通过动态刷新配置的方式来控制并发线程的数量。
断路器

分布式架构中,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),直接切断原来的主逻辑调用。
在Hystrix中的断路器除了切断主逻辑的功能之外,还有更复杂的逻辑。

在服务消费端的服务降级逻辑因为hystrix命令调用依赖服务超时,触发了降级逻辑,但是即使这样,受限于Hystrix超时时间的问题,调用依然很有可能产生堆积。

这个时候断路器就会发挥作用,断路器的三个重要参数:快照时间窗、请求总数下限、错误百分比下限。这个参数的作用分别是:

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
  • 请求总数下限:在快照时间窗内,必须满足请求总数下限才有资格根据熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用此时不足20次,即时所有的请求都超时或其他原因失败,断路器都不会打开。
  • 错误百分比下限:当请求总数在快照时间窗内超过了下限,比如发生了30次调用,如果在这30次调用中,有16次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%下限情况下,这时候就会将断路器打开。

在断路器未打开之前,服务会根据之前的降级在延迟时间时返回fallback,当熔断器在10秒内发现请求总数超过20,并且错误百分比超过50%,这个时候熔断器打开。
打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就不会等待5秒之后才返回fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。

在断路器打开之后,处理逻辑并没有结束,我们的降级逻辑已经被成了主逻辑,对于原来的主逻辑要恢复问题,hystrix实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。

综述:hystrix的断路器实现了对依赖资源故障的端口、对降级策略的自动切换以及对主逻辑的自动恢复机制,使得微服务在依赖外部服务或资源的时候得到了保护,同时对于一些具备降级逻辑的业务需求可以实现自动化的切换与恢复,相比于设置开关由监控和运维来进行切换的传统实现方式显得更为智能和高效。

Hystrix监控面板

hystrix-eureka-consumer-ribbon中接口实现使用了@HystrixCommand修饰,HystrixCommand和HystrixObservableCommand实例在执行过程中记录这些请求情况的指标信息会在内存中汇总,并保留一段时间,以供内部或外部进行查询使用,users这个接口的调用情况会被Hystrix记录下来,以用来给断路器和Hystrix Dashboard使用。

配置监控

见项目hystrix-dashboard
引入依赖:

1
2
3
4
5
6
7
8
9
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
46
47
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.hu</groupId>
<artifactId>Hystrix-Dashboard</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Dalston.SR1</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<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-actuator</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

配置:

1
2
3
4
5
6
7
8
server:
port: 7904
spring:
application:
name: hystrix-dashboard
logging:
level:
root: INFO

启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* EnableHystrixDashboard注解,开启Hystrix Dashboard功能
* SpringCloudApplication是 spring cloud注解,包含了如下注解:
* -@Target({ElementType.TYPE})
* -@Retention(RetentionPolicy.RUNTIME)
* -@Documented
* -@Inherited
* -@SpringBootApplication
* -@EnableDiscoveryClient Eureka客户端,和EnableEurekaClient一个意思
* -@EnableCircuitBreaker 熔断器
*/
@EnableHystrixDashboard
@SpringCloudApplication
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}

访问:http://localhost:7904/hystrix

Hystrix Dashboard的监控首页,该页面中并没有具体的监控信息。 页面上两个参数:
Delay:该参数用来控制服务器上轮询监控信息的延迟时间,默认为2000毫秒,我们可以通过配置该属性来降低客户端的网络和CPU消耗。
Title:该参数对应了上图头部标题Hystrix Stream之后的内容,默认会使用具体监控实例的URL,我们可以通过配置该信息来展示更合适的标题。

从页面的文字内容中我们可以知道,Hystrix Dashboard共支持三种不同的监控方式,依次为:

前两者都对集群的监控,需要整合Turbine才能实现,在这里对单个服务实例的监控。

Hystrix Dashboard监控单实例节点需要通过访问实例的/hystrix.stream接口来实现,那需要为服务实例添加这个端点,而添加该功能的步骤:

1、在服务实例hystrix-eureka-consumer-ribbon的pom.xml中新增spring-boot-starter-actuator监控模块以开启监控相关的端点并确保已经引入断路器的依赖spring-cloud-starter-hystrix

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

启动类中开启了断路器功能。

2、确保在服务实例的主类中已经使用@EnableCircuitBreaker或@EnableHystrix注解,开启了断路器功能。
之后再Hystrix Dashboard的首页输入http://localhost:7902/hystrix.stream,已启动对hystrix-eureka-consumer-ribbon的监控。

监控页面介绍:
在监控信息的左上部分找到两个重要的图形信息:一个实心圆和一条曲线。
实心圆,共有两种含义: 它通过颜色的变化代表了实例的健康程度,它的健康度从绿色、黄色、橙色、红色递减。 除了颜色的变化之外,大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。 所以通过该实心圆的展示,我们就可以在大量的实例中快速的发现故障实例和高压力实例。
曲线:用来记录2分钟内流量的相对变化,我们可以通过它来观察到流量的上升和下降趋势。

Hystrix集群监控数据聚合

使用Turbine来对服务的Hystrix数据进行聚合展示,有HTTP收集聚合和通过消息代理收集聚合两种方式。

1、通过HTTP收集聚合

创建一个模块,名字为:turbine,引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<artifactId>turbine</artifactId>

<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Dalston.SR1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 7922
spring:
application:
name: turbine
eureka:
client:
serviceUrl:
defaultZone: http://user:123456@localhost:8761/eureka
logging:
level:
root: INFO
management:
port: 9999
turbine:
app-config: eureka-consumer-ribbon-hystrix #指定了需要收集监控信息的服务名
cluster-name-expression: default #指定了集群名称为default
#服务数量非常多的时候,可以启动多个Turbine服务来构建不同的聚合集群,
#而该参数可以用来区分这些不同的聚合集群,同时该参数值可以在Hystrix仪表盘中用来定位不同的聚合集群,
#只需要在Hystrix Stream的URL中通过cluster参数来指定。
combine-host-port: true #让同一主机上的服务通过主机名与端口号的组合来进行区分,默认情况下会以host来区分不同的服务

启动类:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableAutoConfiguration
@EnableTurbine //注解开启Turbine
@EnableDiscoveryClient
public class TurbineApplication {

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

}

创建完毕,启动项目:
访问Hystrix Dashboard,输入对turbine监控的url:http://localhost:7922/turbine.stream
将看到针对服务hystrix-eureka-consumer-ribbon的聚合监控数据。

2、通过消息代理收集聚合

Spring Cloud在封装Turbine的时候,还实现了基于消息代理的收集实现,可以将所有需要收集的监控信息都输出到消息代理中,然后Turbine服务再从消息代理中异步的获取这些监控信息,最后将这些监控信息聚合并输出到Hystrix Dashboard中。
创建模块turbine-amqp,添加配置和依赖,见项目turbine-amqp。
创建完成后需要对服务消费者hystrix-eureka-consumer-ribbon做修改,使其监控信息能够输出到RabbitMQ上。
在消费者服务上增加对mq的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<artifactId>turbine-amqp</artifactId>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Dalston.SR1</version>
<relativePath/>
</parent>

<dependencies>
<!--spring-cloud-starter-turbine-amqp-->
<!--包含了spring-cloud-starter-turbine-stream和pring-cloud-starter-stream-rabbit。-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-turbine-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableAutoConfiguration
@EnableTurbineStream //启用Turbine Stream的配置
@EnableDiscoveryClient
public class TurbineApplication {

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 7927
spring:
application:
name: turbine-mq
eureka:
client:
serviceUrl:
defaultZone: http://user:123456@localhost:8761/eureka
logging:
level:
root: INFO
management:
port: 9999

完成配置后,通过Hystrix Dashboard开启对turbine-amqp的监控,可以获得和之前的效果,只是这里我们的监控信息收集时是通过了消息代理异步实现的。