Java微服务-第二节 服务熔断与降级Hystrix、微服务网关Zuul
服务熔断与降级Hystrix
熔断与降级
- 概念:
- 熔断: 类似保险丝. 防止整个系统故障,保护自己和下游服务
- 降级:抛弃非核心业务,保障核心业务的正常运行.
- 对比:
- 相同点:目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;最终表现类似:,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
- 不同点:触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
服务雪崩效应原因和解决思路
- 原因:商品详情展示服务会依赖商品服务, 价格服务,商品评论服务,调用三个依赖服务会共享商品详情服务的线程池,如果其中的商品评论服务不可用(超时,代码异常等等), 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩。即:大量请求线程同步等待造成的资源耗尽。
- 解决方案:
- 超时机制:如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题
- 服务限流:通过线程池+队列的方式或者通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中
- 服务熔断:当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费
- 服务降级:有服务熔断,必然要有服务降级。所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景
Hystrix简介
- hystrix对应的中文名字是“豪猪”,在分布式环境中,不可避免地会遇到所依赖的服务挂掉的情况,Hystrix 可以通过增加 延迟容忍度 与 错误容忍度,来控制这些分布式系统的交互。Hystrix 在服务与服务之间建立了一个中间层,防止服务之间出现故障,并提供了失败时的 fallback 策略,来增加你系统的整体可靠性和弹性。
- 官网:https://github.com/Netflix/Hystrix、https://github.com/Netflix/Hystrix/wiki
- Hystrix 提供了以下服务
- 引入第三方的 client 类库,通过延迟与失败的检测,来保护服务与服务之间的调用(网络间调用最为典型)
- 阻止复杂的分布式系统中出现级联故障
- 快速失败与快速恢复机制
- 提供兜底方案(fallback)并在适当的时机优雅降级
- 提供实时监控、报警与操作控制
- Hystrix 的设计原则
- 防止任何一个单节点将容器中的所有线程都占满
- 通过快速失败,取代放在队列中等待
- 提供在故障时的应急方法(fallback)
- 使用隔离技术 (如 bulkhead, swimlane, 和 circuit breaker patterns) 来限制任何一个依赖项的影响面
- 提供实时监控、报警等手段
- 提供低延迟的配置变更
- 防止客户端执行失败,不仅仅是执行网络请求的客户端
order-server项目集成Hystrix
-
在order-server中添加hystrix依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
在启动类中添加
@EnableCircuitBreaker
注解//启动类 @springBootApplication //扫描@EnableFeignclients注解对应的包名下的所有接口,并且创建代理对象。 @EnableFeignClients(basePackages ="com.coderzhong.demo.feign") //开启熔断 @EnableCircuitBreaker public class OrderServerApplication { public static void main(string[] args){ SpringApplication.run(0rderserverApplication.class,args);} //删除RestTemplate实例的注入 }
-
在最外层添加熔断降级的处理. 在order-server中的控制器中的方法添加
@HystrixCommand(fallbackMethod = "saveFail")
注解 (注意fallbackMethod需要和原方法一样的签名(参数、返回值))@RestController public class HelloController { @Autowired private RedisTemplate redisTemplate; @RequestMapping("test1") //现在这个情况 还没有熔断, 如果要开启熔断, 不调用业务方法,那么需要开启熔断开关, 但是开启熔断开关有条件 // 满足三个条件 /** * 1 在指定的时间的时间窗口 10s * 2 需要达到一定的请求量 3个请求 * 3 需要请求的错误率到达20% */ @HystrixCommand(fallbackMethod ="test1Fallback", commandProperties = { @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds",value = "10000"), @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "3"), @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "30") } ) public String test1(){ System.out.println("调用业务方法"); //故障方法 int i = 1/0; return "success"; } public string testlFallback(){ //通过短信、email通知运维人员。 System.out.println("调用降级处理"); return "default"; } }
-
访问,再10s内连续访问5次,打印结果如下:
//第1次访问 调用业务方法 调用降级服务 //第2次访问 调用业务方法 调用降级服务 //第3次访问 调用业务方法 调用降级服务 //第4次访问 调用降级服务 //第5次访问 调用降级服务
- 可以看出从第4次访问开始,就已经不在调用test1方法了,实现了服务降级
服务降级的重点属性
- 熔断隔离策略:可以给某个方法设置熔断隔离的策略,有2种
- 线程池(默认):服务耗时比较长的建议使用线程池,当前修饰的方法最多可以有多少线程被调用
- 信号量:服务响应很快的建议使用信号量,比如访问的缓存服务等,当前修饰的方法最多可以被调用多少次。
commandProperties = { @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "100"),//配置请求量 @HystrixProperty(name="metrics.rollingStats.timeInMilliseconds",value = "3000"),//配置时间窗口 @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "50"),//配置出现错误百分比 @HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests",value = "30"),//配置最大请求连接数量,即信号量,就是当前方法最多被调用多少次,下面属性配置为信号量模式这个才有效。 @HystrixProperty(name="fallback.isolation.semaphore.maxConcurrentRequests",value = "1000"),//配置降级处理的最大并发数,下面属性配置为信号量模式这个才有效。 @HystrixProperty(name="execution.isolation.strategy",value = "semaphore")//配置处理模式, 信号量的模式还是线程池的模式,默认是线程池的模式,semaphore是信号量。 }, threadPoolProperties = { @HystrixProperty(name="coreSize",value = "10")})//对于线程模式, 配置最大的核心处理线程,这个方法最多有多少个线程来访问。除了coreSize还有maximumSize最大扩展的线程数、maxQueueSize最大的队列数(线程达到最大时,队列中可以放几个请求) }
-
超时时间调整,项目全局设置服务降级属性
是否开启超时限制 (一定不要禁用) hystrix: command: default: execution: timeout: enabled: 超时时间调整 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 4000
Feign集成Hystrix
- 远程调用的时候也涉及服务降级,比如在API层设置服务降级,一旦调用某个方法出现失败就可以进行降级调用。
-
在product-api项目中,ProductFeignApi的
@FeignClient(name = "product-server",fallback = ProductFeignFallback.class)
//一旦调用product-server项目的find方法出现服务降级,就会调用ProductFeignFallback.class的find方法 @FeignClient(name = "product-server",fallback = ProductFeignHystrix.class)) public interface ProductFeignApi { @RequestMapping("/api/v1/product/find") Product find(@RequestParam("id") Long id); }
-
在product-api项目中添加ProductFeignHystrix类
@Component public class ProductFeignFallback implements ProductFeignApi{ @0verride public Product find(Long id){ Product product = new Product(); product.setName("兜底数据"); product.setPrice(0); product.setStock(0); return product; } } //注意,需要在OrderServerApplication启动类新增扫描注解 @SpringBootApplication(scanBasePackages = "com.coderzhong.demo")
-
在调用者order-server添加配置,启动服务降级。
//默认是关闭的,需要手动开启一下 feign: hystrix: enabled: true
-
配置product-server项目的服务降级超时时间yml
//超时2s就会走降级 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2000
-
在product-server项目的ProductFeignClient的find方法中设置一个sleep测试
public Product find(Long id) { Product product = productService.get(id); //模拟该方法耗时 try { TimeUnit.SECONDS.sleep( timeout: 5); } catch(InterruptedException e){ e.printStackTrace(); } Product result = new Product(); BeanUtils.copyProperties(product,result); result.setName(result.getName()+",data from "+port); return result; }
- 运行项目测试,一旦order-server访问product-server的find方法因为超时大于2s,就会走服务降级ProductFeignHystrix的find方法。
微服务网关Zuul
Zuul简介
- Zuul网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能,提供路由请求、鉴权、监控、缓存、限流等功能
- 作用
- 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求,即统一的权限判断、防止爬虫的判断。
- 审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论,比如日志监控追踪(ELK/Cat)
- 动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
- 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平,比如限流处理。
- 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
- 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
- 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
网关项目zuul-server搭建
-
使用Spring Initializr创建SpringBoot项目,然后Dependencies选择Spring Cloud Discovery->Eureka Discovery Client、Spring Cloud Routing->Zuul(Maintenance)(废弃)
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
-
添加application.yml配置文件并添加相关的配置信息.
server: port: 9000 spring: application: name: zuul-server eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/
-
在启动类上贴上@EnableZuulProxy注解,可以直接启动该服务了
@SpringBootApplication @EnableZuulProxy //注意不要选EnableZuulServer public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
相关配置
-
默认情况下是开启zuul网关的, 可以直接通过访问网关来访问具体的某个服务,默认规则:
网关地址:/服务名称(注册中心的服务名称)/资源路径?参数
网关地址:/服务名称(注册中心的)/资源路径?参数 http://localhost:9000/order-server/api/v1/orders?productId=2&userId=3 http://localhost:9000/product-server/api/v1/product/find?id=2
-
也可以自定义路由规则
- 基于URL进行配置:通过指定URL路由到哪个服务。(只能指定product-server一个节点)
zuul: ignoredPatterns: /*-server/** #忽略地址,禁止一些地址访问,黑名单 routes: product-server-route: #路由Id, 自定义名字, 不能重复 #指定rul,只要url+path路径匹配上就会到product-server path: /product/** #访问路径, ? 代表单个字符, * 代表多个字符, 但是不能跨层级, ** 代表多个字符, 可以跨层级 url: http://localhost:8081/
-
基于服务名称进行配置(指定product-server n个节点,自动有负载均衡)
zuul: ignoredPatterns: /*-server/** #忽略地址,禁止一些地址访问,黑名单 routes: product-server-route: #路由Id, 自定义名字, 不能重复,商品服务路由 #指定rul,只要url+path路径匹配上就会到product-server path: /product/** serviceId: product-server # 服务的ID 注册中心注册的serviceId order-server-route: #订单服务路由 path: /order/** serviceId: order-server
- 访问:http://localhost:9000/product/api/v1/product/find?id=1,会路由到product-server。
- 基于URL进行配置:通过指定URL路由到哪个服务。(只能指定product-server一个节点)
自定义Zuul过滤器实现登录鉴权
- Zuul的底层是一个ZuulServlet:https://github.com/Netflix/zuul/wiki/How-it-Works
-
ZuulServlet在生命周期方法中判断了很多过滤器,生命周期如下(可以深入研究一下):
-
场景设置:判断是否有携带cookie参数,如果携带了就认证通过,否则不通过,跳转到失败页面
@Component public class AuthZuulFilter extends ZuulFilter { //过滤器类型:pre/route/post/error @Override public String filterType() { return "pre"; } //排序:值越小,越优先 @Override public int filterOrder() { return 3; } //是否需要过滤,如果为true,执行过滤方法run @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { //1. 获取到请求对象request RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); //2.判断请求参数 //到header中取值 String cookie = request.getHeader("Cookie"); if(StringUtils.isEmpty(cookie)){ //到参数中取 cookie = request.getParameter("Cookie"); } //3. 判断cookie是否为空 if(StringUtils.isEmpty(cookie)){ requestContext.setSendZuulResponse(false); requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); } return null; } }
-
注意: 默认情况,网关会把”Cookie”, “Set-Cookie”, “Authorization”这三个请求头过滤掉,下游的服务是获取不到这几个请求头的(比如订单、商品服务无法过去到该请求头字段),如果不需要这个过滤,可以在网关服务yml配置
zuul: routes: # 关闭zuul网关对敏感请求头参数的过滤 sensitive-headers:
EnableZuulProxy和EnableZuulServer的区别
- zuul提供了两个注解
@EnableZuulProxy
,@EnableZuulServer
,来启用不同的过滤器集合。 @EnableZuulProxy
启用的过滤器 是@EnableZuulServer
的超集- PreDecorationFilter: 代理的Filter
- RibbonRoutingFilter: 基于ServiceId远程调用的filter
- SimpleHostRoutingFilter: 基于Url的远程调用的filter
@EnableZuulServer
不提供路由功能,作为server模式而不是代理模式运行