SpringCloud Hystrix熔断器
15、Hystrix熔断器:简介及作用
目标:理解Hystrix的作用
介绍:Hystrix,英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。 是Netflix公司的一款组件。
源码访问地址:https://github.com/Netflix/Hystrix
那么Hystrix的作用是什么呢?具体要保护什么呢?
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。
- 作用:防止雪崩
什么是雪崩问题?
一个微服务中,可能会对外提供多个HTTP接口,以下每个Dependency当成一个HTTP接口:
如果此时,某个HTTP接口出现异常(调用超时)例如:下图中的Dependency I:
假如HTTP接口 I 发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞。
服务器支持的线程数是有限的(tomcat默认200个线程),若请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它HTTP接口都不可用,并且假如有其他的微服务需要调用这个接口,岂不是也跟着阻塞,也拖累了其他的微服务所在服务器也资源耗尽?这就叫做雪崩效应。
这就好比银行的柜台窗口,假如现在有10个窗口,每个窗口理解成一个线程,每个窗口都可以办理各种业务(例如存钱、取钱、贷款)。这个时候1个客户需要办理贷款业务,耗费时间很久,一直占用一个窗口。后面又来了9个客户也做贷款,也各自占用了一个窗口,那10个窗口岂不是都不能处理其他业务了?
Hystrix解决雪崩问题的手段主要是服务降级,包括:
- 线程隔离
- 服务熔断(断路器)
16、Hystrix熔断器:线程隔离原理
线程隔离示意图:
- Hystrix为每个HTTP接口调用分配一个小的线程池(默认10个线程),用户的请求将不再直接访问接口,而是通过线程池中的空闲线程来访问接口,如果线程池已满,或者请求超时,或请求接口异常,则会进行降级处理,什么是服务降级?
服务降级:保证服务弱可用。
-
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息)。
-
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有影响。
-
触发Hystrix服务降级的条件
-
请求报错
-
线程池已满
-
请求超时
-
17、Hystrix熔断器:动手实践线程隔离
17.1 实现步骤
-
第一步:引入依赖,在user-consumer消费端系统的pom.xml文件添加如下依赖:
<!-- 配置hystrix启动器 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
第二步:开启熔断器,在启动类上添加注解:@EnableCircuitBreaker
@SpringBootApplication @EnableDiscoveryClient // 开启Eureka客户端 @EnableCircuitBreaker // 开启熔断器 public class ConsumerApplication { // ...... }
可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication
因此,我们可以使用这个组合注解来代替之前的3个注解:
@SpringCloudApplication
public class ConsumerApplication {
// ......
}
-
第三步:编写降级逻辑,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用@HystrixCommond注解来完成,改造ConsumerController:
@RestController @RequestMapping("/consumer") @Slf4j public class ConsumerController { /** 注入发现者 */ @Autowired private DiscoveryClient discoveryClient; @Autowired private RestTemplate restTemplate; /** 根据主键id查询用户 */ @GetMapping("/{id}") @HystrixCommand(fallbackMethod = "findOneFallback") public String findOne(@PathVariable("id")Long id){ /*// 根据服务id获取该服务的全部服务实例 List<ServiceInstance> instances = discoveryClient .getInstances("user-service"); // 获取第一个服务实例(因为目前我们只有一个服务实例) ServiceInstance serviceInstance = instances.get(0); // 获取服务实例所在的主机 String host = serviceInstance.getHost(); // 获取服务实例所在的端口 int port = serviceInstance.getPort(); // 定义服务实例访问URL String url = "http://" + host + ":" + port + "/user/" + id;*/ // 定义服务实例访问URL String url = "http://user-service/user/" + id; return restTemplate.getForObject(url, String.class); } public String findOneFallback(Long id){ log.error("查询用户信息失败。id:{}", id); return "对不起,网络太拥挤了!"; } }
要注意,因为熔断的降级方法必须跟原方法保证相同的参数列表和返回值声明。而失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以把findOne的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
说明:
1、@HystrixCommand(fallbackMethod=“findOneFallBack”):用来声明一个降级逻辑的方法
2、@HystrixCommand默认分配了10个线程,可以修改:
@HystrixCommand(threadPoolProperties = {
@HystrixProperty(name = “coreSize”, value = “3”) // 线程池核心线程数大小,默认10个
}) -
第四步:测试降级逻辑,当user-service正常提供服务时,访问与以前一致,但是当将user-service关掉时,会发现页面返回了降级处理信息:
第五步:使用默认fallback方法:刚才是为一个接口指定降级方法,如果接口很多,那岂不是要写很多降级方法?因此可以在整个类上加一个注解,指定一个默认fallback方法
package cn.itcast.consumer.controller;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/consumer")
@Slf4j
// 该类中所有方法返回类型要与该方法的返回类型一致,且必须采用String作为返回值
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
/** 注入发现者 */
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
/** 根据主键id查询用户 */
@GetMapping("/{id}")
@HystrixCommand
public String findOne(@PathVariable("id")Long id){
/*// 根据服务id获取该服务的全部服务实例
List<ServiceInstance> instances = discoveryClient
.getInstances("user-service");
// 获取第一个服务实例(因为目前我们只有一个服务实例)
ServiceInstance serviceInstance = instances.get(0);
// 获取服务实例所在的主机
String host = serviceInstance.getHost();
// 获取服务实例所在的端口
int port = serviceInstance.getPort();
// 定义服务实例访问URL
String url = "http://" + host + ":" + port + "/user/" + id;*/
// 定义服务实例访问URL
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
public String findOneFallback(Long id){
log.error("查询用户信息失败。id:{}", id);
return "对不起,网络太拥挤了!";
}
public String defaultFallback(){
return "默认提示:对不起,网络太拥挤了!";
}
}
第六步:重启user-consumer,再次访问测试:
-
第七步:超时配置,在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystrix的默认超时时长为1秒,我们可以通过配置修改这个值(项目中一般使用它的默认值1秒),在user-consumer的yml文件中添加超时配置:
# 线程隔离 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2000
这个配置会作用于全局所有接口,为了触发超时,可以在user-service中的userController中休眠2秒:
public User findOne(@PathVariable("id") Long id) try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } return userService.findOne(id); }
-
第八步:重启user-service、user-consumer,再次访问user-consumer测试:
可以发现,请求的时长已经到了2s+,证明配置生效了。 如果把休眠时间修改到2秒以下,又可以正常访问了。
17.2 小结
-
加入hystrix启动器:spring-cloud-starter-netflix-hystrix
-
线程隔离需要用到注解:
- @EnableCircuitBreaker 启用熔断器
- @HystrixCommand(fallbackMethod = “findOneFallback”)
- @DefaultProperties(defaultFallback = “defaultFallback”)
- 提供降级方法
18、Hystrix熔断器:服务熔断原理
熔断器,也叫断路器,其英文单词为:Circuit Breaker
Hystrix的熔断状态机模型:
状态机有3个状态:
- Closed:关闭状态(断路器关闭),访问该请求都正常访问。
- Open:打开状态(断路器打开),访问该请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求达到阈值,则触发熔断,断路器会打开。默认失败比例的阈值是:请求失败比例超过50% 或 请求失败次数超过20次。 这个时候访问这个接口全部直接返回降级信息。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S),5S后断路器会自动进入半开状态。
- 此时再访问一次请求,若这个请求是正常的,则会关闭断路器,变成关闭状态.
- 否则重新变成打开状态,再次进行5秒休眠计时。
19、Hystrix熔断器:动手实践服务熔断
19.1 实现步骤
-
第一步:为了能够精确控制请求的成功或失败,在user-consumer的调用业务中加入一段逻辑
/** 根据主键id查询用户 */ @GetMapping("/{id}") @HystrixCommand public String findOne(@PathVariable("id")Long id){ if (id == 1){ throw new RuntimeException("太忙了!"); } // 定义服务实例访问URL String url = "http://user-service/user/" + id; return restTemplate.getForObject(url, String.class); }
说明:这样如果参数是id为1,一定失败,其它情况都成功。(不要忘了注释user-service中的休眠逻辑),执行流程如下:
1、当访问:http://localhost:8080/consumer/1,肯定失败,返回降级逻辑。
2、当访问:http://localhost:8080/consumer/2,肯定成功。
3、当我们疯狂访问id为1的请求时(超过20次),就会触发熔断。断路器会打开,一切请求都会被降级处理。
4、此时你访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有5秒左右。
5、5秒后进入半开状态之后,若再访问id为2的请求是可以的。
-
不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:
circuitBreaker.requestVolumeThreshold=10 # 触发熔断的最小请求次数,默认20 circuitBreaker.sleepWindowInMilliseconds=20000 # 休眠时长,默认是5000毫秒 circuitBreaker.errorThresholdPercentage=50 # 触发熔断的失败请求最小占比,默认50%
默认配置:com.netflix.hystrix.HystrixCommandProperties
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ZAvCMyU-1614169245473)(assets/1572162913791.png)]
-
第二步:配置服务熔断参数(user-consumer)
/** 根据主键id查询用户 */ @GetMapping("/{id}") @HystrixCommand(commandProperties = { @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10"), @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="20000"), @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="50") }) public String findOne(@PathVariable("id")Long id){ if (id == 1){ throw new RuntimeException("太忙了!"); } // 定义服务实例访问URL String url = "http://user-service/user/" + id; return restTemplate.getForObject(url, String.class); }
-
第三步:访问user-consumer测试
- 请求 http://localhost:8080/consumer/1 10次
- 请求 http://localhost:8080/consumer/2 1次(失败),必须等到20秒后才能成功。
Memorial Day is 512 days |