文章目录
系列链接:
- GitHub:源码
- SpringCloud(1)–入门、版本、环境搭建
- SpringCloud(2)–服务注册与发现(Eureka、Zookeeper、Consul)
- SpringCloud(3)–服务调用(Ribbon、OpenFeign)
- SpringCloud(4)–服务降级(Hystrix、降级、熔断、监控)
- SpringCloud(5)–服务网关(GateWay)
- …
服务降级
首先需要了解一个概念,服务雪崩:
多个微服务之间调用时,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的”扇出“。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而导致系统”雪崩效应“。
Hystrix
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,它能够保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障(雪崩)。
主要作用
- 服务降级:比如当某个服务繁忙,不能让客户端的请求一直等待,应该立刻返回给客户端一个备选方案。
- 服务熔断:当某个服务出现问题,卡死了,不能让用户一直等待,需要关闭所有对此服务的访问然后调用服务降级。
- 服务限流:比如秒杀场景,不能访问用户瞬间都访问服务器,限制一次只可以有多少请求。
- 接近实时的监控
Hystrix
服务降级
比如当某个服务繁忙,不能让客户端的请求一直等待,应该立刻返回给客户端一个备选方案。
出现降级的情况:
- 程序运行异常
- 超时
- 发生服务熔断
- 线程池/信号量打满
创建带降级机制的pay模块
-
名称: cloud-provider-hystrix-payment8001
-
pom文件
<!-- hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
配置文件,为了减少资源占用,这里使用单机版 Eureka
server: port: 8001 spring: application: name: cloud-provider-hystrix-payment eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka7001.com:7001/eureka # defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
-
主启动类
@SpringBootApplication @EnableEurekaClient public class PaymentHystrixMain8001 { public static void main(String[] args){ SpringApplication.run(PaymentHystrixMain8001.class,args); } }
-
service,节约时间不写接口
@Service public class PaymentService { /* 正确的方法 */ public String paymentInfo_OK(Integer id){ return "线程池:"+Thread.currentThread().getName()+" paymentInfo_OK,id:"+id+"\t"+"???"; } /* 会超时报错的方法 */ public String paymentInfo_TimeOut(Integer id){ int timeNumber = 5; try{ TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id:"+id+"\t"+"!!!"; } }
-
controller
@RestController @Slf4j @RequestMapping("/payment") public class PaymentController { @Resource private PaymentService paymentService; @Value("${server.port}") private String serverPort; @GetMapping("/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id){ String result = paymentService.paymentInfo_OK(id); log.info("****result:"+result); return result; } @GetMapping("/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id){ String result = paymentService.paymentInfo_TimeOut(id); log.info("****result:"+result); return result; } }
-
启动项目,并发度低的情况下可以使用,我们使用 JMeter 并发2w个请求TimeOut接口,然后访问OK接口会发现也需要进行等待。这就是因为被压测的方法它占用了服务器大部分资源, 导致其他请求也变慢了。
创建带降级的order模块
-
名字: cloud-consumer-feign-hystrix-order80
-
pom
<!-- openfeign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- hystrix--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
配置文件
server: port: 80 eureka: client: register-with-eureka: false service-url: defaultZone: http://eureka7001.com:7001/eureka
-
主启动类
@SpringBootApplication @EnableFeignClients public class OrderHystrixMain80 { public static void main(String[] args){ SpringApplication.run(OrderHystrixMain80.class,args); } }
-
远程调用pay模块的接口
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT") public interface PaymentHystrixService { @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id); }
-
controller:
@RestController @Slf4j @RequestMapping("/consumer") public class OrderHystrixController { @Resource private PaymentHystrixService paymentHystrixService; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id){ String result = paymentHystrixService.paymentInfo_OK(id); return result; } @GetMapping("/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id){ String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; } }
-
测试,启动order模块,再次压测2万并发pay模块的timeout接口,发现order访问也变慢了,没有配置Ribbon超时参数还会调用报错。
原因
出现这种现象的原因是,8001的tomcat容器中线程池的工作线程被timeout接口挤占完毕,80此时调用8001中的ok接口就无法得到处理。
解决
- 8001超时/宕机:80不能一直等待,需要进行服务降级
- 8001没问题:80自己出故障或需要自己的等待时间小于服务提供者,则需要自己处理降级
配置服务降级
修改pay模块
-
为service会延迟的方法添加 @HystrixCommand 注解
/* 会超时报错的方法 */ @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000") })//设置调用超时时间的峰值,超时则会调用设置的paymentInfo_TimeOutHandler方法 public String paymentInfo_TimeOut(Integer id){ int timeNumber = 5; //int age = 10/0; //其他异常也会触发服务降级 try{ TimeUnit.SECONDS.sleep(timeNumber); } catch (InterruptedException e) { e.printStackTrace(); } return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id:"+id+"\t"+"!!!"; } public String paymentInfo_TimeOutHandler(Integer id){ return "线程池:"+Thread.currentThread().getName()+" 系统繁忙,请稍后再试,id:"+id+"\t"+"。。。"; }
-
主启动类上,添加激活hystrix的注解@EnableCircuitBreaker
@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class PaymentHystrixMain8001 { public static void main(String[] args){ SpringApplication.run(PaymentHystrixMain8001.class,args); } }
-
访问8001的timeout接口,可以看到触发了降级
修改order模块
一般服务降级,都是放在客户端。此外,对于@HystrixCommand内的修改,建议重启微服务。
-
修改配置文件,feign开启hystrix支持
feign: hystrix: enabled: true
-
主启动类添加@EnableHystrix,启用hystrix
@SpringBootApplication @EnableFeignClients @EnableHystrix public class OrderHystrixMain80 { public static void main(String[] args){ SpringApplication.run(OrderHystrixMain80.class,args); } }
-
修改controller【pay模块timeout改为3s,限制5s】
@GetMapping("/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",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_TimeOutHandler(Integer id){ return "本80系统繁忙,请稍后再试,id:"+id+"\t"+"。。。"; }
-
测试,order访问pay需要3s,而自己只运行1.5s,所以会在order中降级
-
目前两个降级模块出现的问题
- 降级方法与业务方法写在了一块,耦合度高
- 每个业务方法都写了一个降级方法,重复代码多
重复代码的问题
解决:
配置一个全局的降级方法,所有方法都可以走这个降级方法,至于某些特殊创建,再单独创建方法。
-
创建一个全局方法
//全局降级方法 public String paymentInfo_Global_FallbackMethod(){ return "Global异常处理信息,请稍后再试,。。。"; }
-
使用@DefaultProperties注解指定其为全局降级方法(默认降级方法)
@RestController @Slf4j @RequestMapping("/consumer") @DefaultProperties(defaultFallback = "paymentInfo_Global_FallbackMethod") public class OrderHystrixController { ... }
-
业务方法不指定具体降级方法,就会使用默认降级方法
@HystrixCommand public String paymentInfo_TimeOut(@PathVariable("id") Integer id){ String result = paymentHystrixService.paymentInfo_TimeOut(id); return result; }
-
测试
代码耦合度的问题
解决:
修改order模块,这里开始,pay模块就不服务降级了,服务降级写在order模块即可。
-
PaymentHystrixService接口是远程调用pay模块的,我们这里创建一个类实现改接口进行统一降级处理。
@Component public class PaymentFallbackService implements PaymentHystrixService{ @Override public String paymentInfo_OK(@PathVariable("id") Integer id) { return "----PaymentFallbackService fall back-paymentInfo_OK,..."; } @Override public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { return "----PaymentFallbackService fall back-paymentInfo_TimeOut,..."; } }
-
确保配置文件中开启了feign的hystrix支持,最后让PaymentHystrixService的实现类生效,修改接口的@FeignClient注解
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
-
启动测试,启动order和pay正常访问OK接口
如果将pay服务关闭,order再次访问OK接口(没有其他降级方法)
解决耦合后又出现的问题:
这样虽然解决了代码耦合度问题,但是又出现了过多重复代码的问题,每个方法都有一个降级方法
服务熔断
当某个服务出现问题:
-
需要关闭所有对此服务的访问【断路器】,然后调用服务降级。
-
当检测到该服务响应正常后,恢复调用链路。
继续修改pay模块
-
修改Payservice接口,添加服务熔断相关的方法
//服务熔断 @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = { @HystrixProperty(name = "circuitBreaker.enable",value = "true"),//是否启用断路器 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),//请求次数 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"),//时间窗口期 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")//失败率达到多少后跳闸 }) public String paymentCircuitBreaker(@PathVariable("id") Integer id){ if(id<0) throw new RuntimeException("****id 不能为负数"); String serialNumber = IdUtil.simpleUUID();//hutool工具 return Thread.currentThread().getName()+"\t"+"调用成功,流水号:"+serialNumber; } public String paymentCircuitBreaker_fallback(Integer id){ return "id:"+id+",不能为负数,请稍后再试,。。。"; }
配置服务降级方法就不说了,对于配置的参数表示:
- 如果并发超过10个或者10个并发中失败了6个,就会开启断路器
- 在时间窗口期10秒之内会尝试请求,如果请求成功就会关闭断路器
-
修改controller,添加一个测试方法
//服务熔断 @GetMapping("/circuit/{id}") public String paymentCircuitBreaker(@PathVariable("id") Integer id){ String result = paymentService.paymentCircuitBreaker(id); log.info("****result:"+result); return result; }
-
测试:
-
多次访问,并且错误率超过60%:
-
此时服务熔断,此时即使访问正确的也会报错:
-
等待窗口期过了,取消熔断
-
总结补充
Hystrix所有可配置的属性
此外,所有的参数配置可以查看HystrixCommandProperties类当中的成员变量。
熔断整体流程
-
请求进来,首先查询缓存,如果缓存有,直接返回;如果缓存没有,则到2.
-
查看断路器是否开启,如果开启的,Hystrix直接将请求转发到降级返回,然后返回;如果断路器是关闭的,则到3。
-
判断线程池等资源是否已经满了,如果已经满了也会走降级方法,如果资源没有满,则到4。
-
判断我们使用的什么类型的Hystrix,决定调用构造方法还是run方法,然后处理请求
-
然后Hystrix将本次请求的结果信息汇报给断路器,断路器收到信息,判断是否符合开启或关闭断路器的条件。
-
如果本次请求处理失败,又会进入降级方法;如果处理成功,判断处理是否超时,如果超时了也进入降级方法。没有超时,则本次请求处理成功,将结果返回给controller。
服务监控
HystrixDashboard
HystrixDashboard是Hystrix提供的准实时调用监控,它记录了所有Hystrix发起的请求的执行信息,并以图形化的形式显示。
HystrixDashboard的使用
-
创建项目,名称为cloud-consumer-hystrix-dashboard9001
-
pom文件
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency>
-
配置文件
server: port: 9001
-
主启动类
@SpringBootApplication @EnableHystrixDashboard public class HystrixDashboarMain9001 { public static void main(String[] args){ SpringApplication.run(HystrixDashboarMain9001.class,args); } }
-
修改所有pay模块(8001,8002,8003…)添加一个pom依赖,我们之前都配过了
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
启动9001即可,访问: http://localhost:9001/hystrix
-
此时仅仅是可以访问HystrixDashboard,并不代表已经监控了8001,8002。如果要监控,还需要配置(8001为例):
-
8001的主启动类添加
/** * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 * ServletRegistrationBean因为SpringBoot的默认路径不是 “/hystrix.stream" * 只要在自己的项目里配置上下的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet() ; ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; }
-
-
到此,可以启动服务启动7001,8001,9001,然后在web界面,指定9001要监控8001: