玩转Hystrix一篇就够了

Hystrix

背景(分布式系统面临的问题)

复杂的分布式体系结构的应用程序有数十个依赖关系,每个依赖在某些时候都可能不可避免的出现问题(网络卡顿、程序出错、调用超时,甚至是机房断电),如果多个调用依赖中每个环节都正常,那么服务执行会一切顺利;但是如果其中某个服务出现问题,则可能会引起服务雪崩

那什么是服务雪崩呢?

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的"扇出”如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”、

对于高流量的应用来说,单-的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

Hystrix是什么?

Hystrix是一个用于处理分 布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。”断路器”本身是一种开关装置,当某个服务单元发生故障之后, 通过断路器的故障监控(类似熔断保险丝) ,向调用方返回一个符合预期的、可处理的备选响应(FallBack) ,而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。springcloud社区神仙打架,底下程序员遭殃,Hystrix很优秀,但是目前已经停更进维了,他虽然已经停更了,但是他优秀的设计理念依然被后面的替代框架所沿用,比如resilience4j、sentine。resilience4j在国外用的比较多,sentine在中国用的比较多。具体查看官网

重要的概念

服务降级 fallback

当某个服务单元发生故障之后,向调用方返回一个符合预期的、可处理的备选响应(FallBack) ,而不是长时间的等待或者抛出调用方无法处理的异常;不让客户端等待并立刻返回一个友好的提示,用来兜底的解决方案。什么时候会发生服务降级呢?程序运行异常、超时、服务熔断触发服务降级、线程池/带宽量满了也会导致服务降级。

服务熔断break

类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。处理流程是服务的降级->进而熔断->恢复调用链路

服务限流 flowlimit

秒杀高并发等操作,流量请求突然变多的时候,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,要么排队有序进行处理这些请求,要么友好提示立即返回,这样保证服务正常,限制⼀次只可以有多少请求,不让服务挂了。

基本案例

基本服务

Server 8081

SimulationService.java
@Service
public class SimulationService {

    /**
     * 模拟正常、迅速的服务
     * @param id
     * @return
     */
    public String simulationSuccess(String id) {
        return "thread:" + Thread.currentThread().getName() + " success:" + id;
    }

    /**
     * 模拟耗时的服务
     * @param id
     * @return
     */
    public String simulationTimeOut(String id) {
        int timeout = 3;
        try {
            TimeUnit.SECONDS.sleep(timeout);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "thread:" + Thread.currentThread().getName() + " timeout:" + id;
    }

}
SimulationController.java
@RestController
public class SimulationController {

    @Autowired
    SimulationService simulationService;

    @GetMapping("/simulation/hystrix/success/{id}")
    public String simulationSuccess(@PathVariable String id){
        String simulationSuccess = simulationService.simulationSuccess(id);
        return simulationSuccess;
    }

    @GetMapping("/simulation/hystrix/timeout/{id}")
    public String simulationTimeOut(@PathVariable String id){
        String simulationTimeOut = simulationService.simulationTimeOut(id);
        return simulationTimeOut;
    }

}

上面这个案例实现了一个端口为8081的服务提供者,实现了一个普通简易的接口,一个模拟耗时操作。当请求simulationSuccess时,数据能立即返回,当请求simulationTimeOut的时候,请求需要等待3秒才会返回数据。正常情况下,请求simulationSuccess会迅速返回结果,但是,当我们用压力测试工具(比如postman、Jmeter)疯狂请求(比如1秒20000的请求)simulationTimeOut时,我们再去访问simulationSuccess时,发现结果返回没那么迅速了;如果压力给的足够大,这个服务也会被拖死,导致客户端请求数据出错;出现这种情况的原因是因为tomcat的默认的工作线程数被打满了,没有多余的线程来分解压力和处理。

构造微服务架构

基本环境

  • 启动一个Eureka服务注册中心
  • 将上述的服务注册进入Eureka
  • 服务调用使用OpenFeign

搭建服务消费者 80

具体搭建过程参考我写的Eureka相关文章或者搜索网络上关于Eureka的用法

建立openFeign服务映射接口
@Component
@FeignClient(value = "cloud-eureka-provide-userinfo-service")
public interface SimulationService {

    @GetMapping("/simulation/hystrix/success/{id}")
    String simulationSuccess(@PathVariable("id") String id);

    @GetMapping("/simulation/hystrix/timeout/{id}")
    String simulationTimeOut(@PathVariable("id") String id);

}
消费接口编写
@RestController
public class SimulationController {

    @Autowired
    SimulationService simulationService;

    @GetMapping("/consume/hystrix/success/{id}")
    public String simulationSuccess(@PathVariable String id){
        String simulationSuccess = simulationService.simulationSuccess(id);
        return simulationSuccess;
    }
    @GetMapping("/consume/hystrix/timeout/{id}")
    public String simulationTimeOut(@PathVariable String id){
        String simulationTimeOut = simulationService.simulationTimeOut(id);
        return simulationTimeOut;
    }

}

上面我们建立了一个服务消费者来消费8081提供的服务,端口为80,正常情况下80消费8081的服务是正常的,但是量变容易引起质变,当我们用压力测试工具请求8081时,这个时候80去调用8081服务的时候,响应就会变的非常缓慢,用户体验极差。

正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生。

如何解决?

问题分析

  • 网络/资源拥塞导致服务变慢:超时策略。
  • 程序错误或宕机:兜底方案

解决方案

  • 8081超时时,为了让调用方不卡死,服务端必须要有服务降级

  • 如果8081宕机或程序出错了,为了让调用方不卡死等待,服务端必须要有服务降级

  • 如果8081提供服务是可接受的,调用方出现故障或者自我要求比服务提供方更加苛刻,客户端自己处理降级。

服务降级

Hystrix提供的服务降级能非常方便的解决上述的问题,Hystrix可用在服务提供方和服务调用方,但是一般来说我们将Hystrix用在服务调用方比较多。

提供方服务降级

服务降级我们用到了 @HystrixCommand这个注解,官网中提供的实例是继承HystrixCommand来实现的。

添加依赖



  org.springframework.cloud
  spring-cloud-starter-netflix-hystrix

启动Hystrix

只需要在主启动类上添加@EnableCircuitBreaker

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class EurekaHystrixProvideApplication8081 {

    public static void main(String[] args) {
        SpringApplication.run(EurekaHystrixProvideApplication8081.class,args);
    }

}

修改SimulationService

  • 超时限制
@HystrixCommand(
      // 绑定兜底方法 timeoutHandle
  fallbackMethod = "timeoutHandle",commandProperties = {
    // 下面这个指定的是超时阈值
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
  })
public String simulationTimeOut(String id) {
  int timeout = 5;
  try {
    TimeUnit.SECONDS.sleep(timeout);
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  return "thread:" + Thread.currentThread().getName() + " timeout:" + timeout;
}

/**
     * 兜底的方法,请求超时或者出现异常都会跳转到这个方法
     * @param id
     * @return
     */
public String timeoutHandle(String id){
  return "thread:" + Thread.currentThread().getName() + "  request fail, id:" + id;
}

我们请求 8081simulationTimeOut,因为该方法需要5秒才能处理完成,但是我们设置的阈值是3秒,所以程序跳转执行timeoutHandle方法,并返回类似thread:HystrixTimer-4 request fail, id:1,从线程名也说明了fallback是Hystrix新起的一个线程去执行的。

  • 异常限制
@HystrixCommand(
      // 绑定兜底方法 timeoutHandle
  fallbackMethod = "timeoutHandle",commandProperties = {
    // 下面这个指定的是超时阈值
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
  })
public String simulationTimeOut(String id) {
  int a = 100/0;
  return "thread:" + Thread.currentThread().getName() + " timeout:" + timeout;
}

/**
     * 兜底的方法,请求超时或者出现异常都会跳转到这个方法
     * @param id
     * @return
     */
public String timeoutHandle(String id){
  return "thread:" + Thread.currentThread().getName() + "  request fail, id:" + id;
}

请求simulationTimeOut时,因为a=100/0会导致程序出现异常,所以还是会执行timeoutHandle方法来兜底。

调用方服务降级

添加依赖



  org.springframework.cloud
  spring-cloud-starter-netflix-hystrix

修改yml

# 开启hystrix
feign:
  hystrix:
    enabled: true

启动Hystrix

启动类上添加@EnableHystrix注解即可

@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class EurekaHystrixConsumeApplication80 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaHystrixConsumeApplication80.class,args);
    }
}

修改SimulationController

@GetMapping("/consume/hystrix/timeout/{id}")
@HystrixCommand(
  // 绑定兜底方法 timeoutHandle
  fallbackMethod = "timeoutHandle",commandProperties = {
  // 下面这个指定的是超时阈值 
  @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
  })
public String simulationTimeOut(@PathVariable String id){
  String simulationTimeOut = simulationService.simulationTimeOut(id);
  return simulationTimeOut;
}

/**
     * 兜底方法,异常或者超时都会触发该方法
     * @param id
     * @return
     */
public String timeoutHandle(String id){
  return "thread:" + Thread.currentThread().getName() + "  request fail, id:" + id;
}

调用方调用simulationtimeOut方法后,因为发生异常,所以会执行兜底的方法,会返回thread:hystrix-SimulationController-2 request fail, id:12。从线程名也能看出Hystrix也是创建新的线程来执行兜底方法的。

全局fallback

使用@DefaultProperties(defaultFallback="")在调用类头部添加上来执行默认的兜底方法,如果@HystrixCommand指定了fallback的话就执行它指定的,如果没有,就执行默认的,避免了代码的膨胀,合理的减少了代码量,让通用的和独享的各自分开。

@RestController
@DefaultProperties(defaultFallback = "globalHandle")
public class SimulationController {

    @Autowired
    SimulationService simulationService;

    @GetMapping("/consume/hystrix/success/{id}")
    @HystrixCommand
    public String simulationSuccess(@PathVariable String id){
        String simulationSuccess = simulationService.simulationSuccess(id);
        return simulationSuccess;
    }
    @GetMapping("/consume/hystrix/timeout/{id}")
    @HystrixCommand(
            // 绑定兜底方法 timeoutHandle
            fallbackMethod = "timeoutHandle",commandProperties = {
            // 下面这个指定的是超时阈值
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
    })
    public String simulationTimeOut(@PathVariable String id){
        String simulationTimeOut = simulationService.simulationTimeOut(id);
        return simulationTimeOut;
    }

    /**
     * 兜底方法,异常或者超时都会触发该方法
     * @param id
     * @return
     */
    public String timeoutHandle(String id){
        return "thread:" + Thread.currentThread().getName() + "  request fail, id:" + id;
    }

    /**
     * 全局服务降级兜底的方法
     * @param id
     * @return
     */
    public String globalHandle(String id){
        return "thread:" + Thread.currentThread().getName() + "  globalHandle, id:" + id;
    }

}

feign的服务降级处理

该方式可以针对某个服务来编写服务降级处理,当面对服务宕机的时候,这个时候就会进行服务降级。

上面我们知道了80上基于openFeign是编写了接口类SimulationService,我们实现该接口编写SimulationServiceFallBack,这个类就可以统一的为这个类进行异常处理。

  • SimulationServiceFallBack

    @Component
    public class SimulationServiceFallBack implements SimulationService {
        @Override
        public String simulationSuccess(String id) {
            return "---simulationSuccess fail-----";
        }
    
        @Override
        public String simulationTimeOut(String id) {
            return "---simulationTimeOut fail-----";
        }
    }
    
  • 修改SimulationService

    @Component
    @FeignClient(value = "cloud-eureka-provide-userinfo-service",fallback = SimulationServiceFallBack.class)
    public interface SimulationService {
    
        @GetMapping("/simulation/hystrix/success/{id}")
        String simulationSuccess(@PathVariable("id") String id);
    
        @GetMapping("/simulation/hystrix/timeout/{id}")
        String simulationTimeOut(@PathVariable("id") String id);
    
    }
    

    这样就针对某个服务做了服务降级处理,这样的方式适用于Feign的服务调用的方式,只需要在编写的服务映射接口上绑定 fallback即可。这样的方式在不改动接口的情况下,又做了服务降级处理,高内聚低耦合,让代码更加的层次分明。让客户端在服务端不可用的情况下,也会获得提示,而不是耗死服务。

服务熔断

什么是断路器?

类比家里电路中的保险丝。

什么是熔断?

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。在Spring Cloud框架里,熔断机制通过Hystrix实现。 Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。 参考大神论文

实现案例

这里我们修改8081

  • simulationService

                                      ...
    /**
      * 下面是服务熔断测试方法
      * @param id
      * @return
      */
    @HystrixCommand(fallbackMethod = "circuitBreakerFallBack",commandProperties = {
    @HystrixProperty(name = "circuitBreaker.enabled",value = "true"), // 开启断路器
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), // 请求总数阈值
    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")  // 失败率达到60%的时候开启熔断
    })
    public String simulationCircuitBreaker(Integer id){
      if (id<0){
        throw new RuntimeException("id 必须大于等于 0");
      }
      return "simulationCircuitBreaker:"+ UUID.randomUUID().toString();
    }
    
    /**
      * 兜底方法
      * @param id
      * @return
      */
    public String circuitBreakerFallBack(Integer id){
      return "circuitBreakerFallBack:"+ UUID.randomUUID().toString();
    }
    
  • simulationController

                                          ...
    /**
      * 服务熔断测试
      * @param id
      * @return
      */
    @GetMapping("/simulation/hystrix/circuitBreaker/{id}")
    public String simulationCircuitBreaker(@PathVariable Integer id){
      String simulationCircuitBreaker = simulationService.simulationCircuitBreaker(id);
      return simulationCircuitBreaker;
    }
    

    我们使用@HystrixCommand来配置服务熔断,上面的配置可选项可参考HystrixCommandProperties这个类,该实例的配置的效果是10s内如果错误率达到60%会发生服务熔断,如果请求次数超过10次才会开启熔断

当我们请求http://localhost:8081/simulation/hystrix/circuitBreaker/1的时候,服务执行正常,当我们传入id为负数时,例如http://localhost:8081/simulation/hystrix/circuitBreaker/-10的时候,服务降级,执行circuitBreakerFallBack方法,当我们在短时间内,这种请求出错(降级)过多的时候,会发生服务熔断,这时我们再传入正数id,发现请求依然是降级的,但是发出正确请求一段时间之后,返回会正常,这就是服务的降级->进而熔断->恢复调用链路,也是Hystrix的强大之处。

熔断的类型

  • 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入熔断状态
  • 熔断关闭:熔断关闭不会对服务进行熔断
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断

官方流程

详细参考官网

请求流程

官网流程图
  1. 构造一个HystrixCommandHystrixObservableCommand对象
  2. 执行命令
  3. 响应是否已缓存?
  4. 电路开路了吗?
  5. 线程池/队列/信号量是否已满?
  6. HystrixObservableCommand.construct() 要么 HystrixCommand.run()
  7. 计算电路健康
  8. 获取后备
  9. 返回成功的回应

熔断流程

熔断流程图
  1. 假设电路上的音量达到某个阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())...
  2. 并假设误差百分比超过阈值误差百分比(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())...
  3. 然后,断路器从转换关闭开启
  4. 当它断开时,它会使针对该断路器的所有请求短路。
  5. 经过一段时间(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds())后,下一个单个请求被允许通过(这是半开状态)。如果请求失败,断路器将开启在睡眠窗口期间返回到该状态。如果请求成功,断路器将切换到,关闭状态并且再次循环步骤一。

可视化仪表盘

除了隔离依赖服务的调用以外,Hystrix还提供 了准实时的调用监控(HystrixIDashboard) ,Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。

搭建 DashBoard 模块

这里yml文件没有什么需要配置的,就是指定端口,这里我指定的端口是9000

pom


        
            org.springframework.cloud
            spring-cloud-starter-netflix-hystrix-dashboard
        
        
            org.springframework.boot
            spring-boot-starter-actuator
        

改启动

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashBoard9000 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashBoard9000.class,args);
    }
}

对监控服务的要求

所有需要监控的服务都需要配置spring-boot-starter-actuator依赖

监控服务改造

  • 添加依赖

    
                org.springframework.boot
                spring-boot-starter-actuator
    
    
  • 主启动上添加ServletRegistrationBean

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableCircuitBreaker
    public class EurekaHystrixProvideApplication8081 {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaHystrixProvideApplication8081.class,args);
        }
    
        /**
         * 该配置为了Hystrix服务监控,与服务本身无关,是springcloud升级后的坑
         * 主要因为ServletRegistrationBean中springboot的默认路径不是"/hystrix.stream"
         * @return
         */
        @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;
        }
    }
    

web配置

Hystrix DashBoard 服务搭建好之后,我们可以浏览器访问http://localhost:9000/hystrix打开

Hystrix 监控主页

地址栏输入需要监控的服务地址+/hystrix.stream,例如http://localhost:8081/hystrix.stream

输入监控信息

点击Monitor Stream进入监控页面

监控页面

自此,Hystrix的大部分功能就叙述完了,更多信息可以 参考官网

你可能感兴趣的:(玩转Hystrix一篇就够了)