认识微服务(五)之 Hystrix 熔断器
- 1 简介
- 2 雪崩问题
- 3 线程隔离,服务降级:
- 3.1 原理
- 3.2 动手实践
- 3.2.1 引入依赖
- 3.2.2 开启熔断
- 3.2.3 改造消费者
- 3.2.4 改造服务提供者
- 3.2.5 启动测试
- 3.2.6 统一处理
- 3.2.7 优化
- 4 服务熔断
- 4.1 熔断原理
- 4.2 动手实践
- 5 项目地址
1 简介
Hystrix,即熔断器。
主页:https://github.com/Netflix/Hystrix/
Hystrix 是 Netflix 开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
熔断器 Hystrix 是容错管理工具,作用是通过隔离、控制服务从而对延迟和故障提供更强大的容错能力,避免整个系统被拖垮。
复杂分布式架构通常都具有很多依赖,当一个应用高度耦合其他服务时非常危险且容易导致失败,这种失败很容易伤害服务的调用者,最后导致一个接一个的连续错误,应用本身就处在被拖垮的风险中,最后失去控制,就像在一个高流量的网站中,某个单一的后端一旦发生延迟,将会在数秒内导致所有应用资源被耗尽。如何处理这些问题是有关系统性能和效率的关键性问题。
当在系统高峰时期,大量对微服务的调用可能会堵塞远程服务器的线程池,如果这个线程池没有和主应用服务器的线程池隔离,就可能导致整个服务器挂机。
Hystrix 使用自己的线程池,这样和主应用服务器线程池隔离,如果调用花费很长时间,会停止调用,不同的命令或命令组能够被配置使用它们各自的线程池,可以隔离不同的服务。
2 雪崩问题
微服务中,服务间调用关系错综复杂,一个请求,可能需要多个调用微服务接口才能实现,会形成非常复杂的调用链路:
如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务有可能调用其它服务。
如果此时,某个服务出现异常:
例如微服务 I 发生异常,请求阻塞,用户不会得到相应,则 tomcat 的这个线程不会释放,于是越来越多的用户请求到来, 越来越多的线程会阻塞:
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
就好比,一条手机生产线,生产不同的机器,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台手机无法装配,陷入等待零件的状态,直到零件就位,才可继续组装。此时如果有多个机型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。
Hystrix解决雪崩问题的手段有两个:
A、线程隔离,服务降级;
B、服务熔断。
3 线程隔离,服务降级:
3.1 原理
线程隔离示意图:
解读:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
用户的请求将不再被直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息)。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
触发Hystrix服务降级的情况:
A、线程池已满;
B、请求超时。
3.2 动手实践
3.2.1 引入依赖
首先在 consumer-service 中引入 Hystrix 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
3.2.2 开启熔断
添加 @EnableCircuitBreaker
注解或者 @EnableHystrix
,这里建议使用前者
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
// 这次我们使用了OkHttp客户端,只需要注入工厂即可
return new RestTemplate();
}
}
因为一个标准的 Eureka 服务,一般都需要 @EnableDiscoveryClient
、@SpringBootApplication
以及 @EnableCircuitBreaker
三个注解,所以 Spring 官方提供了一个更加简单的注解来代替这三个注解:@SpringCloudApplication
,所以以后用这一个就可以了。
@SpringCloudApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
// 这次我们使用了OkHttp客户端,只需要注入工厂即可
return new RestTemplate();
}
}
3.2.3 改造消费者
我们改造 ConsumerUserDao,添加一个用来访问的 user 服务的 DAO,并且声明一个失败时的回滚处理函数:
package com.zc.sc.dao;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.zc.sc.pojo.ConsumerUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.List;
/**
* @作者: zc
* @时间: 2021/2/3 11:14
* @描述: 通过 RestTemplate 远程查询 user-service 中的接口
*/
@Component
public class ConsumerUserDao {
private static final Logger logger = LoggerFactory.getLogger(ConsumerUserDao.class);
@Autowired
private RestTemplate restTemplate;
/*
@Autowired
DiscoveryClient discoveryClient; // spring包下的类
*/
@HystrixCommand(fallbackMethod = "queryUserByIdFallback")
public ConsumerUser queryUserByIdOne(Long id){
long begin = System.currentTimeMillis();
String url = "http://user-service/us/user/" + id;
ConsumerUser user = this.restTemplate.getForObject(url, ConsumerUser.class);
long end = System.currentTimeMillis();
// 记录访问用时:
logger.info("访问用时:{}", end - begin);
return user;
}
//这个方法与上面请求的方法返回值类型,参数列表必须一致!!!!!
public ConsumerUser queryUserByIdFallback(Long id){
ConsumerUser user = new ConsumerUser();
user.setId(id);
user.setUsername("用户信息查询出现异常!");
return user;
}
/* public ConsumerUser queryUserByIdOne(Long id) {
// List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// ServiceInstance instance = instances.get(0);
// String host = instance.getHost();
// int port = instance.getPort();
// return restTemplate.getForObject("http://"+host+":"+port+"/us/user/"+id,ConsumerUser.class);
return restTemplate.getForObject("http://user-service/us/user/"+id, ConsumerUser.class);
}*/
}
@HystrixCommand(fallbackMethod="queryUserByIdFallback")
:声明一个失败回滚处理函数 queryUserByIdFallback,当queryUserById 执行超时(默认是1000毫秒,默认时间在 HystrixCommandProperties.java 中default_executionTimeoutInMilliseconds=1000),就会执行 fallback 函数,返回错误提示。
为了方便查看熔断的触发时机,我们记录请求访问时间。
在原来的业务逻辑中调用这个DAO:
3.2.4 改造服务提供者
改造服务提供者 user-service 子项目,随机休眠一段时间,以触发熔断,不建议:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User queryById(Long id) {
return userMapper.selectByPrimaryKey(id);
}
/* public User queryById(Long id) {
// 为了演示超时现象,我们在这里然线程休眠,时间随机 0~2000毫秒
try {
Thread.sleep(new Random().nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.userMapper.selectByPrimaryKey(id);
}*/
}
建议改造 consumer-service 子项目:
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand(fallbackMethod = "queryByIdFallback")
public String queryById(@PathVariable("id") Long id) {
String url = "http://user-service/us/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(Long id) {
return "服务器忙!";
}
}
服务提供者 user-service 子项目中:
@RestController
@RequestMapping(value = "user")
public class UserController {
@Autowired
private UserService userService;
/* @RequestMapping("/{id}")
@ResponseBody
public User selectUser(@PathVariable("id") Long id) {
User user = userService.queryById(id);
return user;
}*/
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.userService.queryById(id);
}
}
3.2.5 启动测试
然后运行并查看日志:
3.2.6 统一处理
如果每一个方法都提供一个熔断处理方法,那么会显的类特别的臃肿,所以我们可以提供一个通用的处理方式,也就是在类上面添加一个注解@DefaultProperties
,直接修改 ConsumerController 进行测试:
@RestController // 相当于 @Controller 和 @ResponseBody
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
/*
@Autowired
private ConsumerUserService consumerUserService;
@GetMapping("{id}")
public ConsumerUser queryUserById(@PathVariable("id") Long id) {
return this.consumerUserService.queryUserByIdAll(id);
}*/
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand//开启启用
public String queryById(@PathVariable("id") Long id) {
String url = "http://user-service/us/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String defaultFallback() {
return "抱歉,服务器很忙!";
}
}
测试:
如果通用和特有的都存在,按照就近原则,特有获胜。
3.2.7 优化
为了更加合理的利用服务熔断时间处理,我们可以通过配置一定的参数,来决定当前请求触发熔断的条件。比如指定一定的超时时间。Hystix 的超时时间默认 1000ms,也就是说请求超过 1000ms 没有返回,就会触发熔断。默认时间在HystrixCommandProperties.java 中 default_executionTimeoutInMilliseconds=1000;
那么如何进行自定义配置呢?其实很简单,只需要在 HystrixCommand 中添加一个选项即可:
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
配置如下:
package com.zc.sc.controller;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.zc.sc.pojo.ConsumerUser;
import com.zc.sc.service.ConsumerUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
/**
* @作者: zc
* @时间: 2021/2/3 11:21
* @描述: Controller
*/
@RestController // 相当于 @Controller 和 @ResponseBody
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
// @Autowired
// private ConsumerUserService consumerUserService;
//
// @GetMapping("{id}")
// public ConsumerUser queryUserById(@PathVariable("id") Long id) {
// return this.consumerUserService.queryUserByIdAll(id);
// }
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "3000")
})
public String queryById(@PathVariable("id") Long id) {
String url = "http://user-service/us/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String defaultFallback() {
return "抱歉,服务器很忙!";
}
}
因为我们前面在 UserController 中休眠了 2s,所以这里配置的超时时间 3s 是大于这个 2s 的,所以可以正常访问:
那么如何为所有的请求进行统一配置呢?
我们可以在 application.yml 配置文件中进行配置。添加 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
来设置 Hystrix 超时时间。
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMillisecond: 3000 # 设置 hystrix 的超时时间为3000ms
4 服务熔断
4.1 熔断原理
熔断器,页脚断路器,其应为单词为:Circuit Breaker。
熔断机制的原理很简单,像家里的电路熔断器,如果电路发生短路能立刻熔断电路,避免发生灾难。在分布式系统中应用这一模式之后,服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
不同于电路熔断只能断不能自动重连,Hystrix 可以实现弹性容错,当情况好转之后,可以自动重连。这就好比魔术师把鸽子变没了容易,但是真正考验技术的是如何把消失的鸽子再变回来。
通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功则回到电路闭合状态,否则继续断开。
Hystrix熔断状态模型:
状态机有3个状态:
A、Closed:关闭状态(断路器关闭),所有请求都正常访问。
B、Open:开状态(断路器打开),所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例阈值为50%,请求次数最少不定于20次。
C、Half Open:半开状态,Open状态不是永久的,打开后会进入休眠时间(默认是5s)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路器,否则继续再次进行休眠计时。
4.2 动手实践
首先要删除之前在提供方 UserController 中设置休眠的 2000ms 代码,为了能够精准控制请求的成功或失败,我们在ConsumerController 的调用业务中加入如下一段逻辑:
package com.zc.sc.controller;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.zc.sc.pojo.ConsumerUser;
import com.zc.sc.service.ConsumerUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
/**
* @作者: zc
* @时间: 2021/2/3 11:21
* @描述: Controller
*/
@RestController // 相当于 @Controller 和 @ResponseBody
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
// @Autowired
// private ConsumerUserService consumerUserService;
//
// @GetMapping("{id}")
// public ConsumerUser queryUserById(@PathVariable("id") Long id) {
// return this.consumerUserService.queryUserByIdAll(id);
// }
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "3000")
})
public String queryById(@PathVariable("id") Long id) {
// 逻辑手动指定熔断
if(id % 2 == 0) {
throw new RuntimeException("");
}
String url = "http://user-service/us/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String defaultFallback() {
return "抱歉,服务器很忙!";
}
}
这样如果参数id为奇数,正常,偶数一定失败,此时 user-service 中的休眠逻辑便可以去除,因为我们可以手动准确控制熔断了。
测试:
不过,默认的熔断触发要求比较高,休眠时间较短,为了方便测试,我们可以通过配置修改熔断策略:
circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=60
解读:
- requestVolumeThreshold:触发熔断的最小请求次数,默认值为20
- sleepWindowInMilliseconds:休眠时长,默认5000毫秒
- errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
配置如下:
@RestController // 相当于 @Controller 和 @ResponseBody
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
// @Autowired
// private ConsumerUserService consumerUserService;
//
// @GetMapping("{id}")
// public ConsumerUser queryUserById(@PathVariable("id") Long id) {
// return this.consumerUserService.queryUserByIdAll(id);
// }
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
// @HystrixCommand(commandProperties = {
// @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
// value = "3000")
// })
@HystrixCommand(
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
})
public String queryById(@PathVariable("id") Long id) {
// 逻辑手动指定熔断
if(id % 2 == 0) {
throw new RuntimeException("");
}
String url = "http://user-service/us/user/" + id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String defaultFallback() {
return "抱歉,服务器很忙!";
}
}
访问测试:
在快速多次访问 http://localhost:8082/cs/consumer/2 之后,之后再去访问 http://localhost:8082/cs/consumer/1,就会出现如下异常,触发熔断。稍等一会之后再去访问 http://localhost:8082/cs/consumer/1,又会正常。