文章目录
1.Hystrix的一些概念
1.1服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C有调用其他的微服务,这就是所谓的"扇出"。如果删除的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,这就是所谓的"雪崩效应"。
1.2什么是Hystrix
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器"本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应,而不是长时间等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
1.3服务降级
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback,就是一个出了事儿有人兜着的兜底功能。
服务降级的几种场景:程序运行异常,超时,服务器熔断触发服务降级,线程池/信号量打满也会导致服务降级。
1.4服务熔断
相当于保险丝的功能当达到最大服务方问后直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示 。当检测到该节点为服务调用响应正常后,回复调用链路。
1.5服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
2.问题演示
设计思路写两个接口一个没有延时直接返回,一个需要耗时3s当各有一个线程分别访问这两个接口都能得到正确的结果,只不过延时3s的会稍微慢一点,不延时的立即返回结果。
@Service
public class PaymentService {
//正常访问肯定OK
public String paymentInfo_OK(Integer id){
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_OK,id:" + id;
}
//执行过程中耗时严重
public String paymentInfo_TimeOut(Integer id){
int timeNumber = 3;
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,延迟了(单位秒):" + timeNumber;
}
}
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/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("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*************result: " + result);
return result;
}
}
但是当有20000个线程呢?我们通过JMeter创造200个线程作为线程组,每隔1s访问一次,循环100次访问那个耗时的接口。观察现象
然后点击运行观察现象
首先是控制台确实输出了一大堆日志信息
然后就是最关键的东西来了我们之前访问的ok的那个接口非常丝滑没有任何阻碍直接返回非常清爽,但是这次在20000个线程访问TimeOut接口的前提下,他居然——便秘了!!!
这就相当于你在追一个女的,但是你只是她的备胎,她在养鱼,那么你费劲吧啦的跟她聊天想要得到她的回复,但是呢人家压根儿就没拿你当盘菜你在跟她聊天时她总是同时和很多人聊得火热,半天不回你,回你了也只是洗澡了睡了晚安88。
刚才只是在本地测的如果这时候再加一个消费者那么访问那个不需要延时的服务时也会出现缓慢如果你狂点还有可能报超时的错误。这是因为8001端口同一层次的其他服务被困死;因为tomcat线程池里面的工作线程挤占完毕,80消费者调用8001客户端会响应缓慢。
试想一下当你有大量线程访问一个比较耗时的服务那么其他服务一定会受到影响,如果比较重要的服务没受到影响那还行,要是受到影响了那估计就得杀一个程序员祭天了。。正因为有这些问题所以降级/容错/限流等技术诞生。
3. Hystrix的应用
3.1解决方案
对方服务8001超时了,调用者80不能一直卡死等待,必须有服务降级
对方服务8001宕机了,调用者80不能一直等待,必须有服务降级
对方服务OK,调用者(80)自己出故障或有自我要求(自我等待的时间小于服务提供者),自己处理降级。
3.2服务降级
3.2.1在pom文件中添加Hystrix的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
3.2.2 支付侧的服务降级
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})//3s内走正常的业务逻辑超过3s就认为超时了就得走下面的服务降级,业务类引用
public String paymentInfo_TimeOut(Integer id){
int timeNumber = 5;
//int age = 10/0模拟一个报错的例子
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,延迟了(单位秒):" + timeNumber;
}
public String paymentInfo_TimeOutHandler(Integer id){
return "线程池:"+Thread.currentThread().getName()+"系统繁忙或运行报错" + id+"\t + o(╥﹏╥)o";
}
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//主启动类激活
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
首先在业务类上添加@HystrixCommand配置好服务降级的条件和服务降级具体的方法,一旦满足这个条件就会自动调用这个方法。在主启动类上添加@EnableCircuitBreaker主启动类进行激活。我们的延时方法是5s但是我们3s就让他服务降级来模拟超时场景。我们也可以在方法中加入一些异常代码来模拟报错。
访问localhost:8001/payment/hystrix/timeout/7,得到结果都应该是服务降级的结果
3.2.3订单侧的服务降级
首先修改yml文件在yml文件加上这么一段开启hystrix
feign:
hystrix:
enabled: true
主启动类加上@EnableHystrix
业务类
这里修改一下服务提供者的代码把延时改成3s,然后支付测超过5s才报超时异常。这里订单侧超过1.5s没响应就报超时异常服务降级。
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutFallbackMethod",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentInfo_TimeOutFallbackMethod(@PathVariable("id") Integer id){
return "我是消费者80,对方支付系统繁忙请10s后再试或者自己看看自己有啥毛病没";
}
设计思路及预期结果:我把订单侧超时时间设置成1.5s,而实际延时操作要用3s,所以一定会调用这个兜底的服务降级的方法。
结果展示
3.2.4全局服务降级
如果每一个需要服务降级的方法下面都写一个兜底的方法那就太麻烦了,而且你把兜底的与业务无关的代码写在业务代码那块他也不得劲啊。
我们可以在controller类上加一个@DefaultProperties(defaultFallback = “payment_Global_FallbackMethod”),defaultFallback是默认的兜底方法除非是一些特殊情况否则大部分都可以通过这个兜底来解决,从而减少了代码膨胀。
//全局的fallback
public String payment_Global_FallbackMethod(){
return "Global异常信息处理,请稍后重试";
}
在这个方法里故意写一个10/0的异常用来触发这个兜底方法。最应该执行全局fallback返回那句话。
3.2.5通配服务降级
全局服务降级虽然解决了代码膨胀的问题,但是业务代码与非业务代码耦合度高的问题仍然没能得到解决,我们可以抽出一个通配降级实现类,这个类实现业务接口然后再写具体的降级逻辑。这样就将业务代码与非业务代码进行了解耦。同时之前只演示了超时和运行时异常这次看一下当服务提供者宕机,客户端配置服务降级,当服务提供者宕机调用兜底方法。
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "----------PaymentHystrixService fall back-paymentInfo_OK,o(╥﹏╥)o";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "----------PaymentHystrixService fall back-paymentInfo_TimeOut,o(╥﹏╥)o";
}
}
创建通配降级实现类实现PaymentHystrixService接口实现方法写对应的兜底方法逻辑,同时在接口上将这个类配置为服务降级的实现类
@FeignClient这个注解后面的两个参数分别是要找CLOUD-PROVIDER-HYSTRIX-PAYMENT这个服务,然后服务降级调用的是PaymentFallbackService里面实现的方法。
访问ok这个接口应该得到的是正确的结果
当我将服务提供者8001宕机再次访问这个接口,因为我之前配置了服务降级的方法所以这个应该调用fallback当中的方法。
3.3服务熔断
3.3.1服务熔断的流程:
一开始断路器是Closed状态如果请求成功那么断路器还是关,如果失败了但是在我规定的阈值之下意味着没有发生熔断,断路器仍然关,一旦我的请求超过特定阈值或者失败次数达到一定比例断路器打开服务熔断短期请求全部无法使用,一段时间之后在时间窗口期内试着能不能让一些请求通过即断路器处于半开状态如果可以通过那么断路器就变成了Closed状态,如果失败则断路器重回Open状态。
在支付侧的service,PaymentService中配置服务熔断,将是否开启断路器,请求次数,时间窗口期,失败率这些都规定好。一旦是负数抛出异常,成功则返回流水号。
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),/* 是否开启断路器*/
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求总数阈值
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 快照时间窗口
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 错误百分比阈值
})
public String paymentCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
/**
* 当断路器开启时,主逻辑熔断降级,该 fallback 方法就会替换原 paymentCircuitBreaker 方法,处理请求
*
* @param id
* @return
*/
public String paymentCircuitBreakerFallback(Integer id) {
return Thread.currentThread().getName() + "\t" + "id 不能负数或超时或自身错误,请稍后再试,/(ㄒoㄒ)/~~ id: " + id;
}
3.3.2注解参数解释:
1.快照时间窗:断路器是否打开需要统计一些请求和错误数据,而统计时间范围就是快照时间窗,默认最近的10s。
2.请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断,默认值为20意味着在10s内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3.错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。
然后在controller里调用测试方法看看什么结果。
再传一个负数
这时候我狂点-7,频繁访问异常请求,这时候失败率非常高断路器打开即使我这次传入的是正确的由于断路器打开他仍然熔断降级,过一会才能自动恢复。
我多点几次正数的他慢慢自己就恢复了
3.3.3断路器开启或关闭的条件:
1.当满足一定阈值的时候(默认10s内超过20个请求)
2.当失败率达到一定的时候(默认10s内超过50%的请求失败)
3.当达到上述阈值,断路器将会开启
4.当开启的时候所有请求都不会进行转发
5.一段时间之后(默认是5s),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器将会关闭,若失败,继续开启,重复4和5。
3.3.4断路器打开以及恢复
再有请求调用的时候将不会调用主逻辑,而是直接调用降级fallback,通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
3.4服务监控hystrixDashborad
除了隔离以来服务的调用以外,Hystrix还提供了准实时的调用监控,Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。
启动类要加上这个注解@EnableHystrixDashborad注解,然后application.yml中配置端口号为9001。
然后访问(http://localhost:9001/hystrix)这个网址得到如下结果
在被监控的服务下加上这个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在8001的主启动类加上如下配置(Springcloud版本问题)
/**
* 注意:新版本Hystrix需要在主启动类中指定监控路径
* 此配置是为了服务监控而配置,与服务容错本身无关,spring cloud升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
*
* @return ServletRegistrationBean
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
// 一启动就加载
registrationBean.setLoadOnStartup(1);
// 添加url
registrationBean.addUrlMappings("/hystrix.stream");
// 设置名称
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
在Dashboard的路径下加上http://localhost:8001/hystrix.stream然后点击Monitor Stream即可进入监控界面
按照颜色从左到右依次是成功,熔断,错误请求,超时,线程池拒绝访问,失败,10s内错误百分比。
你多点几下成功的就会得到
曲线:用来记录两分钟内流量的相对变化,可以通过它来观察到流量的上升和下降的趋势。
实心圆:首先它通过颜色的变化代表了实例的健康程度,它的健康程度从绿色<黄色<橙色<红色递减。其次它的大小也会根据实际请求的流量发生变化,流量越大实心圆就越大。所以通过该实心圆的展示,就能在大量的实例中快速的发现故障实例和高压力实例。
3.5Hystrix工作流程
(官网上的)How it Works · Netflix/Hystrix Wiki (github.com)
第一步:创建HystrixCommand(用在依赖的服务返回单个操作结果的时候)或HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候)对象。
第二步:命令执行,其中HystrixCommand实现了下面前两种执行方式;而HystrixObserableCommand实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一结果的对象,或是在发生错误的时候抛出异常。queue():异步执行,直接返回一个Future对象,其中包含了服务执行结果结束时要返回的单一结果对象。observe():返回Observable对象,它代表了一个操作的多个结果,它是一个Hot Observable(不论"事件源"是否有"订阅者",都会在创建后对事件进行发布,所以对于Hot Observable的每一个"订阅者"都有可能是从"事件源"的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable():同样会返回Observable对象,也代表了操作的多个结果,但是它返回的是一个Cold Observable(没有"订阅者"的时候并不会发布事件,而是进行等待,直到有"订阅者",之后才发布事件,所以对Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程)。
第三步:若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
第四步:检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步);如果断路器是关闭的,检查是否有可用的资源来执行命令(第5步)。
第五步:线程池/请求队列/信号量是否占满,如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(第8步)。
第六步:Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务,HystrixCommand.run():返回单一的结果或抛出异常,HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
第七步:Hystrix会将"成功",“失败”,“拒绝”,“超时"等信息报告给断路器,而断路器会维护一组计数器来统计这些数据,断路器会使用这些统计数据来决定是否将断路器打开,来对某个依赖服务的请求进行"熔断/短路”。
第八步:当命令执行失败的时候,Hystrix会进入fallback尝试回退处理(服务降级).而能够引起服务降级处理的情况有下面几种:第四步当前命令处于"熔断/短路"的状态。断路器打开的时候。第五步:当前命令的线程池、请求队列或者信号量被占满的时候。第六步:HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。
第九步:当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回。
参考尚硅谷