• springcloud学习(三)之Hystrix


    前言

    雪崩效应

    在微服务架构中,⼀个应⽤可能会有多个微服务组成,微服务之间的数据交互通过远程过程调⽤完成。

    这就带来⼀个问题,假设微服务A调⽤微服务B和微服务C,微服务B和微服务C⼜调⽤其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调⽤响应时间过⻓或者不可⽤,对微服务A的调⽤就会占⽤越来越多的系统资源,进⽽引起系统崩溃,所谓的“雪崩效应”。

    如图中所示,最下游简历微服务响应时间过⻓,⼤量请求阻塞,⼤量线程不会释放,会导致服务器资源耗尽,最终导致上游服务甚⾄整个系统瘫痪。

    扇⼊:代表着该微服务被调⽤的次数,扇⼊⼤,说明该模块复⽤性好

    扇出:该微服务调⽤其他微服务的个数,扇出⼤,说明业务逻辑复杂

    扇⼊⼤是⼀个好事,扇出⼤不⼀定是好事

    雪崩效应的三种解决方案

    从系统的可靠性与可用性出发,常见的解决雪崩效应的三种技术方案:服务熔断、服务降级、服务限流。

    服务熔断

    服务熔断体现在一个“断”字上,通俗来讲,就是当检测到某个扇出链路的微服务不可用或者响应时间太长时,熔断该服务的调用,进行服务降级,快速返回错误的响应信息。当该服务正常之后,再恢复该服务的调用链路。
    服务熔断与服务降级是通常在一起使用的,比如Hystrix。
    服务消费者工程引入jar坐标:

    <!--熔断器Hystrix-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            </dependency>
    

    服务消费者启动类上添加@EnableCircuitBreaker开启熔断注解。

    在服务消费者的controller中调用代码方法上添加注解@HystrixProperty,里面配置超时时间属性,设置为2秒。

    @GetMapping("/checkStateTimeout/{userId}")
        @HystrixCommand(
                commandProperties = {
                        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
                        value = "2000")
                }
    
        )
        public Integer findResumeOpenState(@PathVariable Long userId) {
            String url="http://lagou-service-resume/resume/openstate/"+userId;
            Integer forObject =restTemplate.getForObject(url,Integer.class);
            System.out.println("======>>>从eureka server获取服务提供者实例:"+url);
            return forObject;
        }
    

    为了测试,我们在8080端口的服务提供者工程里面的方法中添加上线程休眠语句,模拟请求超时:

     @GetMapping("/openstate/{userId}")
        public Integer findDefaultResumeState(@PathVariable Long userId) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return port;
        }
    

    效果测试:

    上面我们说的是服务熔断,当服务提供者出现异常或者请求超时时,返回给调用方的是异常信息。我们不想显示异常页面,这样对用户太不友好,所以我们返回一个预设的默认值(兜底数据)给服务的调用者,这就是服务降级。

    服务降级

    服务降级,通俗讲就是整体资源不够⽤了,先将⼀些不关紧的服务停掉(调⽤我的时候,给你返回⼀个预留的值,也叫做兜底数据),待渡过难关⾼峰过去,再把那些服务打开。服务降级⼀般是从整体考虑,就是当某个服务熔断之后,服务器将不再被调⽤,此刻客户端可以⾃⼰准备⼀个本地的fallback回调,返回⼀个缺省值,这样做,虽然服务⽔平下降,但好⽍可⽤,⽐直接挂掉
    要强。

    在上一步服务熔断的代码基础上,我们添加服务降级的代码。

    我们定义一个预设方法,与原方法的入参与返回结构类型保持一致,在@HystrixProperty注解里配置fallbackMethod参数为预设的方法名即可。

    如下,我们在预设方法中返回默认值-1:

        @GetMapping("/checkStateTimeoutFallBack/{userId}")
        @HystrixCommand(
                commandProperties = {
                        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
                                value = "2000")
                },
                fallbackMethod = "myFallback"
        )
        public Integer findResumeOpenStateFallBack(@PathVariable Long userId) {
            String url="http://lagou-service-resume/resume/openstate/"+userId;
            Integer forObject =restTemplate.getForObject(url,Integer.class);
            System.out.println("======>>>从eureka server获取服务提供者实例:"+url);
            return forObject;
        }
    
        //预设方法
        public Integer myFallback(Long userId){
            return -1; //返回预设值(兜底数据)
        }
    
    

    效果如下:

    服务限流

    服务降级是当服务出问题或者影响到核⼼流程的性能时,暂时将服务屏蔽掉,待⾼峰或者问题解决后再打开;但是有些场景并不能⽤服务降级来解决,⽐如秒杀业务这样的核⼼功能,这个时候可以结合服务限流来限制这些场景的并发/请求量
    限流措施也很多,⽐如:

    • 限制总并发数(⽐如数据库连接池、线程池)
    • 限制瞬时并发数(如nginx限制瞬时并发连接数)
    • 限制时间窗⼝内的平均速率(如Guava的RateLimiter、 nginx的limit_req模块,限制每秒的平均速率)
    • 限制远程接⼝调⽤速率、限制MQ的消费速率等

    Hystrix 舱壁模式(线程池隔离策略)

    Hystrix默认是有一个线程池,来统一维护需要熔断及降级的方法,所有添加了@HystrixCommand注解的方法共用一个线程池。

    如果不进⾏任何设置,所有熔断⽅法使⽤⼀个Hystrix线程池(默认10个线程),那么这样的话会导致问题,这个问题并不是扇出链路微服务不可⽤导致的,⽽是我们的线程机制导致的,如果⽅法A的请求把10个线程都⽤了,⽅法2请求处理的时候压根都没法去访问B,因为没有线程可⽤,并不是B服务不可⽤。

    为了避免问题服务请求过多导致正常服务⽆法访问, Hystrix 不是采⽤增加线程数,⽽是单独的为每⼀个控制⽅法创建⼀个线程池的⽅式,这种模式叫做“舱壁模式",也是线程隔离的⼿段。

    舱壁模式在现实生活中的例子:

    比如一艘船,里面混乱的放着牛奶、面包、药品等各种物品,如果这艘船某一个地方漏水,那么这艘船里的所有物品都会受到影响。(类似于Hystrix默认的全局一个线程池管理所有方法)
    怎么解决这个问题呢?
    给船舱里面加上隔板,比如把面包和药品彻底隔离开,这样当药品这边漏水了之后,也不会影响到面包。(这就是Hystrix的舱壁模式)

    我们可以先看一下Hystrix的默认的所有方法共用一个线程池的效果:

    在PostMan里面批量请求加了@HystrixCommand的两个方法,循环请求二十次,模拟多线程:

    在Windows命令行中通过jps及jstack命令查看线程信息:(Linux系统中将findstr命令改为grep命令即可)

    可以看到一共有10个线程。

    接下来我们运用舱壁模式配置线程池参数,部分代码如下(完整代码见文末源码):

     @GetMapping("/checkStateTimeout/{userId}")
        @HystrixCommand(
                //线程池 的标识,要保持唯一,不然与其它方法相同名的线程池就共用
                threadPoolKey = "checkStateTimeout",
                //线程数细节配置
                threadPoolProperties = {
                        @HystrixProperty(name="coreSize",value = "1"),  //线程数(同时运行的线程数)
                        @HystrixProperty(name="maxQueueSize",value = "20") //等待队列长度
                },
                commandProperties = {
                        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
                        value = "2000")
                }
        )
        public Integer checkStateTimeout(@PathVariable Long userId) {
            String url="http://lagou-service-resume/resume/openstate/"+userId;
            Integer forObject =restTemplate.getForObject(url,Integer.class);
            System.out.println("======>>>从eureka server获取服务提供者实例:"+url);
            return forObject;
        }
    
    
        @GetMapping("/checkStateTimeoutFallBack/{userId}")
        @HystrixCommand(
                //线程池 的标识,要保持唯一,不然与其它方法相同名的线程池就共用
                threadPoolKey = "findResumeOpenStateFallBack",
                //线程数细节配置
                threadPoolProperties = {
                        @HystrixProperty(name="coreSize",value = "2"),  //线程数(同时运行的线程数)
                        @HystrixProperty(name="maxQueueSize",value = "20") //等待队列长度
                },
    
                commandProperties = {
                        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
                                value = "2000")
                },
                fallbackMethod = "myFallback"
        )
        public Integer findResumeOpenStateFallBack(@PathVariable Long userId) {
            String url="http://lagou-service-resume/resume/openstate/"+userId;
            Integer forObject =restTemplate.getForObject(url,Integer.class);
            System.out.println("======>>>从eureka server获取服务提供者实例:"+url);
            return forObject;
        }
        
        //预设方法
        public Integer myFallback(Long userId){
            return -1; //返回预设值(兜底数据)
        }
    

    我们在代码中给checkStateTimeout方法配置线程数为1,给checkStateTimeoutFallBack方法配置线程数为2,然后在postman中批量运行线程,
    结束之后查看线程信息如下:

    可以看到名为checkStateTimeout的线程只有1个,名为checkStateTimeoutFallBack的线程有2个,与我们程序中设置一致。

    Hystrix工作流程

    在微服务调用链路中当某个服务某一次出现问题时,Hystrix并不是直接一刀切的切断有问题的链路,从此不再启用,而是通过一段时间间隔的阈值来进行检测判断,具体的流程如下图:

    工作流程:

    1. 当调⽤出现问题时,开启⼀个时间窗(10s)

    2. 在这个时间窗内,统计调⽤次数是否达到最⼩请求数(自己设定)
      <1>如果没有达到,则重置统计信息,回到第1步
      <2>如果达到了,则统计失败的请求数占所有请求数的百分⽐,是否达到阈值?
      ①如果达到,则跳闸(不再请求对应服务)
      ②如果没有达到,则重置统计信息,回到第1步

    3. 如果跳闸,则会开启⼀个活动窗⼝(默认5s),每隔5s, Hystrix会让⼀个请求通过,到达那个问题服务,看是否调⽤成功,如果成功,重置断路器回到第1步,如果失败,回到第3步

    springboot中暴露健康检查等断点接⼝,在服务消费者一方的application.yml中配置如下

    management:
      endpoints:
        web:
          exposure:
            include: "*"
    # 暴露健康接⼝的细节
      endpoint:
        health:
          show-details: always
    

    查看健康状态:

    接下来我们配置一些参数,
    实现“8秒钟内,请求次数达到2个,并且失败率在50%以上,就跳闸,跳闸后活动窗⼝设置为3s”,常用的参数如下:

    //8秒钟内,请求次数达到2个,并且失败率在50%以上,就跳闸
                       //跳闸后活动窗⼝设置为3s
                       @HystrixProperty(name =
                               "metrics.rollingStats.timeInMilliseconds",value = "8000"),
                       @HystrixProperty(name =
                               "circuitBreaker.requestVolumeThreshold",value = "2"),
                       @HystrixProperty(name =
                               "circuitBreaker.errorThresholdPercentage",value = "50"),
                       @HystrixProperty(name =
                               "circuitBreaker.sleepWindowInMilliseconds",value = "3000")
    
    

    在postman中批量执行调用checkStateTimeoutFallBack方法,在健康状态监测中可以看到hystrix的状态变化,由熔断到恢复:
    连续调用几次后熔断:

    后来变为正常:

    Hystrix Dashboard断路监控仪表盘

    前面通过/actuator/health接口查看到的信息是一个hystrix的整体的运行状态,要想看具体的细节,比如每个加了@HystrixCommand注解的方法的hystrix的运行信息的话,我们应该寻找另外的方式。Hystrix Dashboard断路监控仪表盘就是干这个的。

    /actuator/hystrix.stream接⼝可以获取到监控的⽂字信息,但是不直观,Hystrix官⽅还提供了基于图形化的DashBoard(仪表板)监控平台,我们可以通过新建单独的DashBoard服务通过图形化的页面来直观查看hystrix的详细信息。

    在父工程下新建一个监控工程,导入依赖:

    <!--hystrix-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
       </dependency>
       <!--hystrix 仪表盘-->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
       </dependency>
    

    启动类上加上@EnableHystrixDashboard注解。
    在被监测的微服务中注册监控servlet,我们在服务启动类中注入bean:

    @Bean
       public ServletRegistrationBean getServlet(){
           HystrixMetricsStreamServlet streamServlet = new
                   HystrixMetricsStreamServlet();
           ServletRegistrationBean registrationBean = new
                   ServletRegistrationBean(streamServlet);
           registrationBean.setLoadOnStartup(1);
           registrationBean.addUrlMappings("/actuator/hystrix.stream");
           registrationBean.setName("HystrixMetricsStreamServlet");
           return registrationBean;
       }
    

    在被监控微服务发布之后,可以直接访问监控servlet:

    可以看到数据,但是并不直观。

    发布监控微服务,访问http://localhost:9000/hystrix如下:

    进入监控详情页面后如下:

    我们可以实时的监控方法的状态。

    Hystrix Turbine聚合监控

    前面我们是针对一个微服务实例的Hystrix进行数据查询分析, 在微服务架构下,⼀个微服务的实例往往是多个(集群化)

    ⽐如⾃动投递微服务我们可以部署三台:

    实例1(hystrix) ip1:port1/actuator/hystrix.stream

    实例2(hystrix) ip2:port2/actuator/hystrix.stream

    实例3(hystrix) ip3:port3/actuator/hystrix.stream

    按照已有的⽅法,我们就可以结合dashboard仪表盘每次输⼊⼀个监控数据流url,进去查看。但是这样比较麻烦,我们能否有一个聚合的工具把同一个服务的所有实例的Hystrix状态信息聚合起来呢?
    答案是:Hystrix Turbine聚合(聚合各个实例上的hystrix监控数据)

    Turbine的原理图如下:

    建立一个hystrix-turbine微服务,引入依赖:

    <dependencies>
        <!--hystrix turbine聚合监控-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
        </dependency>
        <!--
        引⼊eureka客户端的两个原因
        1、微服务架构下的服务都尽量注册到服务中⼼去,便于统⼀管理
        2、后续在当前turbine项⽬中我们需要配置turbine聚合的服务,⽐如,我们希望聚合
        lagou-service-autodeliver这个服务的各个实例的hystrix数据流,那随后
        我们就需要在application.yml⽂件中配置这个服务名,那么turbine获取服务下具
        体实例的数据流的
        时候需要ip和端⼝等实例信息,那么怎么根据服务名称获取到这些信息呢?
        当然可以从eureka服务注册中⼼获取
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        </dependencies>
    

    application.yml中进行turbine的相关配置:

    #turbine配置
    turbine:
    #appCofing配置需要聚合的服务名称,⽐如这⾥聚合⾃动投递微服务的hystrix监控数据
    #如果要聚合多个微服务的监控数据,那么可以使⽤英⽂逗号拼接,⽐如 a,b,c
      appConfig: lagou-service-autodeliver
      clusterNameExpression: "'default'" # 集群默认名称
    

    在启动类上添加@EnableTurbine开启仪表盘以及Turbine聚合。

    依次启动eureka注册中心、服务提供者、服务消费者、HystrixDashboard服务、HystrixTuibin服务。
    完毕后在浏览器输入http://localhost:9001/turbine.stream,在postman中请求8090和8091两个服务消费者实例,可以看到turbine的监控信息:

    访问http://localhost:9000/hystrix,将tuibine.stream地址输入:

    postman中批量访问两个服务消费者实例:

    可以观察多台主机里的方法聚合的状态信息:

    源码地址

    hystrix-demo源码地址: hystrix-demo源码地址

    附录:

    idea删除模块后创建同名模块遇到的问题及解决方案

    idea复制项目module出现的问题

    springboot启动报无法加载主类错误

    欢迎访问我的博客:https://www.liuyj.top

  • 相关阅读:
    Div+Css布局教程(-)CSS必备知识
    html表格设置
    wxAui Frame Management用法
    aui
    MySQL死锁
    InnoDB索引存储结构
    MySQL事务调优
    MySQL慢SQL语句常见诱因
    InnoDB的LRU淘汰策略
    InnoDB事务之redo log工作原理
  • 原文地址:https://www.cnblogs.com/liuyj-top/p/14233089.html
Copyright © 2020-2023  润新知