Hystrix的工作流程
- 构造一个HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数
- 执行命令,Hystrix提供了四种执行命令的方式
- 判断是否使用缓存响应请求,若启用了缓存,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;
- 判断熔断器是否打开,如果打开,跳到第8步;
- 判断线程池/队列/信号量是否已满,已满则跳到第8步;
- 执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;
- 统计熔断器监控指标
- 走Fallback备用逻辑
- 返回请求响应
Hystrix的容错处理
启动类上添加一个@EnableHystrix注解
package com.xfgg.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
@SpringBootApplication
@EnableHystrix
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
在调用接口的方法上添加一个@HystrixCommand 注解,用于指定依赖服务调用延迟或失败时调用的方法
package com.xfgg.demo.controller;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class Usercontroller {
@Autowired
RestTemplate restTemplate;
@GetMapping("/callHello")
@HystrixCommand(fallbackMethod = "defaultCallHello")
public String callHello() {
String result = restTemplate.getForObject("http://localhost:8088/house/hello", String.class);
return result;
}
public String defaultCallHello(){
return "fail";
}
}
调用失败触发熔断时会用defaultCallHello方法来回退具体的内容
去掉启动类上的@EnableHystrix注解,重启服务,再次调用接口返回的是500的错误信息
{
code: 500,
message: "I/O error on GET request for
"http://localhost:8088/house/hello": Connection refused; nested
exception is java.net.ConnectException: Connection refused
", data:
null
}
Feign整合Hystrix实现容错处理
在属性文件中开启feign对Hystrix的支持
feign.hystrix.enabled=true
两种方式来整合
- Fallback方式
在 Feign 的客户端类上的 @FeignClient 注解中指定 fallback 进行回退
创建一个 Feign 的客户端类 UserRemoteClient,为其配置 fallback
package com.xfgg.demo.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "eureka-client-user-service",fallback = UserRemoteClientFallback.class)
public interface UserRemoteClient {
@GetMapping("/user/hello")
String hello();
}
UserRemoteClientFallback 类需要实现 UserRemoteClient 类中所有的方法,返回回退时的内容
package com.xfgg.demo.feign;
import org.springframework.stereotype.Component;
@Component
public class UserRemoteClientFallback implements UserRemoteClient{
@Override
public String hello() {
return "fail";
}
}
停掉所有 eureka-client-user-service 服务,然后访问 /callHello 接口,这个时候 eureka-client-user-service 服务是不可用的,必然会触发回退,返回的是fail字符
在这种情况下,如果你的接口调用了多个服务的接口,那么只有 eureka-client-user-service 服务会没数据,不会影响别的服务,如果不用 Hystrix 回退处理,整个请求都将失败
{
code:200,
message:"",
data:{
id:1,
money:100.12,
name:"fail"
}
}
- FallbackFactory方式
fallback已经可以实现服务不可用时回退的功能,如果想知道回退的原因可以使用FallbackFactory来实现回退功能
package com.xfgg.demo.feign;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);
@Override
public UserRemoteClient create(final Throwable throwable) {
logger.error("UserRemoteClient回退",throwable);
return new UserRemoteClient() {
@Override
public String hello() {
return "fail";
}
};
}
}
FallbackFactory的使用是在@FeignClient注解中用fallbackFactory指定回退类
package com.xfgg.demo.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "eureka-client-user-service",fallback = UserRemoteClientFallback.class,fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {
@GetMapping("/user/hello")
String hello();
}
Feign中禁用Hystrix
properties文件中
feign.hystrix.enabled=false
或者添加配置类
@Configuration
public class FeignConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}
Hystrix的实时监控
添加Actuator的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动类中添加 @EnableHystrix 开启 Hystrix
启动过程中出现了sl4j日志文件依赖冲突
去掉其中一个以来就可以运行了
运行成功将暴露actuator端点
看到一直在输出“ping:”,出现这种情况是因为还没有数据,等到 HystrixCommand 执行了之后就可以看到具体数据
调用一下 /callHello 接口 http://localhost:8086/callHello,访问之后就可以看到 http://localhost:8086/actuator/hystrix.stream 这个页面中输出的数据了
使用Dashboard对监控进行图形化展示
在 pom.xml 中添加 dashboard 的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
在启动类头部添加注解@EnbaleHystrixDashBoard注解
package com.xfgg.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
属性配置文件中配置服务名称和服务端口
spring.application.name=hystrix-dashboard-demo
server.port=9011
启动服务,访问 http://localhost:9011/hystrix 就可以看到 dashboard 的主页
在主页中有 3 个地方需要我们填写,第一行是监控的 stream 地址,也就是将之前文字监控信息的地址输入到第一个文本框中。第二行的 Delay 是时间,表示用多少毫秒同步一次监控信息,Title 是标题,这个可以随便填写
输入完成后就可以点击 Monitor Stream 按钮以图形化的方式查看监控的数据了
界面监控参数
完整的Hystrix熔断降级实例
创建eureka注册中心
配置文件
spring.application.name=spring-cloud-eureka
server.port=8071
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://master:8071/eureka/
创建服务提供者
配置文件
spring.application.name=spring-cloud-producer
server.port=8072
eureka.client.service-url.defaultZone=http://localhost:8071/eureka/
创建服务消费者
ExampleRemote
package com.xfgg.demo.remote;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "spring-cloud-producer",fallback = ExampleRemoteHystrix.class)
public interface ExampleRemote {
@RequestMapping("/")
String echo(@RequestParam(value = "str")String str);
}
ExampleRemoteHystrix
package com.xfgg.demo.remote;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestParam;
@Component
public class ExampleRemoteHystrix implements ExampleRemote{
@Override
public String echo(@RequestParam(value = "str") String str){
return "hello"+str+", this message send failed";
}
}
效果图
关闭服务提供者然后调用,触发fallback
通过修改参数了解相关接口性能指标
hystrix的各种配置和各种参数
HystrixCommand
配置方式
通过在方法上添加@HystrixCommand注解并配置注解的参数来配置,但有的时候会有多个Hystrix方法,会出现重复代码所以我们使用@DefaultProperties注解来给整个类的Hystrix方法设置一个默认值
@DefaultProperties(defaultFallback = "fallBack")
在需要返回错误信息的服务调用方法上直接加入注解即可
@HystrixComman
完整代码
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "fallBack")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
@HystrixCommand
public String queryById(@PathVariable("id") Long id){
String url = "http://user-server/user/"+id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String fallBack(){
return "服务太拥挤了,请稍后再试!";
}
}
配置项
- commandKey:标识一个Hystrix命令,默认会取被注解的方法名,取名要唯一,防止多个方法之间因为键重复而互相影响
- groupKey:一组Hystrix命令的集合,可不配置
- threadPoolKey:用来标识一个线程池,没设置的话会取groupkey,第一个方法执行后线程池的属性就固定了,
- commandProperties:与此命令相关的属性
- threadPoolProperties:与线程池相关的属性
- observableExecutionMode:当 Hystrix 命令被包装成 RxJava 的 Observer 异步执行时,此配置指定了 Observable 被执行的模式,默认是 ObservableExecutionMode.EAGER,Observable 会在被创建后立刻执行,而 ObservableExecutionMode.EAGER模式下,则会产生一个 Observable 被 subscribe 后执行。我们常见的命令都是同步执行的,此配置项可以不配置。
- ignoreExceptions:默认 Hystrix 在执行方法时捕获到异常时执行回退,并统计失败率以修改熔断器的状态,而被忽略的异常则会直接抛到外层,不会执行回退方法,也不会影响熔断器的状态。
- raiseHystrixExceptions:当配置项包括 HystrixRuntimeException 时,所有的未被忽略的异常都会被包装成 HystrixRuntimeException,配置其他种类的异常好像并没有什么影响。
- fallbackMethod:方法执行时熔断、错误、超时时会执行的回退方法,需要保持此方法与 Hystrix 方法的签名和返回值一致。
- defaultFallback:默认回退方法,当配置 fallbackMethod 项时此项没有意义,另外,默认回退方法不能有参数,返回值要与 Hystrix方法的返回值相同。
CommandProperties
Hystrix 的命令属性是由 @HystrixProperty 注解数组构成的,HystrixProperty 由 name 和 value 两个属性,数据类型都是字符串。
线程隔离
- execution.isolation.strategy:配置请求隔离的方式,有 threadPool(线程池,默认)和 semaphore(信号量)两种,信号量方式高效但配置不灵活,我们一般采用 Java 里常用的线程池方式。
- execution.timeout.enabled:是否给方法执行设置超时,默认为true
- execution.isolation.thread.timeoutInMilliseconds:方法执行超时时间,默认值是1000,即1秒,此值根据业务场景配置
- execution.isolation.thread.interruptOnTimeout: execution.isolation.thread.interruptOnCancel:execution.isolation.thread.interruptOnCancel:是否在方法执行超时/被取消时中断方法。需要注意在 JVM 中我们无法强制中断一个线程,如果 Hystrix 方法里没有处理中断信号的逻辑,那么中断会被忽略。
- execution.isolation.semaphore.maxConcurrentRequests:默认值是 10,此配置项要在 execution.isolation.strategy 配置为 semaphore 时才会生效,它指定了一个 Hystrix 方法使用信号量隔离时的最大并发数,超过此并发数的请求会被拒绝。信号量隔离的配置就这么一个,也是前文说信号量隔离配置不灵活的原因。
统计器(看得我一愣一愣的)
滑动窗口:Hystrix 的统计器是由滑动窗口来实现的。
桶:bucket 是 Hystrix 统计滑动窗口数据时的最小单位。
- metrics.rollingStats.timeInMilliseconds:此配置项指定了窗口的大小,单位是 ms,默认值是 1000,即一个滑动窗口默认统计的是 1s 内的请求数据。
- metrics.healthSnapshot.intervalInMilliseconds:它指定了健康数据统计器(影响 Hystrix 熔断)中每个桶的大小,默认是 500ms,在进行统计时,Hystrix 通过 metrics.rollingStats.timeInMilliseconds / metrics.healthSnapshot.intervalInMilliseconds 计算出桶数,在窗口滑动时,每滑过一个桶的时间间隔时就统计一次当前窗口内请求的失败率。
- metrics.rollingStats.numBuckets:Hystrix 会将命令执行的结果类型都统计汇总到一块,给上层应用使用或生成统计图表,此配置项即指定了,生成统计数据流时滑动窗口应该拆分的桶数。此配置项最易跟上面的 metrics.healthSnapshot.intervalInMilliseconds 搞混,认为此项影响健康数据流的桶数。 此项默认是 10,并且需要保持此值能被 metrics.rollingStats.timeInMilliseconds 整除。
- metrics.rollingPercentile.enabled:是否统计方法响应时间百分比,默认为 true 时,Hystrix 会统计方法执行的 1%,10%,50%,90%,99% 等比例请求的平均耗时用以生成统计图表。
- metrics.rollingPercentile.timeInMilliseconds:统计响应时间百分比时的窗口大小,默认为 60000,即一分钟。
- metrics.rollingPercentile.numBuckets:统计响应时间百分比时滑动窗口要划分的桶用,默认为6,需要保持能被metrics.rollingPercentile.timeInMilliseconds 整除。
- metrics.rollingPercentile.bucketSize:统计响应时间百分比时,每个滑动窗口的桶内要保留的请求数,桶内的请求超出这个值后,会覆盖最前面保存的数据。默认值为 100,在统计响应百分比配置全为默认的情况下,每个桶的时间长度为 10s = 60000ms / 6,但这 10s 内只保留最近的 100 条请求的数据
- circuitBreaker.errorThresholdPercentage:在通过滑动窗口获取到当前时间段内 Hystrix 方法执行的失败率后,就需要根据此配置来判断是否要将熔断器打开了。 此配置项默认值是 50,即窗口时间内超过 50% 的请求失败后会打开熔断器将后续请求快速失败。
- circuitBreaker.sleepWindowInMilliseconds:熔断器打开后,所有的请求都会快速失败,但何时服务恢复正常就是下一个要面对的问题。熔断器打开时,Hystrix 会在经过一段时间后就放行一条请求,如果这条请求执行成功了,说明此时服务很可能已经恢复了正常,那么会将熔断器关闭,如果此请求执行失败,则认为服务依然不可用,熔断器继续保持打开状态。此配置项指定了熔断器打开后经过多长时间允许一次请求尝试执行,默认值是 5000。
熔断器
- circuitBreaker.enabled:是否启用熔断器,默认为 true;
- circuitBreaker.forceOpen: circuitBreaker.forceClosed:是否强制启用/关闭熔断器,保持默认值,不配置即可
- circuitBreaker.requestVolumeThreshold:启用熔断器功能窗口时间内的最小请求数。试想如果没有这么一个限制,我们配置了 50% 的请求失败会打开熔断器,窗口时间内只有 3 条请求,恰巧两条都失败了,那么熔断器就被打开了,5s 内的请求都被快速失败。此配置项的值需要根据接口的 QPS 进行计算,值太小会有误打开熔断器的可能,值太大超出了时间窗口内的总请求数,则熔断永远也不会被触发。建议设置为 QPS * 窗口秒数 * 60%
实例
在服务调用方法中调整该服务调用方法的默认超时时长:
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
或者在配置文件中修改
hystrix.commend.default.execution.isolation.thread.timeoutInMilliseconds=3000
hystrix.commend.user-service.execution.isolation.thread.timeoutInMilliseconds=5000
hystrix.commend.queryById.execution.isolation.thread.timeoutInMilliseconds=2000
default部分为更改全局的默认超时时长
user-service部分为设置访问user-service服务器的超时时长
queryById部分为设置queryById方法的超时时长
实例
熔断:当大量请求无法正常访问时(触发服务降级中的超时时长),Hystrix则认为服务有问题,此时将会关闭服务,所有访问该服务的请求全部直接返回错误,不再等待超时时长
即:触发服务降级达到熔断器的阈值则开启熔断
熔断的三种状态
Open、Half Open、Closed
即:
熔断器打开:当所有请求失败的比例到达阀值(默认为50%,请求次数不低于20次),则会出发熔断器打开状态,所有请求直接回复错误
熔断器半开:熔断器打开状态(默认5S),之后进入半开状态,放部分请求通过,测试访问是否成功,如成功则熔断器转入关闭状态,如依旧不成功则熔断器继续进入打开状态并再次进行倒计时
熔断器关闭:所有请求可以正常访问服务
配置参数
circuitBreaker.requestVolumeThreshold=20 //触发熔断的最小请求次数,默认为20
circuitBreaker.sleepWindowInMilliseconds=5000 //休眠时长,默认为5秒
circuitBreaker.errorThresholdPercentage=50 //触发熔断的失败请求最小占比,默认50%
当主类上使用@EnableCircuitBreaker注解,即熔断器方式生效
threadPoolProperties
配置方式
线程池的配置也是由HystrixProperty数组构成,配置方式与命令属性一致
配置项
- coresize:核心线程池的大小,默认值是 10,一般根据 QPS * 99% cost + redundancy count 计算得出。
- allowMaximumSizeToDivergeFromCoreSize:是否允许线程池扩展到最大线程池数量,默认为 false;
- maximumSize:线程池中线程的最大数量,默认值是 10,此配置项单独配置时并不会生效,需要启用 allowMaximumSizeToDivergeFromCoreSize 项。
- maxQueueSize:作业队列的最大值,默认值为 -1,设置为此值时,队列会使用 SynchronousQueue,此时其 size 为0,Hystrix 不会向队列内存放作业。如果此值设置为一个正的 int 型,队列会使用一个固定 size 的 LinkedBlockingQueue,此时在核心线程池内的线程都在忙碌时,会将作业暂时存放在此队列内,但超出此队列的请求依然会被拒绝。
- queueSizeRejectionThreshold:由于 maxQueueSize 值在线程池被创建后就固定了大小,如果需要动态修改队列长度的话可以设置此值,即使队列未满,队列内作业达到此值时同样会拒绝请求。此值默认是 5,所以有时候只设置了 maxQueueSize 也不会起作用。
- keepAliveTimeMinutes:由上面的 maximumSize,我们知道,线程池内核心线程数目都在忙碌,再有新的请求到达时,线程池容量可以被扩充为到最大数量,等到线程池空闲后,多于核心数量的线程还会被回收,此值指定了线程被回收前的存活时间,默认为 2,即两分钟。
实例
编写代码模拟外部接口
@HystrixCommand(commandKey = "testCoreSizeCommand",groupKey = "testGroup",fallbackMethod = "TimeOutFallBack"
,threadPoolProperties = {
@HystrixProperty(name = "coreSize",value = "2"),
@HystrixProperty(name = "allowMaximumSizeToDivergeFromCoreSize",value = "true"),
@HystrixProperty(name = "maximumSize",value = "2"),
@HystrixProperty(name = "maxQueueSize",value = "2")
},
commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy",value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "16000")
})
@Override
public String coreSizeTest() throws InterruptedException{
Thread.sleep(5000);
System.out.println("进来一次");
return "coreSizeTest finished";
}
public String TimeOutFallBack(){
return "降级sorry,the request is timeout";
}
每个请求会sleep5秒后返回
编写一个controller,调用service
@RequestMapping(value = "test_coresize",method = RequestMethod.GET)
public String getHystrixSizeTest() throws InterruptedException{
String test = exampleRemote.coreSizeTest();
return test;
}
发送请求访问localhost:8080/test_coresize
编写三个线程同时访问接口
public class MyThread extends Thread{
@Override
public void run(){
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json; charset=UTF-8");
headers.add("Accept", "*/*");
HttpEntity<String> requestEntity = new HttpEntity<>("", headers);
ResponseEntity<String> exchange = restTemplate.exchange("http://localhost:8080/test_coresize", HttpMethod.GET, requestEntity,String.class);
String body = exchange.getBody();
System.out.println("请求结果:"+body);
}
public static void main(String[] args) {
MyThread myThred1 = new MyThread();
MyThread myThred2 = new MyThread();
MyThread myThred3 = new MyThread();
myThred1.start();
myThred2.start();
myThred3.start();
}
}
根据对参数的修改分析
- 设置coresize=2,maximumSize=2,未定义降级方法
两个请求成功,第三个请求返回false,超过了最大线程数之后的请求会被hystrix降级处理,即调用hystrxi中的fallback属性指定的降级方法。如果没有指定降级方法,则会默认返回null - 设置coresize=2,maximumSize=2,定义降级方法:返回信息降级sorry, the request is timeout
打印了降级方法返回的信息 - 设置coresize=2,maximumSize=4,未定义降级方法
三个请求是一起返回,说明:请求数小于最大线程数,却大于核心线程数的时候,会一起处理所有的请求,当所有请求处理完毕的时候,会将多余核心数量的线程释放。 - 设置coresize=2,maxQueueSize=2,maximumSize=4,未定义降级方法
两个请求先返回,还有一个请求在5秒后返回
总结
-
如果请求量少,达不到 coreSize,通常会使用核心线程来执行任务。
-
如果设置了 maxQueueSize,当请求数超过了 coreSize, 通常会把请求放到 queue 里,待核心线程有空闲时消费。
-
如果 queue 长度无法存储请求,则会创建新线程执行直到达到 maximumSize 最大线程数,多出核心线程数的线程会在空闲时回收。