[toc]

Hystrix

1. 背景

分布式系统环境下,服务间类似依赖非常常见,一个业务调用通常依赖多个基础服务。如下图,对于同步调用,当库存服务不可用时,商品服务请求线程被阻塞,当有大批量请求调用库存服务时,最终可能导致整个商品服务资源耗尽,无法继续对外提供服务。并且这种不可用可能沿请求调用链向上传递,这种现象被称为雪崩效应。

img

1.1 雪崩效应常见场景

  • 硬件故障:如服务器宕机,机房断电,光纤被挖断等。
  • 流量激增:如异常流量,重试加大流量等。
  • 缓存穿透:一般发生在应用重启,所有缓存失效时,以及短时间内大量缓存失效时。大量的缓存不命中,使请求直击后端服务,造成服务提供者超负荷运行,引起服务不可用。
  • 程序BUG:如程序逻辑导致内存泄漏,JVM长时间FullGC等。
  • 同步等待:服务间采用同步调用模式,同步等待造成的资源耗尽。

1.2 雪崩效应应对策略

针对造成雪崩效应的不同场景,可以使用不同的应对策略,没有一种通用所有场景的策略,参考如下:

  • 硬件故障:多机房容灾、异地多活等。
  • 流量激增:服务自动扩容、流量控制(限流、关闭重试)等。
  • 缓存穿透:缓存预加载、缓存异步加载等。
  • 程序BUG:修改程序bug、及时释放资源等。
  • 同步等待:资源隔离、MQ解耦、不可用服务调用快速失败等。资源隔离通常指不同服务调用采用不同的线程池;不可用服务调用快速失败一般通过熔断器模式结合超时机制实现。

综上所述,如果一个应用不能对来自依赖的故障进行隔离,那该应用本身就处在被拖垮的风险中。 因此,为了构建稳定、可靠的分布式系统,我们的服务应当具有自我保护能力,当依赖服务不可用时,当前服务启动自我保护功能,从而避免发生雪崩效应。本文将重点介绍使用Hystrix解决同步等待的雪崩问题。

2. 简介

  • Hystrix [hɪst’rɪks], 英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。

  • Hystrix也是Netflix公司的一款组件。

  • Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。

  • Hystix解决雪崩问题的手段有两个:

    • 线程隔离
    • 服务熔断

2.1 线程隔离示意图:

  • Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?

  • 服务降级:优先保证核心服务,而非核心服务不可用或弱可用。

  • 用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。

  • 触发Hystix服务降级的情况:

    • 线程池已满
    • 请求超时

1533829598310

2.2 Hystrix整个工作流如下

  1. 构造一个 HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数;
  2. 执行命令,Hystrix提供了4种执行命令的方法,后面详述;
  3. 判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;
  4. 判断熔断器是否打开,如果打开,跳到第8步;
  5. 判断线程池/队列/信号量是否已满,已满则跳到第8步;
  6. 执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;
  7. 统计熔断器监控指标;
  8. 走Fallback备用逻辑
  9. 返回请求响应

img

3. Hystrix入门

3.1 引入依赖

首先在justweb-service-consumer的pom.xml中引入Hystrix依赖:

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

3.2 开启熔断

​ 可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:**@SpringCloudApplication**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
*
* @SpringBootApplication
* @EnableDiscoveryClient == @EnableEurekaClient
* @EnableCircuitBreaker
*
* 可以整合成@SpringCloudApplication
* */
@SpringCloudApplication
public class JustwebServiceConsumerApplication {

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}

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

3.3 编写降级逻辑

3.3.1 设置到方法上

  • @HystrixCommand(fallbackMethod = “queryByIdFallBack”)
  • public String queryByIdFallBack(Long id){return “网络故障,请稍后再试!”;}
  • 返回值和被熔断的方法的返回值一致
  • 熔断方法的参数和被熔断方法的参数一致
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
@Controller
@RequestMapping("/consumer/user")
public class UserController {


@Autowired
private RestTemplate restTemplate;

/**
* eureka的客户端可以获取到eureka中的服务
* */
@Autowired
private DiscoveryClient discoveryClient;


@GetMapping
@ResponseBody
@HystrixCommand(fallbackMethod = "queryByIdFallBack")
public String queryUserById(@RequestParam("id") Long id){
//User user = this.restTemplate.getForObject("http://localhost/user/queryById/" + id, User.class);
//根据服务名称,获取实例,有可能是集群,所以service实例是个集合,从中取出第一个实例

ServiceInstance instance = discoveryClient.getInstances("SERVICE-PROVIDER").get(0);
//String url = "http://"+instance.getHost()+":"+instance.getPort()+"/user/queryById/"+id;
String url = "http://service-provider/user/queryById/"+id;
String user = this.restTemplate.getForObject(url, String.class);
return user;
}


/**@HystrixCommand(fallbackMethod = "queryByIdFallBack")
* 普通熔断方法:
* 返回值和被熔断的方法的返回值一致
* 熔断方法的参数和被熔断方法的参数一致
* */
public String queryByIdFallBack(Long id){
return "网络故障,请稍后再试!";
}

}

3.3.2 设置到类上(默认降级方法)

我们刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以我们可以把Fallback配置加在类上,实现默认fallback

  1. @HystrixCommand 熔断方法
  2. 返回值要和被熔断的方法的返回值一致
  3. 熔断方法不需要参数
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
@Controller
@RequestMapping("/consumer/user")
@DefaultProperties(defaultFallback = "fallBackMethod")//指定一个类的全局熔断方法
public class UserController {


@Autowired
private RestTemplate restTemplate;

/**
* eureka的客户端可以获取到eureka中的服务
* */
@Autowired
private DiscoveryClient discoveryClient;


@GetMapping
@ResponseBody
@HystrixCommand// 标记该方法需要熔断
public String queryUserById(@RequestParam("id") Long id){
//User user = this.restTemplate.getForObject("http://localhost/user/queryById/" + id, User.class);
//根据服务名称,获取实例,有可能是集群,所以service实例是个集合,从中取出第一个实例

ServiceInstance instance = discoveryClient.getInstances("SERVICE-PROVIDER").get(0);
//String url = "http://"+instance.getHost()+":"+instance.getPort()+"/user/queryById/"+id;
String url = "http://service-provider/user/queryById/"+id;
String user = this.restTemplate.getForObject(url, String.class);
return user;
}

/**
* //指定一个类的全局熔断方法
* @DefaultProperties(defaultFallback = "fallBackMethod")
*
* 1. @HystrixCommand 熔断方法
* 2. 返回值要和被熔断的方法的返回值一致
* 3. 熔断方法不需要参数
*/
public String fallBackMethod(){
return "全局网络故障,请稍后再试!";
}

}

3.4 设置超时

在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystix的默认超时时长为1,我们可以通过配置修改这个值:我们可以通过hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds来设置Hystrix超时时间。该配置没有提示。

1
2
3
4
5
6
7
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms

改造服务提供者的UserController接口,随机休眠一段时间,以触发熔断:

1
2
3
4
5
6
7
8
9
@GetMapping("{id}")
public User queryUserById(@PathVariable("id") Long id) {
try {
Thread.sleep(5000/7000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.userService.queryUserById(id);
}

3.5 服务熔断

3.5.1 熔断原理

熔断器,也叫断路器,其英文单词为:Circuit Breaker 熔断机制的原理很简单,像家里的电路熔断器,如果电路发生短路能立刻熔断电路,避免发生灾难。在分布式系统中应用这一模式之后, 服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
​ 不同于电路熔断只能断不能自动重连,Hystrix 可以实现弹性容错,当情况好转之后,可以自动重连。这就好比魔术师把鸽子变没了容易,但是真正考验技术的是如何把消失的鸽子再变回来。
​ 通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功则回到电路闭合状态,否则继续断开。

熔断状态机3个状态:

  • Closed:关闭状态,所有请求都正常访问。
  • Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
  • Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5 s)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续保持打开,再次进行休眠计时

3.5.2 动手实践

为了能够精确控制请求的成功或失败,我们在consumer的调用业务中加入一段逻辑:

1
2
3
4
5
6
7
8
9
@GetMapping("{id}")
@HystrixCommand
public String queryUserById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}

这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了清空service-provider中的休眠逻辑)

我们准备两个请求窗口:

当我们疯狂访问id为1的请求时(超过20次),就会触发熔断。断路器会断开,一切请求都会被降级处理。

此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有几毫秒左右:

1543053265477

不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:

1
2
3
4
5
6
7
8
#requestVolumeThreshold:触发熔断的最小请求次数,默认20
#errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
#sleepWindowInMilliseconds:休眠时长,默认是5000毫秒

circuitBreaker:
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 10000
errorThresholdPercentage: 50