扇出
多个微服务之间调用的时候,假如微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的"扇出"。
雪崩效应
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩
Hystrix
Hystrix是一个用于处理分布式系统的延迟和容错的开源库
在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
向调用方返回一个符合预期的、可处理的备选响应(Fallback),而不是长时间的等待或者抛出调用方无法处理的异常。
遗憾的是,Hystrix官宣,停更进维
服务降级就是不让客户端等待并返回一个友好提示,比如:服务器忙,请稍候再试
触发降级最常见的情况
- 程序运行异常
- 超时自动降级
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
- 人工降级
搭建好我们的工程,cloud-provider-hystrix-payment8001,
引入依赖:当然还有很多别的启动器与依赖。。有点多就不写了
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
编写配置文件
server:
port: 8001
spring:
application:
name: cloud-hystrix-payment-service #服务名
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka/
主启动类:
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
再写一堆乱七八糟的service层和controller层后,
再创建工程cloud-consumer-feign-hystrix-order80
又是索然无味的那三步骤,导入依赖,编写配置,创建主启动类(真给我写烦了)
引入依赖
org.springframework.cloud spring-cloud-starter-netflix-hystrix 编写配置文件
server: port: 80 spring: application: name: cloud-consumer-hystrix-order-service eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka/ ribbon: ReadTimeout: 6000 #读取 ConnectTimeout: 6000 #连接
主启动类
@SpringBootApplication @EnableEurekaClient @EnableFeignClients public class OrderHystrixMain80 { public static void main(String[] args) { SpringApplication.run(OrderHystrixMain80.class,args); } }
因为我们在服务端的方法做了手脚,所以消费者访问的时候是要等5秒的,又因为在消费者里配置了feign等待6s,所以此时是能在5秒后看到页面效果的。
public String payment_Timeout(Integer id){ int timeNumber = 5; try { TimeUnit.SECONDS.sleep(timeNumber); }catch (Exception e) {e.printStackTrace();} return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id: "+id+"\t"+"呜呜呜"+" 耗时(秒)"+timeNumber; }
那么当并发量高的时候,还有这样的效果吗?
下载地址:Index of /dist/jmeter/binaries
当然,我是有老师直接给的工具的,双击那个jar包或者 在小黑窗里 java -jar jar包路径运行。(swing做界面)
开启Jmeter
1.添加线程组
2.来20000个并发压死8001
3. 添加http请求,20000个请求都去访问paymentInfo_TimeOut服务
先在浏览器测试 这是可以访问成功的 http://localhost:8001/payment/hystrix/timeout/1
4、保存测试计划,下次启动的时候也能用
现在我们在保证访问http://localhost/consumer/payment/hystrix/timeout/2
可以在5秒后成功访问到
开启并发压力测试,
再在浏览器访问相同的http://localhost/consumer/payment/hystrix/timeout/2
结果就访问不了了,不愧是高并发压力啊
为什么会打圈呢,最终导致消费端超时报错
tomcat的默认的工作线程数被打满了,没有多余的线程来分解压力和处理。
服务降级处理
解决的思路
超时不再等待,宕机或程序运行出错要有兜底
什么时候降级
- 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级
8001提供端服务降级
@HystrixCommand
提供端设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback。
我们让线程睡眠15秒,这样就达到了降级的要求,超时和算数异常,都会走兜底方法payment_TimeoutHandler——服务降级
//fallbackMethod 降级方法的名字
//timeoutInMilliseconds 当前方法最大的等待时间(超过5秒立刻降级)
@HystrixCommand(fallbackMethod = "payment_TimeoutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000") //5秒钟以内就是正常的业务逻辑
})
public String payment_Timeout(Integer id){
//int timeNumber = 5;
int timeNumber = 15; //模拟非正常情况
//int i = 1/0 ; //模拟非正常情况
try { TimeUnit.SECONDS.sleep(timeNumber); }catch (Exception e) {e.printStackTrace();}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id: "+id+"\t"+"呜呜呜"+" 耗时(秒)"+timeNumber;
}
//兜底方法,上面方法出问题,我来处理,返回一个出错信息
//注意1、返回值类型与目标方法一致
//2.让方法参数与目标方法一致(我是上面方法的备胎)
public String payment_TimeoutHandler(Integer id) {
return "线程池:"+Thread.currentThread().getName()+" payment_TimeoutHandler,系统繁忙,请稍后再试\t o(╥﹏╥)o ";
}
测试:http://localhost/consumer/payment/hystrix/timeout/2
此时浏览器页面返回的就是我们自定义的错误页面了,越来越有那个味道了,嗯~~~
80消费端降级处理
服务降级可以在服务提供者侧,也可以在服务消费者侧。更多是在服务消费者侧
下面配置是默认开启的,所以不用配置,知道有就好了
feign:
hystrix:
enabled: true #如果处理自身的容错就开启(使用自己的降级处理,默认开启)。开启方式与生产端不一样。
再在消费端的主启动类加注解@EnableHystrix
消费端的Controller中加入降级处理
@HystrixCommand(fallbackMethod = "payment_TimeoutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000")//超过2秒就降级自己
})
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.payment_Timeout(id);
log.info("*******result:"+result);
return result;
}
//兜底方法,上面方法出问题,我来处理,返回一个出错信息
public String payment_TimeoutHandler(Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒后再试。或自己运行出错,请检查自己。";
}
再次测试
不知道几秒后(1秒或者2秒??)返回我消费端自己的降级处理
存在问题
我们改一下,消费端的降级处理中的等待时间为3秒
再次测试,发现是1秒就走了消费端的兜底方法?为什么呢
因为咱没配置开启,所以即使表面上看的是3000,实际上用的是1秒。
OpenFeign 客户端超时控制 ---- hystrix超时配置
9000就是消费端等待提供端的时间。
在配置文件里开启了hystrix的超时时间为9000.
以下纯属我自己的憨憨想法。。
消费端80 降级处理器等3秒,服务端8001 降级处理器等5秒
1、线程在服务端睡了13秒,那么此时走的降级处理器是消费端自己的
2、那要是线程睡了4秒,还是选择消费端的自己的降级处理器
那为啥还要在服务端配置降级处理器,都用不到它的兜底方法?
#hystrix的超时时间
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 9000
再去访问
http://localhost/consumer/payment/hystrix/timeout/2
那就好使了
全局降级处理器(优化)
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量
1、每个方法配置一个服务降级方法,技术上可以,但实际上傻X
2、除了个别重要核心业务有专属,其它普通的可以通过统一跳转到统一处理结果页面
@DefaultProperties(defaultFallback = "全局降级处理方法") 写在类上
@RestController @Slf4j @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") //全局的 public class OrderHystrixController { @Resource private PaymentHystrixService paymentHystrixService; //超时降级演示 @HystrixCommand(commandProperties = { @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")//超过1.5秒就降级自己 }) @GetMapping("/consumer/payment/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id){ String result = paymentHystrixService.payment_Timeout(id); log.info("*******result:"+result); return result; } //下面是全局默认fallback方法 public String payment_Global_FallbackMethod(){ return "Global异常处理信息,请稍后再试,(┬_┬)"; } }
测试
服务端宕机或关闭,单独写降级处理
当然你也不写,那就是混在一块了。超时、报错、服务器宕机都用同一套降级处理方法。为了追求完美。
@Component
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK , (┬_┬)";
}
@Override
public String payment_Timeout(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut , (┬_┬)";
}
}
顺便告诉他,服务器宕机的话要走得降级处理器在哪
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息,当检测到该节点微服务调用响应正常后,恢复调用链路。
熔断机制的注解还是@HystrixCommand
Hystrix会监控微服务间调用的状态,当失败的调用到一定阈值,缺省是10秒内20次调用并有50%的失败情况,就会启动熔断机制。
时间窗口期-达到错误百分比
熔断类型:
熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
circuitBreaker 断路器
断路器的三个重要参数:快照时间窗、请求总数阈值、错误百分比阈值。
1、快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
2、请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断。默认20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3、错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。
circuitBreaker.enabled,value = "true" | 是否开启断路器 |
circuitBreaker.requestVolumeThreshold,value = "10" | 当快照时间窗(默认10秒)内达到此数量才有资格打开断路,默认20个 |
circuitBreaker.sleepWindowInMilliseconds,value = "10000" | 断路多久以后开始尝试是否恢复,默认5s |
circuitBreaker.errorThresholdPercentage,value = "60" | 出错百分比阈值,当达到此阈值后,开始短路。默认50% |
//服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), //是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), //当在配置时间窗口内达到此数量,达到错误百分比阈值,默认20个
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), //断路多久以后开始尝试是否恢复,默认5s
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),//出错百分比阈值,当达到此阈值后,开始短路。默认50%
})
public String paymentCircuitBreaker(Integer id){
if (id < 0){
throw new RuntimeException("*****id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();//hutool.cn工具包
return Thread.currentThread().getName()+"\t"+"调用成功,流水号:"+serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){
return "id 不能负数,请稍候再试,(┬_┬)/~~ id: " +id;
}
当断路器开启的时候,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallbak
hystrix自动恢复功能
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
熔断测试:
controller加一个方法
//===服务熔断 @GetMapping("/payment/circuit/{id}") public String paymentCircuitBreaker(@PathVariable("id") Integer id){ String result = paymentService.paymentCircuitBreaker(id); log.info("*******result:"+result); return result; }
1、浏览器输入正确的id
2、浏览器输入错误的id
3、触发熔断,10s内起码错误的请求占比60%以上,
4、 再回去输入正确的id,发现也不好使了(已经熔断了)
服务的降级->进而熔断->恢复调用链路
创建新的工程 cloud-consumer-hystrix-dashboard9001
1、引入依赖
!--新增hystrix dashboard--> dependency>
org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard /dependency> dependency>org.springframework.boot spring-boot-starter-actuator /dependency>2、配置文件
server: port: 9001 hystrix: dashboard: proxy-stream-allow-list: "localhost"
3、主启动
4.被监控服务要引入的依赖
actuator 直译为“执行器”
org.springframework.boot spring-boot-starter-actuator 5.在被监控服务主启动类指定监控路径
@SpringBootApplication @EnableEurekaClient @EnableHystrix public class PaymentHystrixMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentHystrixMain8001.class,args); } /** *此配置是为了服务监控而配置,与服务容错本身无关,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; } }
测试:
官网:Spring Cloud Gateway
一句话:Geteway是原Zuul1.x版的替代
Gateway是一个web项目,本质也是微服务(可当做过滤器使用),也是微服务的入口。
服务响应也要过网关
使用要求
基于Spring5,SpringBoot2
SpringCloud Gatway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通讯框架Netty
Spring Cloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架
统一的路由方式且基于Filter链
网关的位置
nginx做负载均衡。(nginx没法写业务)nginx去找网关。
网关的作用
GateWay的工作流程
路由转发+执行过滤器链
客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping中找到与请求匹配的路由,将其发送到Gateway Web Handler.
Handler再通过指定的过滤器链来将请求发送给我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前("pre")或之后("post")执行业务逻辑。
Filter在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量控制等有着非常重要的作用
路由是构建网关的基本模块(路由是一个动作),它由4个属性ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
创建新的工程:cloud-gateway-gateway9527,又是那无聊至极的3步
1、引入依赖
org.springframework.cloud spring-cloud-starter-gateway 2、配置文件
server: port: 9527 spring: application: name: cloud-gateway eureka: instance: hostname: cloud-gateway-service client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka
3、主启动
package com.atguigu.springcloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @SpringBootApplication @EnableEurekaClient public class GateWayMain9527 { public static void main(String[] args) { SpringApplication.run( GateWayMain9527.class,args); } }
网关做路由映射,就在配置文件中
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
按道理说,网关后面应该跟的是消费端,现在咱不那么麻烦,直接让网关路由到提供端8001。
启动测试:
当然,你要是直接访问8001也是好使的,老师所说的隐藏我也不觉得安全啊!,照样能访问
默认情况下Gateway会根据注册中心的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://CLOUD-PAYMENT-SERVICE
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
使用网关访问,
第一次
第二次
如果请求与断言相匹配则进行路由
Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给 Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。
Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://CLOUD-PAYMENT-SERVICE
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
#- After=2020-03-08T10:59:34.102+08:00[Asia/Shanghai] 没到时间进行测试会报错
#- Cookie=username,xiaoyumao #并且Cookie是username=xiaoyumao才能访问
#- Header=X-Request-Id, \d+ #请求头中要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.atguigu.com
#- Method=GET
#- Query=username, \d+ #要有参数名称并且是正整数才能路由
比如常用的
使用过滤器,可以在请求被路由前或者之后对请求进行修改