Hystrix
- 1. 服务雪崩
- 2. 服务隔离
- 3. Hystrix 的引入
- 4. 开启Hystrix功能
- 5. 代码使用
- 6. Hystrix 服务隔离策略
- 7. Hystrix 服务降级
- 8. Hystrix 数据监控
- 9. Hystrix熔断
- 9.1 micro-order创建UserService接口以及实现类UserServiceImpl,用于抛异常,开启熔断功能
- 9.2 micro-order的UserController添加errorMessage方法
- 9.3 micro-web的UserService添加errorMessage方法
- 9.4 micro-web的UserServiceImpl重写errorMessage方法,添加降级方法errorMessageFallback
- 9.5 micro-web的UserController添加接口errorMessage
- 9.6 启动服务
- 9.7 打开监控页面
- 9.8 连续发送请求20次,http://localhost:8083/errorMessage?id=1,查看监控结果
- 9.9 熔断器的三个状态
- 代码下载地址
项目搭建参考https://blog.csdn.net/qq_40977118/article/details/104738485
1. 服务雪崩
- 雪崩是系统中的蝴蝶效应导致其发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。从源头上我们无法完全杜绝雪崩源头的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以我们可以提前评估。当整个微服务系统中,有一个节点出现异常情况,就有可能在高并发的情况下出现雪崩,导致调用它的上游系统出现响应延迟,响应延迟就会导致 tomcat 连接资源耗尽,导致该服务节点不能正常接收的情况。
2. 服务隔离
- 如果整个系统雪崩是由于一个接口导致的,由于这一个接口响应不及时导致问题,那么我们就有必要对这个接口进行隔离,就是只允许这个接口最多能接受多少的并发,做了这样的限制后,该接口的主机就会有空余线程来接收其他请求的情况,不会被哪个坏的接口占用满。 Hystrix 就是一个不错的服务隔离框架。
3. Hystrix 的引入
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
4. 开启Hystrix功能
@EnableCircuitBreaker
5. 代码使用
@HystrixCommand
6. Hystrix 服务隔离策略
6.1 线程池隔离
- 默认采用的就是线程池隔离策略THREAD,独立线程接收请求。线程池默认10个线程。通过一下参数修改线程数。
@HystrixProperty(name = "coreSize",value = "100")
package com.spring.fisher.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
private static String SERVER_NAME = "micro-order";
@Autowired
RestTemplate restTemplate;
/**
* Command属性
* execution.isolation.strategy 执行的隔离策略
* THREAD 线程池隔离策略 独立线程接收请求
* SEMAPHORE 信号量隔离策略 在调用线程上执行
* <p>
* execution.isolation.thread.timeoutInMilliseconds 设置HystrixCommand执行的超时时间,单位毫秒
* execution.timeout.enabled 是否启动超时时间,true,false
* execution.isolation.semaphore.maxConcurrentRequests 隔离策略为信号量的时候,该属性来配置信号量的大小,最大并发达到信号量时,后续请求被拒绝
* <p>
* circuitBreaker.enabled 是否开启断路器功能
* circuitBreaker.requestVolumeThreshold 该属性设置在滚动时间窗口中,断路器的最小请求数。默认20,如果在窗口时间内请求次数19,即使19个全部失败,断路器也不会打开
* circuitBreaker.sleepWindowInMilliseconds 改属性用来设置当断路器打开之后的休眠时间,休眠时间结束后断路器为半开状态,断路器能接受请求,如果请求失败又重新回到打开状态,如果请求成功又回到关闭状态
* circuitBreaker.errorThresholdPercentage 该属性设置断路器打开的错误百分比。在滚动时间内,在请求数量超过circuitBreaker.requestVolumeThreshold,如果错误请求数的百分比超过这个比例,断路器就为打开状态
* circuitBreaker.forceOpen true表示强制打开断路器,拒绝所有请求
* circuitBreaker.forceClosed true表示强制进入关闭状态,接收所有请求
* <p>
* metrics.rollingStats.timeInMilliseconds 设置滚动时间窗的长度,单位毫秒。这个时间窗口就是断路器收集信息的持续时间。断路器在收集指标信息的时会根据这个时间窗口把这个窗口拆分成多个桶,每个桶代表一段时间的指标,默认10000
* metrics.rollingStats.numBuckets 滚动时间窗统计指标信息划分的桶的数量,但是滚动时间必须能够整除这个桶的个数,要不然抛异常
* <p>
* requestCache.enabled 是否开启请求缓存,默认为true
* requestLog.enabled 是否打印日志到HystrixRequestLog中,默认true
*
* @HystrixCollapser 请求合并
* maxRequestsInBatch 设置一次请求合并批处理中允许的最大请求数
* timerDelayInMilliseconds 设置批处理过程中每个命令延迟时间
* requestCache.enabled 批处理过程中是否开启请求缓存,默认true
* <p>
* threadPoolProperties
* threadPoolProperties 属性
* coreSize 执行命令线程池的最大线程数,也就是命令执行的最大并发数,默认10
*/
@HystrixCommand(
fallbackMethod = "queryContentsFallback",
commandKey = "queryContents",
groupKey = "querygroup-one",
commandProperties ={
@HystrixProperty(name = "execution.isolation.strategy",value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000000000")},
threadPoolKey ="queryContentsHystrixPool",
threadPoolProperties ={//默认线程池是10
// @HystrixProperty(name = "coreSize",value = "100")
}
)
@Override
public String queryContents() {
log.info(Thread.currentThread().getName()+"=======micro-web:queryContents=======");
String result = restTemplate.getForObject("http://" + SERVER_NAME
+ "/queryUser", String.class);
return result;
}
public String queryContentsFallback() {
log.info("===============queryContentsFallback=================");
return null;
}
@HystrixCommand
@Override
public String queryName() {
return "queryName";
}
}
- 引入测试类jar包
<!--单位测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<!--模拟并发性能测试-->
<dependency>
<groupId>org.databene</groupId>
<artifactId>contiperf</artifactId>
<version>2.3.4</version>
</dependency>
- 创建测试类MyTest,线程池默认10个,这里模拟11个线程并发请求,应该有一个会请求失败。
package com.spring.fisher.test;
import com.spring.fisher.MicroWebAppliaction;
import com.spring.fisher.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.databene.contiperf.PerfTest;
import org.databene.contiperf.junit.ContiPerfRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.concurrent.CountDownLatch;
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = MicroWebAppliaction.class)
@WebAppConfiguration
public class MyTest {
@Autowired
UserService userService;
private Integer count = 11;
private CountDownLatch countDownLatch = new CountDownLatch(count);
//引入 ContiPerf 进行性能测试
@Rule
public ContiPerfRule contiPerfRule = new ContiPerfRule();
//11个线程 执行11次
@Test
@PerfTest(invocations = 11,threads = 11)
public void hystrixTest() {
log.info(Thread.currentThread().getName() + "==>" + userService.queryContents());
}
//使用countDownLatch进行测试
@Test
public void hystrixTest2() throws InterruptedException {
for (Integer i = 0; i < count; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(Thread.currentThread().getName() + "==>" + userService.queryContents());
}
}).start();
countDownLatch.countDown();
}
Thread.currentThread().join();
}
}
- 启动服务
5. 使用hystrixTest测试
- 单元测试中的线程和业务类中的线程是不一样的(Thread-12和hystrix-queryContentsHystrixPool-5),hystrix 是会单独创建线程的
6. 使用hystrixTest2测试
7. 两种测试方式的结果一致,11个请求,只有10个请求成功调用到UserServiceImpl的方法,1个请求失败,没有调用到UserServiceImpl的方法。
6.2 信号量隔离
- 信号量隔离是采用一个全局变量来控制并发量,一个请求过来全局变量加 1,当加到跟配置中的大小相等时就不再接受用户请求了。
//信号量
@HystrixProperty(name = "execution.isolation.strategy",value = "SEMAPHORE"),
//控制信号量隔离级别的并发大小,默认10
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests",value = "100")
- 使用hystrixTest方法测试,线程名称一致,单元测试中的线程和业务类中的线程是一样的,没有单独开启线程。
6.3 结论
- 线程池隔离策略,占用了内存,要开启线程,但是请求速度块。
- 信号量隔离策略,节省内存资源,不需要开启线程,但是用了轻量锁,请求速度相对慢一些。
7. Hystrix 服务降级
- 服务降级是对服务调用过程的出现的异常的友好封装,当出现异常时,不希望直接把异常原样返回,所以当出现异常时我们需要对异常信息进行包装,抛一个友好的信息给前端。
- 指定降级方法fallbackMethod
- 定义降级方法,降级方法的返回值和业务方法的方法值要一样
- 也可以多次降级
8. Hystrix 数据监控
- Hystrix 进行服务熔断时会对调用结果进行统计,比如超时数、bad 请求数、降级数、异常数等等都会有统计,那么统计的数据就需要有一个界面来展示, hystrix-dashboard 就是这么一个展示 hystrix 统计结果的服务。
8.1 创建一个module
8.2 jar包引入
<?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.spring.fisher</groupId>
<artifactId>springcloud-dashboard</artifactId>
<version>1.0-SNAPSHOT</version>
<name>springcloud-dashboard</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<spring-cloud.version>Hoxton.SR1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--eureka客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
8.3 创建配置文件application.properties
server.port=9990
spring.application.name=hystrix-dashboard
##暴露eureka服务的地址
eureka.client.serviceUrl.defaultZone=http://root:root@localhost:8763/eureka/
#自我保护模式,当出现出现网络分区、eureka在短时间内丢失过多客户端时,会进入自我保护模式,即一个服务长时间没有发送心跳,eureka也不会将其删除,默认为true
eureka.server.enable-self-preservation=false
# 暴露监控端点
management.endpoints.web.exposure.include=*
8.4 创建启动类
package com.spring.fisher;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
/**
* 监控界面:http://localhost:9990/hystrix
* 需要监控的端点(使用了hystrix组件的端点):http://localhost:8083/actuator/hystrix.stream
*/
@SpringBootApplication
@EnableEurekaClient
//开启hystrix监控
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class,args);
}
}
8.5 带HystrixCommand注解的服务配置
- hystrix.command.queryContents.circuitBreaker.sleepWindowInMilliseconds这种配置,在@HystrixCommand注解内配置是无效的,需要在properties文件中配置
- 指定commandKey来自定义配置参数,而不会使用全局的参数
#全局超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
#hystrix.command.<commandKey>作为前缀,默认是采用Feign的客户端的方法名字作为标识
hystrix.command.queryContents.circuitBreaker.sleepWindowInMilliseconds=20000
- 如果同时配置了threadPoolKey和groupKey,那么具有相同threadPoolKey的使用同一个线程池;
- 如果只配置了groupKey,那么具有相同的groupKey的使用同一个线程池。
- 被监控的hystrix-service服务需要开启Actuator的hystrix.stream端点
#hystrix.stream 开放所有的监控接口
management.endpoints.web.exposure.include=*
8.6 启动所有项目
8.7 Dashboard 界面
- http://localhost:9990/hystrix
- 输入http://localhost:8083/actuator/hystrix.stream
-title: 监控(随便写)
8.7 多次请求加了@HystrixCommand注解的接口
8.8 查看监控结果
9. Hystrix熔断
- 当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。
熔断发生的三个必要条件:
1、有一个统计的时间周期,滚动窗口
相应的配置属性—metrics.rollingStats.timeInMilliseconds ,默认10000毫秒
2、请求次数必须达到一定数量
相应的配置属性—circuitBreaker.requestVolumeThreshold ,默认20次
3、失败率达到默认失败率
相应的配置属性—circuitBreaker.errorThresholdPercentage ,默认50%
9.1 micro-order创建UserService接口以及实现类UserServiceImpl,用于抛异常,开启熔断功能
package com.spring.fisher.service;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public String errorMessage(Integer id) {
try {
int result = id / 0;
return String.valueOf(result);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
9.2 micro-order的UserController添加errorMessage方法
@RequestMapping("/errorMessage")
public String errorMessage(@RequestParam("id") Integer id) {
return userService.errorMessage(id);
}
9.3 micro-web的UserService添加errorMessage方法
9.4 micro-web的UserServiceImpl重写errorMessage方法,添加降级方法errorMessageFallback
@HystrixCommand(fallbackMethod = "errorMessageFallback")
@Override
public String errorMessage(Integer id) {
log.info("===============micro-web: errorMessage=================");
String result = restTemplate.getForObject("http://" + SERVER_NAME
+ "/errorMessage?id="+id, String.class);
return result;
}
public String errorMessageFallback(Integer id) {
log.info("===============errorMessageFallback=================");
return "errorMessageFallback";
}
9.5 micro-web的UserController添加接口errorMessage
@RequestMapping(value = "/errorMessage")
public String errorMessage(@RequestParam("id") Integer id) {
return userService.errorMessage(id);
}
9.6 启动服务
9.7 打开监控页面
9.8 连续发送请求20次,http://localhost:8083/errorMessage?id=1,查看监控结果
- 熔断前,先走业务方法,抛异常,走降级方法;
- 达到20次之后,熔断开启,直接走降级方法,不走业务方法
9.9 熔断器的三个状态
1、关闭状态
关闭状态时用户请求是可以到达服务提供方的;
2、开启状态
开启状态时用户请求是不能到达服务提供方的,直接会走降级方法;
3、半开状态
当hystrix熔断器开启时,过一段时间后(默认5s),熔断器就会由开启状态变成半开状态。
半开状态的熔断器是可以接受用户请求并把请求传递给服务提供方的,这时候如果远程调用返回成功,那么熔断器就会有半开状态变成关闭状态,反之,如果调用失败,熔断器就会有半开状态变成开启状态。
- 过一段时间,再次请求http://localhost:8083/errorMessage?id=1
- 第一次走了业务方法,因为抛异常,调用失败,所以后面请求直接走降级方法。
Hystrix功能建议在并发比较高的方法上使用,并不是所有方法都得使用的。