hystrix
简介
hystrix
是 netlifx
开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控 hystrix dashboard
等功能,从而提升系统的可用性和容错性
官方 github
:https://github.com/Netflix/Hystrix/wiki/How-To-Use
hystrix
容错机制的特点
要防止雪崩效应,必须有一个强大的容错机制。该容错机制需实现以下两点:
- 包裹请求:使用
HystrixCommand
包裹对依赖的调用逻辑,每个命令在独立线程中执行 - 断路器(跳闸)机制:当某服务的错误率超过一定阈值时,
hystrix
可以自动或者手动跳闸,停止请求该服务一段时间 - 资源隔离:
hystrix
为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定 - 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑
- 自我修复:断路器打开一段时间后,会自动进入半开状态
hystrix
的断路器工作原理
- 当请求达到阈值时候:默认
10
秒内20
次请求,当请求失败率达到50%
时,此时断路器将会开启,所有的请求都不会执行 - 断路器开启
5
秒(默认)时,这时断路器是半开状态, 会允许其中一个请求执行 - 如果执行成功,则断路器会关闭;如果失败,则继续开启。循环重复这两个流程
hystrix
实现服务的隔离、熔断、降级
继续使用 上一篇 文章中的项目,稍加改造即可实现 hystrix
功能
Maven
依赖
eureka-client-consumer
消费方项目添加如下依赖
<!--springcloud hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.properties
配置文件
server.port=8090
#注册进eureka的名称
spring.application.name=eureka-client-consumer
eureka.client.service-url.defaultZone=http://eureka7001:8761/eureka/
eureka.instance.prefer-ip-address=true
#Feign默认整合了Hystrix,要想为Feign打开Hystrix支持,需要此项设置
#在springcloud Dalston之前的版本中,Feign默认开启Hystrix支持,无需设置feign.hystrix.enabled=true
#从springcloud Dalston版本开始,Feign的Hystrix支持默认关闭,需要手动设置开启
feign.hystrix.enabled=true
#配置ribbon的超时时间,默认二者都是1000
#ribbon.ConnectTimeout=2000
#ribbon.ReadTimeout=2000
#第一次启动项目时,请求接口查询数据库有点耗时,会进入降级策略,所以将hystrix的超时时间设置为3s,默认是1s,这是全局设置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
服务消费方主启动类
服务提供方的保持不变,消费方主启动类一定要标注注解 @EnableHystrix
@EnableHystrix
@EnableFeignClients
@EnableEurekaClient
@Slf4j
@SpringBootApplication
public class AppConsumer {
public static void main(String[] args) {
SpringApplication.run(AppConsumer.class, args);
log.info("------AppConsumer Running------");
}
}
服务降级
在 feign
整合 hystrix
的情况下,服务降级的实现主要有以下 3
种方式。主要变动的代码都在消费方。当服务调用失败(程序异常,线程池 / 信号量已满,服务下线)或超时了,再或者服务熔断触发了服务降级,这些情况就让其触发服务降级,从而保证了系统的可用性可靠性,防止了系统的整体缓慢甚至崩溃
方式一
- 使用
feign
的注解@FeignClient
的属性fallback
指定的降级回退方法 - 如果业务层抛出异常,这种方式对异常处理不友好,会直接将异常抛出给用户,除非主动处理这个异常
@FeignClient(name = "eureka-client-producer", fallback = UserConsumerFeignFallback.class)
public interface UserConsumerFeign {
@GetMapping(path = "/user/selectUserById")
ResultVo selectUserById(@RequestParam(name = "id") Integer id);
}
@Component
public class UserConsumerFeignFallback implements UserConsumerFeign {
@Override
public ResultVo selectUserById(Integer id) {
ResultVo resultVo = new ResultVo();
resultVo.setId(id);
resultVo.setUsername("恭喜你已进入UserConsumerFeignFallback类所在的服务降级区域");
resultVo.setNickname("恭喜你已进入UserConsumerFeignFallback类所在的服务降级区域");
return resultVo;
}
}
方式二
- 使用
hystrix
提供的注解@HystrixCommand
的属性fallbackMethod
来指定降级回退方法。但这种方式业务逻辑与降级回退方法耦合在一起,不好进行统一管理 - 如果业务层抛出异常,这种方式对异常处理比较友好,会直接走降级回退方法,并不会直接显式抛出异常来
@Slf4j
@Service
public class UserConsumerServiceImpl implements UserConsumerService {
@Autowired
private UserConsumerFeign userConsumerFeign;
/**
* feign 进行远程调用
*/
@HystrixCommand(fallbackMethod = "queryOneByIdFallback")
@Override
public ResultVo queryOneById(Integer id) {
ResultVo resultVo = userConsumerFeign.selectUserById(id);
log.info("resultVo为:" + resultVo.toString());
log.info("调用服务提供方的端口为:" + resultVo.getPort());
return resultVo;
}
public ResultVo queryOneByIdFallback(Integer id) {
ResultVo resultVo = new ResultVo();
resultVo.setId(id);
resultVo.setUsername("恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
resultVo.setNickname("恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
return resultVo;
}
}
方式三
每一个方法独立配置降级的话会造成代码冗余,加大了工作量。此时可以使用注解 @DefaultProperties
的 defaultFallback
属性来指定类的全局降级回退方法
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallackMethod")
public class PaymentController {
@Resource
private PaymentHystrixService paymentService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_OK(id);
log.info("*******result:" + result);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") //3秒钟以内就是正常的业务逻辑
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
//兜底方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对付支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,(┬_┬)";
}
/**
* 全局fallback处理方法
*/
public String payment_Global_FallackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对付支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,(┬_┬)";
}
}
服务降级测试
这里测试以服务下线(肯定调用失败)和服务调用超时两种情况来测试服务降级。服务降级的实现方式选择第一种或第二种都可以
服务下线
启动 eureka-server-one,eureka-client-consumer
两个项目,eureka-client-consumer
的 controller
层代码未改动。注意没有启动 eureka-client-producer
服务提供方项目,postman
测试如下
在这里我选择以一种方式,所以结果如上
服务调用超时
服务提供方 controller
代码改动
改动 eureka-client-producer
服务提供方的 controller
层代码,如下
@Slf4j
@Controller
@RequestMapping(path = "/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(path = "/selectUserById")
@ResponseBody
public ResultVo selectUserById(Integer id) {
try {
// 产生3~10的随机数
Random random = new Random();
int i = random.nextInt(10) + 3;
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userService.selectOne(id);
}
}
然后再分别启动 eureka-server-one,eureka-client-producer,eureka-client-consumer
这三个项目,postman
测试结果如下
可以看到服务调用超时的时候,进入服务降级了
如何判断调用超时?
默认超时时间?
针对这个问题,就要查看 hystrix
源码了
- 它默认是
1000 ms
即1 s
,而在服务提供方却延时了3~10 s
,肯定超时 - 另外,还可以查看
feign
的超时连接时间默认是2 s
,如下
hystrix
的超时时间配置
全局配置
如果只是想全局的设置,可以在配置文件中配置如下,默认是 1000 ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
使用 @HystrixCommand
如果使用的是 @HystrixCommand
注解,那么可以在注解中直接指定超时时间,如下
@HystrixCommand(fallbackMethod="fallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000" )
}
)
@Override
public String firstLoginTimeOut(Integer id) {
return userFeign.getCouponTimeOut(id);
}
服务熔断
熔断与降级
- 熔断的核心是断路器(跳闸),没有断路器的熔断那就不是熔断了
- 熔断也会触发降级回退方法的
服务熔断测试
服务提供方 controller
代码改动
改动 eureka-client-producer
服务提供方的 controller
层代码,即恢复原样,如下
@Slf4j
@Controller
@RequestMapping(path = "/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping(path = "/selectUserById")
@ResponseBody
public ResultVo selectUserById(Integer id) {
return userService.selectOne(id);
}
}
服务消费方 service
层代码改动
为了演示服务熔断效果,改动如下
@Slf4j
@Service
public class UserConsumerServiceImpl implements UserConsumerService {
@Autowired
private UserConsumerFeign userConsumerFeign;
/**
* 在 10s 统计窗口时间之内,有 10 次请求,请求失败率达到 50% 的时候,此时断路器将会开启,
* 并在 5s 后断路器开始尝试恢复正常,如果没有错误,就完全恢复
*/
@HystrixCommand(fallbackMethod = "queryOneByIdFallback", commandProperties = {
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),// 统计窗口时间,默认10s
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 断路器熔多断的最小请求数,默认20
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),// 断路器打开后,多久以后开始尝试恢复,默认5s
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")// 请求失败率,默认50%
})
@Override
public ResultVo queryOneById(Integer id) {
if (id < 0) {
throw new RuntimeException("id 不能为负数");
}
ResultVo resultVo = userConsumerFeign.selectUserById(id);
log.info("resultVo为:" + resultVo.toString());
log.info("调用服务提供方的端口为:" + resultVo.getPort());
return resultVo;
}
public ResultVo queryOneByIdFallback(Integer id) {
ResultVo resultVo = new ResultVo();
resultVo.setId(id);
resultVo.setUsername("id 不能为负数,恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
resultVo.setNickname("id 不能为负数,恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
return resultVo;
}
}
测试一
测试在 10s
窗口时间之内,我们发送 10
次请求,请求失败率达到 50%
时,让断路器打开,postman
测试如下
传入参数 -1
,此时断路器已经打开。此时在 5s
时间之内,再传入参数 1
,发送请求(5s
后断路器开始尝试恢复正常)。看其效果,如下
可以看到,即使发送请求传入的参数是 1
,结果依旧是熔断状态,因为断路器打开后,在设置的时间内(我们设置 5s
),所有的请求都不会执行,直到过了这个设置的时间内之后,这时断路器是半开状态的。我们再次发送请求传入的参数是 1
,结果如下
此时发送请求传入的参数是 1
,结果正常,此时的断路器就会关闭了
服务隔离
hystrix
进行隔离,有线程池隔离和信号量两种方式
信号量隔离
信号量隔离,就是一个计数器,显示服务的请求数量,起到了限流的作用
线程池隔离
hystrix
的线程池隔离:接口的请求在来到hystrix
之前还要经过controller、service
等,真正被hystrix
接收到后,hystrix
才会创建线程池,把请求放到新的线程中,请求下游的服务- 这个过程中,
hystrix
就可以控制等待超时、失败请求统计等操作。所以这个线程池的大小就决定了对下游服务的并发请求量,实际上也是在这里起到了对下游服务的一个保护。重点就是对下游服务的保护 - 对自己所在的服务是否有保护呢,如果我们的
hystrix
超时时间(execution.isolation.thread.timeoutInMilliseconds
)设置的非常长,那么当下游服务响应慢或无响应时,hystrix
所在的服务也会长时间挂起,这样tomcat
的线程池也就很快会耗尽,失去保护作用。所以我们的hystrix
超时时间,失败统计次数,失败比例等参数设置的合理才能起到对自己的保护作用,也就是对下游响应慢或无响应的服务,能够快速熔断,进行降级,返回降级结果,释放宝贵的tomcat
线程资源
线程池隔离配置示例
@Service
public class UserServiceImpl implements UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserFeign userFeign;
/**
* threadPoolKey:1:如果设置的groupKey值已经存在,他会使用这个groupKey值。
* 这种情况下:同一个groupKey下的依赖调用共用同一个线程池
* 2:如果groupKey值不存在,则会对于这个groupKey新建一个线程池
*
* 1:threadPoolKey的默认值是groupKey,而groupKey默认值是@HystrixCommand标注的方法所在类名
* 2:可以通过在类上加@DefaultProperties(threadPoolKey="xxx")设置默认的threadPoolKey
* 3;可以通过@HystrixCommand(threadPoolKey="xxx")指定当前HystrixCommand实例的threadPoolKey
* 4:threadPoolKey用于从线程池缓存中获取线程池和初始化创建线程池,由于默认以groupKey即类名为threadPoolKey,那么默认所有在一个类中的HystrixCommand共用一个线程池
* 5:动态配置线程池 --
* 可以通过hystrix.command.HystrixCommandKey.threadPoolKeyOverride=线程池key动态设置
* threadPoolKey,对应的HystrixCommand所使用的线程池也会重新创建,还可以继续通过
* hystrix.threadpool.HystrixThreadPoolKey.coreSize=n和hystrix.threadpool.HystrixThreadPoolKey.maximumSize=n动态设置线程池大小
* 6:commandKey的默认值是@HystrixCommand标注的方法名,即每个方法会被当做一个HystrixCommand
*/
@HystrixCommand(threadPoolKey = "time", threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2"),
@HystrixProperty(name = "maxQueueSize", value = "20")}, commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "9000")})
@Override
public String timeOut(Integer mills) {
log.info("-----------mills:的值为:" + mills + "--------------");
return userFeign.timeOut(mills);
}
/**
* 一个类中的HystrixCommand使用两个线程池,进行线程隔离【和上面的方法隔离】
*/
@HystrixCommand(threadPoolKey = "time_1", threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2"),
@HystrixProperty(name = "maxQueueSize", value = "20")}, commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "9000")})
@Override
public String timeOut_1(Integer mills) {
log.info("-----------mills:的值为:" + mills + "--------------");
return userFeign.timeOut(mills);
}
}
学习文档
- 降级,熔断,隔离中的注解
@HystrixCommand
的属性详解:https://blog.csdn.net/zps925458125/article/details/112292971 feign,hystrix,ribbon
的超时时间配置详解:https://blog.csdn.net/hhj13978064496/article/details/104653297