Spring Cloud Hystrix
开发环境
开发工具:Intellij IDEA 2018.2.6
springboot: 2.0.6.RELEASE
jdk:1.8.0_192
maven: 3.6.0
Hystrix:2.0.2.RELEASE
Hystrix简介
hystrix对应的中文名字是 “豪猪”,豪猪身上全是刺,能保护自己不受天敌的伤害,代表了一种防御机制,这与 hystrix 本身的功能不谋而合,因此 Netflix 团队将该框架命名为 Hystrix,并使用了对应的卡通形象做作为logo。Hystrix是一个通过添加延迟容忍和容错逻辑来帮助您控制这些分布式服务之间的交互的库。Hystrix通过隔离服务之间的访问点来实现这一点,停止跨级的级联故障,并提供备用选项,所有这些都可以提高系统的整体弹性。Hystrix是由Netflix的API团队在2011年开始的弹性工程工作演变而来的。2012年,Hystrix继续发展和成熟,Netflix的许多团队都采用了它。如今,在Netflix上,每天都有数百亿的线程被隔离,以及数以千亿计的信号隔离电话。这导致了正常运行时间和弹性的显著改善。
hystrix 提供了什么
-
资源隔离:(线程池隔离和信号量隔离)机制,限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其它服务调用。
-
限流机制:限流机制主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。
-
熔断机制:当失败率达到阀值自动触发降级(如因网络故障、超时造成的失败率真高),熔断器触发的快速失败会进行快速恢复。
-
降级机制:超时降级、资源不足时(线程或信号量)降级 、运行异常降级等,降级后可以配合降级接口返回托底数据。
-
缓存支持:提供了请求缓存、请求合并实现
-
通过近实时的统计/监控/报警功能,来提高故障发现的速度
-
通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度
结合 Spring Cloud 使用
- pom.xml
<?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">
<packaging>jar</packaging>
<groupId>com.andy</groupId>
<artifactId>spring-cloud-hystrix</artifactId>
<version>1.0.6.RELEASE</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.andy</groupId>
<artifactId>spring-cloud-common</artifactId>
<version>1.0.6.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.0.3.RELEASE</version>
<configuration>
<!--<mainClass>${start-class}</mainClass>-->
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- 启动类
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
/**
* <p>
*
* @author Leone
* @since 2018-02-11
**/
@EnableHystrix
@SpringCloudApplication
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
}
- application.yml
server:
port: 8109
spring:
application:
name: mc-hystrix
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka
instance:
appname: hystrix-service
feign:
hystrix:
enabled: true
# spring boot 2.x actuator 的配置
management:
endpoints:
web:
exposure:
include: "*"
base-path: /monitor
endpoint:
health:
show-details: always
# 配置hystrix
hystrix:
threadpool:
default:
coreSize: 10
metrics:
polling-interval-ms: 2000
enabled: true
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 1000 #超时时间
circuitBreaker:
requestVolumeThreshold: 3 #当在配置时间窗口内达到此数量后,进行短路。默认20个
sleepWindowInMilliseconds: 5 #短路多久以后开始尝试是否恢复,默认5s
errorThresholdPercentage: 50% #出错百分比阈值,当达到此阈值后,开始短路。默认50%
hystrix 基于注解结合 Spring Cloud 使用
import com.andy.common.entity.User;
import com.andy.hystrix.config.UserFeignClient;
import com.andy.hystrix.service.RibbonService;
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.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;
/**
* @author Leone
* @since 2017-12-22
**/
@Slf4j
@RestController
@RequestMapping("/hystrix")
//@DefaultProperties(defaultFallback = "defaultFallback")
public class HystrixController {
@Autowired
private RibbonService ribbonService;
@Autowired
private UserFeignClient userFeignClient;
@HystrixCommand(
fallbackMethod = "fallbackMethod",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "10"),//10个核心线程池,超过20个的队列外的请求被拒绝; 当一切都是正常的时候,线程池一般仅会有1到2个线程激活来提供服务
@HystrixProperty(name = "maxQueueSize", value = "100"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "20")},
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"), //命令执行超时时间
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2"), //若干10s一个窗口内失败三次, 则达到触发熔断的最少请求量
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "30000") //断路30s后尝试执行, 默认为5s
})
@GetMapping("/user/{userId}")
public User user(@PathVariable("userId") Long userId) throws Exception {
return userFeignClient.find(userId);
}
@GetMapping("/user/{userId}")
public User fallbackMethod(@PathVariable("userId") Long userId) {
return ribbonService.find(userId);
}
}
hystrix 实操
hystrix 资源隔离
Hystrix使用命令模式HystrixCommand(Command)包装依赖调用逻辑,每个命令在单独线程中/信号授权下执行。
可配置依赖调用超时时间,超时时间一般设为比99.5%平均时间略高即可.当调用超时时,直接返回或执行fallback逻辑。
为每个依赖提供一个小的线程池(或信号),如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
依赖调用结果分:成功,失败(抛出异常),超时,线程拒绝,短路。 请求失败(异常,拒绝,超时,短路)时执行fallback(降级)逻辑。
提供熔断器组件,可以自动运行或手动调用,停止当前依赖一段时间(10秒),熔断器默认错误率阈值为50%,超过将自动运行。
Hystrix的资源隔离策略有两种,分别为:线程池和信号量
对比 | 线程池隔离 | 信号量隔离 |
---|---|---|
线程 | 与调用线程非相同线程 | 与调用线程相同(jetty线程) |
开销 | 排队、调度、上下文开销等 | 无线程切换,开销低 |
异步 | 支持 | 不支持 |
并发支持 | 支持(最大线程池大小) | 支持(最大信号量上限) |
信号量的隔离
it executes on the calling thread and concurrent requests are limited by the semaphore count
我的理解是hystrix自己维护一定数量的信号量,每个线程都需要获取一个信号量才可以继续往下执行,没有获取到信号量的线程就会直接返回做fallback,执行完成后归还信号量。最重要的是,信号量的调用是同步的,也就是说,每次调用都得阻塞调用方的线程,直到结果返回。这样就导致了无法对访问做超时(只能依靠调用协议超时,无法主动释放)
- SemaphoreCommandTest.java
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
/**
* <p> 资源隔离:信号量隔离
*
* @author leone
* @since 2019-03-29
**/
public class SemaphoreCommandTest extends HystrixCommand<String> {
private final int id;
public SemaphoreCommandTest(int id) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
// since we're doing work in the run() method that doesn't involve network traffic
// and executes very fast with low risk we choose SEMAPHORE isolation
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)));
this.id = id;
}
@Override
protected String run() {
// a real implementation would retrieve data from in memory data structure
// or some other similar non-network involved work
return "id: " + id;
}
static class UnitTest {
public static void main(String[] args) {
SemaphoreCommandTest commandTest = new SemaphoreCommandTest(2);
String id = commandTest.execute();
System.out.println(id);
}
}
}
线程池隔离
it executes on a separate thread and concurrent requests are limited by the number of threads in the thread-pool
线程池隔离适合绝大多数的场景,对依赖服务的网络调用timeout,TPS要求高的,这种问题。Netflix在使用过程中详细评估了使用异步线程和同步线程带来的性能差异,结果表明在99%的情况下,异步线程带来的几毫秒延迟的完全可以接受的。
- ThreadPoolCommandTest.java
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixThreadPoolKey;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* hystrix 资源隔离:线程池隔离
*
* @author leone
* @since 2019-03-12
**/
public class ThreadPoolCommandTest extends HystrixCommand<String> {
private final String name;
/**
* 在继承hystrixCommand的构造函数中实现添加线程池参数记性资源隔离
*
* @param name
*/
public ThreadPoolCommandTest(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("ExampleCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ExampleThreadPool"))
);
this.name = name;
}
@Override
protected String run() {
// 依赖逻辑封装在run()方法中
return "Hello\tname: " + name + "\tthread name: " + Thread.currentThread().getName();
}
//调用实例
public static void main(String[] args) throws Exception {
// 每个Command对象不可以重复调用,否则报错:This instance can only be executed once. Please instantiate a new instance.
ThreadPoolCommandTest command = new ThreadPoolCommandTest("Synchronous-hystrix");
// 使用execute()同步调用代码,效果等同于:helloWorldCommandTest.queue().get();
String result = command.execute();
System.out.println("result: " + result);
command = new ThreadPoolCommandTest("Asynchronous-hystrix");
// 异步调用,可自由控制获取结果时机
Future<String> future = command.queue();
// get操作不能超过command定义的超时时间,默认:1秒
result = future.get(100, TimeUnit.MILLISECONDS);
System.out.println("result: " + result);
System.out.println("currentThread: " + Thread.currentThread().getName());
}
}
注意
因为Hystrix默认使用了线程池模式,所以对于每个Command,在初始化的时候,会创建一个对应的线程池,如果项目中需要进行降级的接口非常多,比如有上百个的话,不太了解Hystrix内部机制的同学,按照默认配置直接使用,可能就会造成线程资源的大量浪费。
hystrix 断路器
- 1、 创建一个 HystrixCommand 或 HystrixObservableCommand 实例
第一步就是构建一个 HystrixCommand 或 HystrixObservableCommand 实例来向其它组件发出操作请求,通过构造方法来创建实例。
HystrixCommand:返回一个单响应
HystrixObservableCommand:返回一个观察者发出的响应
- 2、 执行方法
这里有4个方法,前两个只适用于 HystrixCommand 不适用于 HystrixObservableCommand
execute():阻塞型方法,返回单个结果(或者抛出异常)
queue():异步方法,返回一个 Future 对象,可以从中取出单个结果(或者抛出异常)
observe():返回Observable 对象
toObservable():返回Observable 对象
- 3、 缓存判断
检查缓存内是否有对应指令的结果,如果有的话,将缓存的结果直接以 Observable 对象的形式返回
- 4、 断路器判断
检查Circuit Breaker的状态。如果Circuit Breaker的状态为开启状态,Hystrix将不会执行对应指令,而是直接进入失败处理状态(图中8)。如果Circuit Breaker的状态为关闭状态,Hystrix会继续执行(图5)
- 5、 线程池、任务队列、信号量的检查
确认是否有足够的资源执行操作指令。当线程池和队列(或者是信号量,当不使用线程池隔离模式的时候)资源满的时候,Hystrix将不会执行对应指令并且会直接进入失败处理状态(图8)
- 6、 HystrixObservableCommand.construct() 和 HystrixCommand.run()
如果资源充足,Hystrix将会执行操作指令。操作指令的调用最终都会到这两个方法:
HystrixCommand.run():返回一个响应或者抛出一个异常
HystrixObservableCommand.construct():返回一个可观测的发出响应(s)或发送一个onError通知
如果执行指令的时间超时,执行线程会抛出 TimeoutException 异常。Hystrix会抛弃结果并直接进入失败处理状态。如果执行指令成功,Hystrix会进行一系列的数据记录,然后返回执行的结果。
- 7、 统计断路器的健康情况
Hystrix会根据记录的数据来计算失败比率,一旦失败比率达到某一阈值将自动开启Circuit Breaker
- 8、 回退
如果我们在Command中实现了HystrixCommand.getFallback()方法(或HystrixObservableCommand. resumeWithFallback() 方法,Hystrix会返回对应方法的结果。如果没有实现这些方法的话,仍然 Hystrix会返回一个空的 Observable 对象,并且可以通过 onError 来终止并处理错误。
调用不同的方法返回不同的结果:
execute(): 将会抛出异常
queue(): 将会返回一个Future 对象,如果调用它的get()方法将会抛出异常
observe()和 toObservable():都会返回上述的 Observable 对象
- 9、 返回成功
如果Hystrix执行成功,返回的响应取决于在步骤2中调用命令。
execute():阻塞型方法,返回单个结果(或者抛出异常)
queue():异步方法,返回一个 Future 对象,可以从中取出单个结果(或者抛出异常)
observe():返回Observable 对象
toObservable():返回Observable 对象
- CircuitBreakerTest.java
import com.netflix.hystrix.*;
import org.junit.Test;
/**
* <p> 断路器
*
* @author leone
* @since 2019-03-12
**/
public class CircuitBreakerTest {
public static int num = 0;
//
static HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("circuitBreakerTestGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("circuitBreakerTestCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("circuitBreakerTestPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10)) // 配置线程池
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(10)
.withCircuitBreakerErrorThresholdPercentage(80));
// 未配置的值均取系统默认值
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
@Override
protected Object run() throws Exception {
if (num % 2 == 0) {
return String.valueOf(num);
} else {
int j = 0;
// 死循环模拟调用超时
while (true) {
j++;
}
}
}
@Override
protected Object getFallback() {
return "CircuitBreaker fallback: " + num;
}
};
@Test
public void circuitBreakerTest() throws Exception {
for (int i = 0; i < 30; i++) {
CircuitBreakerTest.num = i;
CircuitBreakerTest circuitBreakerTest = new CircuitBreakerTest();
String result = (String) circuitBreakerTest.hystrixCommand.execute();
System.out.println(result);
}
}
}