断路器的概念

有一条山路,被山洪冲垮了,但是大家都不知道。车辆还是沿着山路前进,直到被挡住了去路才发现此路不通,但是此时路上车太多了,前面的车走不了也退不回来,后面的车还在源源不断的开过来,所有人都挤在山上不能动。后来有人就制作了一个提示牌——“此路不通,车辆绕行”。以后每次山路被冲毁的时候,都把这块牌子立在山脚,来往的车辆一看见这牌子就掉头回去了。

这块牌子就是一个断路器,这条山路就是一个服务组件。平时服务组件正常访问的时候断路器不发挥作用,但是一旦服务组件运转异常,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗完毕,导致服务瘫痪。断路器的作用就显现出来了,断路打开后,可用避免连锁故障,fallback 方法可以直接返回一个固定值。

Hystrix

Hystrix,英文意思是豪猪,全身是刺,刺是一种保护机制。Hystrix 也是 Netflix 公司的一款组件。

Hystrix 的作用:实现服务熔断降级处理,保护微服务,防止雪崩效应发生。

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

雪崩效应

什么是雪崩效应?

分布式系统环境下,服务间类似依赖非常常见,一个业务调用通常依赖多个基础服务。

对大部分电商和快递公司来说,每年年底(Q4 季度)由于双 11 等大促活动的存在,将面对大量的用户流量,尤其是属于大促的那几天,无论是用户的商品订单还是物流订单,都将是平时的 3 倍以上。

如果这个时候,单个服务出现问题,调用这个服务就出现线程阻塞,此时若有大量的请求涌入,容器的线程资源就会被消耗完毕导致服务瘫痪。

对于同步调用,当库存服务不可用时,商品服务请求线程被阻塞,当有大批量请求调用库存服务时,最终可能导致整个商品服务资源耗尽,无法继续对外提供服务。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成不可估量的严重后果,这就是常说的服务故障的“雪崩效应”。

为了解决这个问题,有人就提出了一种解决问题的思路,断路器模型。就是每一个调用服务的接口处加一个断路器,默认是关闭的,当对服务调用时,不可用的次数达到一个阀值时,断路器就会打开,通过回调方法迅速返回一个值结束调用,避免出现连锁故障。

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

熔断器的原理

熔断器状态机有 3 个状态:

熔断器有三个状态 CLOSED、 OPEN、HALF_OPEN 。

熔断器默认关闭状态,当触发熔断后状态变更为 OPEN,在等待到指定的时间,Hystrix 会放请求检测服务是否开启,这期间熔断器会变为 HALF_OPEN 半开启状态,熔断探测服务可用则继续变更为 CLOSED 关闭熔断器。

  • Closed:关闭状态,所有请求正常访问
  • Open:打开状态,所有请求都会被降级

Hystrix 会对请求情况计数,当一定时间失败请求百分比达到阈值,则触发熔断,断路器完全关闭,默认失败比例的阈值是 50%,请求次数最低不少于 20 次

  • Half Open:半开状态

Open 状态不是永久的,打开一会后会进入休眠时间(默认 5 秒)。休眠时间过后会进入半开状态。
半开状态:熔断器会判断下一次请求的返回状况,如果成功,熔断器切回 closed 状态。如果失败,熔断器切回 open 状态。

熔断器的核心

  • 线程隔离
    • 线程隔离:是指 Hystrix 为每个依赖服务调用一个小的线程池,如果线程池用尽,调用立即被拒绝,默认不采用排队
  • 服务降级
    • 服务降级(兜底方法):优先保证核心服务,而非核心服务不可用或弱可用。触发 Hystrix 服务降级的情况:线程池已满、请求超时

线程隔离和服务降级之后,用户请求故障时,线程不会被阻塞,更不会无休止等待或者看到系统奔溃,至少可以看到执行结果(熔断机制)。

局部熔断/服务降级案例

目标:服务提供者的服务出现了故障,服务消费者快速失败给用户友好提示。体验服务降级

降级:某个方法发生故障,则返回默认的数据给用户,此时叫服务降级。其他方法可用,该方法不可用 ,只是降低权限

实现步骤

1、引入熔断的依赖坐标

user-consumer中加入依赖

1
2
3
4
5
<!--熔断器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2、开启熔断的注解

修改user-consumerUserConsumerApplication,在该类上添加@EnableCircuitBreaker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class, args);
}

/***
* 将RestTemplate的实例放到Spring容器中
* @return
*/
@Bean
// 开启负载均衡
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

注意:这里也可以使用@SpringCloudApplication,写了@SpringCloudApplication后,其他注解需要全部去掉。

3、服务降级处理

user-consumerUserController中添加降级处理方法

1
2
3
4
5
6
7
8
9
10
/****
* 服务降级处理方法
* 当某个方法发生异常或者执行超时的时候,则直接让该方法处理用户的请求
* @return
*/
public User failBack(Integer id) {
User user = new User();
user.setUsername("服务降级,默认处理!");
return user;
}

在有可能发生问题的方法上添加@HystrixCommand注解来使用降级处理调用,例如在queryById方法上添加降级调用

1
2
3
4
5
6
7
8
9
10
11
12
@HystrixCommand(fallbackMethod = "failBack")
@GetMapping(value = "/{id}")
public User queryById(@PathVariable(value = "id") Integer id) {
// 获取指定实例
List<ServiceInstance> instances = discoveryClient.getInstances("user-provider");
// 获取第一个实例对象
ServiceInstance serviceInstance = instances.get(0);
// 拼接服务地址
// "http://"+ 服务的主机 + ":" +服务的端口号
String instanceUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/find/" + id;
return restTemplate.getForObject(instanceUrl, User.class);
}

启动eureka-serveruser-consumer 还有 user-provider

请求:http://localhost:18082/consumer/3

其他熔断策略配置

熔断后休眠时间:sleepWindowInMilliseconds
熔断触发最小请求次数:requestVolumeThreshold
熔断触发错误比例阈值:errorThresholdPercentage
熔断超时时间:timeoutInMilliseconds

user-consumer 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 配置熔断策略:
hystrix:
command:
default:
circuitBreaker:
# 强制打开熔断器 默认false关闭的。测试配置是否生效
forceOpen: false
# 触发熔断错误比例阈值,默认值50%
errorThresholdPercentage: 50
# 熔断后休眠时长,默认值5秒
sleepWindowInMilliseconds: 10000
# 熔断触发最小请求次数,默认值是20
requestVolumeThreshold: 10
execution:
isolation:
thread:
# 熔断降级超时设置,默认为2秒
timeoutInMilliseconds: 2000

全局服务降级的 fallback 方法

两种编写方式:编写在类上,编写在方法上。

在类的上边对类的所有方法都生效。在方法上,仅对当前方法有效。

1、方法上服务降级的 fallback 兜底方法

1
2
3
// 使用HystrixCommon注解,定义
// 用来声明一个降级逻辑的fallback兜底方法
@HystrixCommand(fallbackMethod="failBack")

2、类上默认服务降级的 fallback 兜底方法

1
2
3
// 刚才把fallback写在了某个业务方法上,如果方法很多,可以将FallBack配置加在类上,实现默认FallBack
// 在类上,指明统一的失败降级方法
@DefaultProperties(defaultFallback=”defaultFailBack“)

添加一个全局熔断方法,.在queryById方法上只添加@HystrixCommand注解,在类上添加@DefaultProperties注解及内容

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
@RestController
@DefaultProperties(defaultFallback = "defaultFallback")
@RequestMapping(value = "/default/consumer")
public class DefaultUserController {

/**
* 全局的服务降级处理方法
*/
public User defaultFallback(){
User user = new User();
user.setUsername("Default---服务降级,默认处理");
return user;
}


@Autowired
private RestTemplate restTemplate;

@Autowired
private DiscoveryClient discoveryClient;

/****
* 在user-consumer服务中通过RestTemplate调用user-provider服务
* @param id
* @return
*/
@GetMapping(value = "/{id}")
@HystrixCommand
public User queryById(@PathVariable(value = "id")Integer id){
String url = "http://user-provider/user/find/"+id;
return restTemplate.getForObject(url,User.class);
}
}

测试访问:http://localhost:18082/default/consumer/3