Java微服务-第二节 服务熔断与降级Hystrix、微服务网关Zuul

服务熔断与降级Hystrix

熔断与降级

  1. 概念:
    1. 熔断: 类似保险丝. 防止整个系统故障,保护自己和下游服务
    2. 降级:抛弃非核心业务,保障核心业务的正常运行.
  2. 对比:
    1. 相同点:目的很一致,都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;最终表现类似:,对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
    2. 不同点:触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;

服务雪崩效应原因和解决思路

  1. 原因:商品详情展示服务会依赖商品服务, 价格服务,商品评论服务,调用三个依赖服务会共享商品详情服务的线程池,如果其中的商品评论服务不可用(超时,代码异常等等), 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩。即:大量请求线程同步等待造成的资源耗尽。
  2. 解决方案:
    1. 超时机制:如果我们加入超时机制,例如2s,那么超过2s就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题
    2. 服务限流:通过线程池+队列的方式或者通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中
    3. 服务熔断:当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为1s,如果短时间内有大量请求在1s内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费
    4. 服务降级:有服务熔断,必然要有服务降级。所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的fallback(回退)回调,返回一个缺省值。 例如:(备用接口/缓存/mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景

Hystrix简介

  1. hystrix对应的中文名字是“豪猪”,在分布式环境中,不可避免地会遇到所依赖的服务挂掉的情况,Hystrix 可以通过增加 延迟容忍度错误容忍度,来控制这些分布式系统的交互。Hystrix 在服务与服务之间建立了一个中间层,防止服务之间出现故障,并提供了失败时的 fallback 策略,来增加你系统的整体可靠性和弹性。
  2. 官网:https://github.com/Netflix/Hystrixhttps://github.com/Netflix/Hystrix/wiki
  3. Hystrix 提供了以下服务
    1. 引入第三方的 client 类库,通过延迟与失败的检测,来保护服务与服务之间的调用(网络间调用最为典型)
    2. 阻止复杂的分布式系统中出现级联故障
    3. 快速失败与快速恢复机制
    4. 提供兜底方案(fallback)并在适当的时机优雅降级
    5. 提供实时监控、报警与操作控制
  4. Hystrix 的设计原则
    1. 防止任何一个单节点将容器中的所有线程都占满
    2. 通过快速失败,取代放在队列中等待
    3. 提供在故障时的应急方法(fallback)
    4. 使用隔离技术 (如 bulkhead, swimlane, 和 circuit breaker patterns) 来限制任何一个依赖项的影响面
    5. 提供实时监控、报警等手段
    6. 提供低延迟的配置变更
    7. 防止客户端执行失败,不仅仅是执行网络请求的客户端

order-server项目集成Hystrix

  1. 在order-server中添加hystrix依赖

      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
     </dependency>
    
  2. 在启动类中添加@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实例的注入
     }
    
  3. 在最外层添加熔断降级的处理. 在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";
         }
     }
    
  4. 访问,再10s内连续访问5次,打印结果如下:

     //第1次访问
     调用业务方法
     调用降级服务
     //第2次访问
     调用业务方法
     调用降级服务
     //第3次访问
     调用业务方法
     调用降级服务
     //第4次访问
     调用降级服务
     //第5次访问
     调用降级服务
    
    1. 可以看出从第4次访问开始,就已经不在调用test1方法了,实现了服务降级

服务降级的重点属性

  1. 熔断隔离策略:可以给某个方法设置熔断隔离的策略,有2种
    1. 线程池(默认):服务耗时比较长的建议使用线程池,当前修饰的方法最多可以有多少线程被调用
    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最大的队列数(线程达到最大时,队列中可以放几个请求)
     }
    
  2. 超时时间调整,项目全局设置服务降级属性

     是否开启超时限制 (一定不要禁用)
     hystrix:
       command:
        default:
         execution:
           timeout:
             enabled: 
        
     超时时间调整
     hystrix:
       command:
         default:
           execution:
             isolation:
               thread:
                 timeoutInMilliseconds: 4000
    

Feign集成Hystrix

  1. 远程调用的时候也涉及服务降级,比如在API层设置服务降级,一旦调用某个方法出现失败就可以进行降级调用。
  2. 在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);
     }
    
  3. 在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")
    
  4. 在调用者order-server添加配置,启动服务降级。

     //默认是关闭的,需要手动开启一下
     feign:
       hystrix:
         enabled: true
    
  5. 配置product-server项目的服务降级超时时间yml

     //超时2s就会走降级
     hystrix:
       command:
         default:
           execution:
             isolation:
               thread:
                 timeoutInMilliseconds: 2000
    
  6. 在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;
     }
    
  7. 运行项目测试,一旦order-server访问product-server的find方法因为超时大于2s,就会走服务降级ProductFeignHystrix的find方法。

微服务网关Zuul

Zuul简介

  1. Zuul网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能,提供路由请求、鉴权、监控、缓存、限流等功能
  2. 作用
    1. 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求,即统一的权限判断、防止爬虫的判断。
    2. 审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论,比如日志监控追踪(ELK/Cat)
    3. 动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
    4. 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平,比如限流处理。
    5. 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
    6. 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
    7. 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。

网关项目zuul-server搭建

  1. 使用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>
    
  2. 添加application.yml配置文件并添加相关的配置信息.

     server:
       port: 9000
     spring:
       application:
         name: zuul-server
     eureka:
       client:
         serviceUrl:
           defaultZone: http://localhost:8761/eureka/
    
  3. 在启动类上贴上@EnableZuulProxy注解,可以直接启动该服务了

     @SpringBootApplication
     @EnableZuulProxy //注意不要选EnableZuulServer
     public class ZuulServerApplication {
         public static void main(String[] args) {
             SpringApplication.run(ZuulServerApplication.class, args);
         }
     }
    

相关配置

  1. 默认情况下是开启zuul网关的, 可以直接通过访问网关来访问具体的某个服务,默认规则:网关地址:/服务名称(注册中心的服务名称)/资源路径?参数

     网关地址:/服务名称(注册中心的)/资源路径?参数
     http://localhost:9000/order-server/api/v1/orders?productId=2&userId=3
     http://localhost:9000/product-server/api/v1/product/find?id=2
    
  2. 也可以自定义路由规则

    1. 基于URL进行配置:通过指定URL路由到哪个服务。(只能指定product-server一个节点)
       zuul:
         ignoredPatterns: /*-server/**  #忽略地址,禁止一些地址访问,黑名单
         routes:
           product-server-route: #路由Id, 自定义名字, 不能重复
             #指定rul,只要url+path路径匹配上就会到product-server
             path: /product/** #访问路径,  ? 代表单个字符, * 代表多个字符, 但是不能跨层级, ** 代表多个字符, 可以跨层级
             url: http://localhost:8081/
      
    2. 基于服务名称进行配置(指定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
      
    3. 访问:http://localhost:9000/product/api/v1/product/find?id=1,会路由到product-server。

自定义Zuul过滤器实现登录鉴权

  1. Zuul的底层是一个ZuulServlet:https://github.com/Netflix/zuul/wiki/How-it-Works
  2. ZuulServlet在生命周期方法中判断了很多过滤器,生命周期如下(可以深入研究一下):

    图

  3. 场景设置:判断是否有携带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;
         }
     }
    
  4. 注意: 默认情况,网关会把”Cookie”, “Set-Cookie”, “Authorization”这三个请求头过滤掉,下游的服务是获取不到这几个请求头的(比如订单、商品服务无法过去到该请求头字段),如果不需要这个过滤,可以在网关服务yml配置

     zuul:
       routes:
       # 关闭zuul网关对敏感请求头参数的过滤
       sensitive-headers:
    

EnableZuulProxy和EnableZuulServer的区别

  1. zuul提供了两个注解 @EnableZuulProxy@EnableZuulServer,来启用不同的过滤器集合。
  2. @EnableZuulProxy 启用的过滤器 是@EnableZuulServer 的超集
    1. PreDecorationFilter: 代理的Filter
    2. RibbonRoutingFilter: 基于ServiceId远程调用的filter
    3. SimpleHostRoutingFilter: 基于Url的远程调用的filter
  3. @EnableZuulServer 不提供路由功能,作为server模式而不是代理模式运行
Table of Contents