防雪崩利器:熔断器 Hystrix 的原理与使用

前言

分布式系统中常常会出现某个基础服务不可用形成整个系统不可用的状况, 这种现象被称为服务雪崩效应. 为了应对服务雪崩, 一种常见的作法是手动服务降级. 而Hystrix的出现,给咱们提供了另外一种选择.java

服务雪崩效应的定义

服务雪崩效应是一种因 服务提供者 的不可用致使 服务调用者 的不可用,并将不可用 逐渐放大 的过程git

服务雪崩效应造成的缘由

我把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为如下三个阶段来分析造成的缘由:github

  1. 服务提供者不可用web

  2. 重试加大流量spring

  3. 服务调用者不可用apache

服务雪崩的每一个阶段均可能由不一样的缘由形成, 好比形成 服务不可用 的缘由有:segmentfault

  • 硬件故障后端

  • 程序Bug缓存

  • 缓存击穿tomcat

  • 用户大量请求

硬件故障可能为硬件损坏形成的服务器主机宕机, 网络硬件故障形成的服务提供者的不可访问. 
缓存击穿通常发生在缓存应用重启, 全部缓存被清空时,以及短期内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,形成服务提供者超负荷运行,引发服务不可用. 
在秒杀和大促开始前,若是准备不充分,用户发起大量请求也会形成服务提供者的不可用.

而造成 重试加大流量 的缘由有:

  • 用户重试

  • 代码逻辑重试

在服务提供者不可用后, 用户因为忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.
服务调用端的会存在大量服务异常后的重试逻辑. 
这些重试都会进一步加大请求流量.

最后, 服务调用者不可用 产生的主要缘由是:

  • 同步等待形成的资源耗尽

当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 因而服务雪崩效应产生了.

服务雪崩的应对策略

针对形成服务雪崩的不一样缘由, 可使用不一样的应对策略:

  1. 流量控制

  2. 改进缓存模式

  3. 服务自动扩容

  4. 服务调用者降级服务

流量控制 的具体措施包括:

  • 网关限流

  • 用户交互限流

  • 关闭重试

由于Nginx的高性能, 目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制, 由此而来的OpenResty也愈来愈热门.

用户交互限流的具体措施有: 1. 采用加载动画,提升用户的忍耐等待时间. 2. 提交按钮添增强制等待时间机制.

改进缓存模式 的措施包括:

  • 缓存预加载

  • 同步改成异步刷新

服务自动扩容 的措施主要有:

  • AWS的auto scaling

服务调用者降级服务 的措施包括:

  • 资源隔离

  • 对依赖服务进行分类

  • 不可用服务的调用快速失败

资源隔离主要是对调用服务的线程池进行隔离.

咱们根据具体业务,将依赖服务分为: 强依赖和若依赖. 强依赖服务不可用会致使当前业务停止,而弱依赖服务的不可用不会致使当前业务的停止.

不可用服务的调用快速失败通常经过 超时机制熔断器 和熔断后的 降级方法 来实现.

使用Hystrix预防服务雪崩

Hystrix [hɪst'rɪks]的中文含义是豪猪, 因其背上长满了刺,而拥有自我保护能力. Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它一样拥有保护系统的能力.

Hystrix的设计原则包括:

  • 资源隔离

  • 熔断器

  • 命令模式

资源隔离

货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个,

种资源隔离减小风险的方式被称为:Bulkheads(舱壁隔离模式). 
Hystrix将一样的模式运用到了服务调用者上.

在一个高度服务化的系统中,咱们实现的一个业务逻辑一般会依赖多个服务,好比: 
商品详情展现服务会依赖商品服务, 价格服务, 商品评论服务

调用三个依赖服务会共享商品详情服务的线程池. 若是其中的商品评论服务不可用, 就会出现线程池里全部线程都因等待响应而被阻塞, 从而形成服务雪崩

Hystrix经过将每一个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩. 
当商品评论服务不可用时, 即便商品服务独立分配的20个线程所有处于同步等待状态,也不会影响其余依赖服务的调用.

熔断器模式

服务的健康情况 = 请求失败数 / 请求总数. 
熔断器开关由关闭到打开的状态转换是经过当前服务健康情况和设定阈值比较决定的.

  1. 当熔断器开关关闭时, 请求被容许经过熔断器. 若是当前健康情况高于设定阈值, 开关继续保持关闭. 若是当前健康情况低于设定阈值, 开关则切换为打开状态.

  2. 当熔断器开关打开时, 请求被禁止经过.

  3. 当熔断器开关处于打开状态, 通过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只容许一个请求经过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止经过.

熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 而且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.

命令模式

Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).
同时咱们在Command的构造方法中能够定义当前服务线程池和熔断器的相关参数. 以下代码所示:

public class Service1HystrixCommand extends HystrixCommand<Response> {
  private Service1 service;
  private Request request;

  public Service1HystrixCommand(Service1 service, Request request){
    supper(
      Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"))
          .andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query"))
          .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool"))
          .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
            .withCoreSize(20))//服务线程池数量
          .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
            .withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
            .withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
      ))
      this.service = service;
      this.request = request;
    );
  }

  @Override
  protected Response run(){
    return service1.call(request);
  }

  @Override
  protected Response getFallback(){
    return Response.dummy();
  }
}

在使用了Command模式构建了服务对象以后, 服务便拥有了熔断器和线程池的功能. 

Hystrix的内部处理逻辑

  1. 构建Hystrix的Command对象, 调用执行方法.

  2. Hystrix检查当前服务的熔断器开关是否开启, 若开启, 则执行降级服务getFallback方法.

  3. 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求, 若超过线程池已满, 则执行降级服务getFallback方法.

  4. 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法.

  5. 若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康情况.

  6. 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康情况.

  7. 若服务执行成功, 返回正常结果.

  8. 若服务降级方法getFallback执行成功, 则返回降级结果.

  9. 若服务降级方法getFallback执行失败, 则抛出异常.

Hystrix Metrics的实现

Hystrix的Metrics中保存了当前服务的健康情况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑. 所以Metrics的实现很是重要.

1.4以前的滑动窗口实现

Hystrix在这些版本中的使用本身定义的滑动窗口数据结构来记录当前时间窗的各类事件(成功,失败,超时,线程池拒绝等)的计数.
事件产生时, 数据结构根据当前时间肯定使用旧桶仍是建立新桶来计数, 并在桶中对计数器经行修改. 
这些修改是多线程并发执行的, 代码中有很多加锁操做,逻辑较为复杂.

1.5以后的滑动窗口实现

Hystrix在这些版本中开始使用RxJava的Observable.window()实现滑动窗口.
RxJava的window使用后台线程建立新桶, 避免了并发建立桶的问题.
同时RxJava的单线程无锁特性也保证了计数变动时的线程安全. 从而使代码更加简洁. 
如下为我使用RxJava的window方法实现的一个简易滑动窗口Metrics, 短短几行代码便能完成统计功能,足以证实RxJava的强大:

@Test
public void timeWindowTest() throws Exception{
  Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
  source.window(1, TimeUnit.SECONDS).subscribe(window -> {
    int[] metrics = new int[2];
    window.subscribe(i -> metrics[i]++,
      InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
      () -> System.out.println("窗口Metrics:" + JSON.toJSONString(metrics)));
  });
  TimeUnit.SECONDS.sleep(3);
}

简单测试:

引入jar包:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
            <version>1.4.3.RELEASE</version>
        </dependency>

导入配置:

server:
  port: 11111
  
#default可替换
hystrix:
  command:
    default:
      execution:
        isolation:
          #线程池隔离仍是信号量隔离 默认是THREAD 信号量是SEMAPHORE
          strategy: THREAD
          semaphore:
            #使用信号量隔离时,支持的最大并发数 默认10
            maxConcurrentRequests: 10
          thread:
            #command的执行的超时时间 默认是1000
            timeoutInMilliseconds: 1000
            #HystrixCommand.run()执行超时时是否被打断 默认true
            interruptOnTimeout: true
            #HystrixCommand.run()被取消时是否被打断 默认false
            interruptOnCancel: false
        timeout:
          #command执行时间超时是否抛异常 默认是true
          enabled: true
        fallback:
          #当执行失败或者请求被拒绝,是否会尝试调用hystrixCommand.getFallback()
          enabled: true
          isolation:
            semaphore:
              #若是并发数达到该设置值,请求会被拒绝和抛出异常而且fallback不会被调用 默认10
              maxConcurrentRequests: 10
      circuitBreaker:
        #用来跟踪熔断器的健康性,若是未达标则让request短路 默认true
        enabled: true
        #一个rolling window内最小的请求数。若是设为20,那么当一个rolling window的时间内
        #(好比说1个rolling window是10秒)收到19个请求,即便19个请求都失败,也不会触发circuit break。默认20
        requestVolumeThreshold: 5
        # 触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内
        #都会拒绝request,也就是5000毫秒后才会关闭circuit,放部分请求过去。默认5000
        sleepWindowInMilliseconds: 5000
        #错误比率阀值,若是错误率>=该值,circuit会被打开,并短路全部请求触发fallback。默认50
        errorThresholdPercentage: 50
        #强制打开熔断器,若是打开这个开关,那么拒绝全部request,默认false
        forceOpen: false
        #强制关闭熔断器 若是这个开关打开,circuit将一直关闭且忽略
        forceClosed: false
      metrics:
        rollingStats:
          #设置统计的时间窗口值的,毫秒值,circuit break 的打开会根据1个rolling window的统计来计算。若rolling window被设为10000毫秒,
          #则rolling window会被分红n个buckets,每一个bucket包含success,failure,timeout,rejection的次数的统计信息。默认10000
          timeInMilliseconds: 10000
          #设置一个rolling window被划分的数量,若numBuckets=10,rolling window=10000,
          #那么一个bucket的时间即1秒。必须符合rolling window % numberBuckets == 0。默认10
          numBuckets: 10
        rollingPercentile:
          #执行时是否enable指标的计算和跟踪,默认true
          enabled: true
          #设置rolling percentile window的时间,默认60000
          timeInMilliseconds: 60000
          #设置rolling percentile window的numberBuckets。逻辑同上。默认6
          numBuckets: 6
          #若是bucket size=100,window=10s,若这10s里有500次执行,
          #只有最后100次执行会被统计到bucket里去。增长该值会增长内存开销以及排序的开销。默认100
          bucketSize: 100
        healthSnapshot:
          #记录health 快照(用来统计成功和错误绿)的间隔,默认500ms
          intervalInMilliseconds: 500
      requestCache:
        #默认true,须要重载getCacheKey(),返回null时不缓存
        enabled: true
      requestLog:
        #记录日志到HystrixRequestLog,默认true
        enabled: true
  collapser:
    default:
      #单次批处理的最大请求数,达到该数量触发批处理,默认Integer.MAX_VALUE
      maxRequestsInBatch: 2147483647
      #触发批处理的延迟,也能够为建立批处理的时间+该值,默认10
      timerDelayInMilliseconds: 10
      requestCache:
        #是否对HystrixCollapser.execute() and HystrixCollapser.queue()的cache,默认true
        enabled: true
  threadpool:
    default:
      #并发执行的最大线程数,默认10
      coreSize: 10
      #Since 1.5.9 能正常运行command的最大支付并发数
      maximumSize: 10
      #BlockingQueue的最大队列数,当设为-1,会使用SynchronousQueue,值为正时使用LinkedBlcokingQueue。
      #该设置只会在初始化时有效,以后不能修改threadpool的queue size,除非reinitialising thread executor。
      #默认-1。
      maxQueueSize: -1
      #即便maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝。
      #由于maxQueueSize不能被动态修改,这个参数将容许咱们动态设置该值。if maxQueueSize == -1,该字段将不起做用
      queueSizeRejectionThreshold: 5
      #Since 1.5.9 该属性使maximumSize生效,值须大于等于coreSize,当设置coreSize小于maximumSize
      allowMaximumSizeToDivergeFromCoreSize: false
      #若是corePoolSize和maxPoolSize设成同样(默认实现)该设置无效。
      #若是经过plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定义实现,该设置才有用,默认1.
      keepAliveTimeMinutes: 1
      metrics:
        rollingStats:
          #线程池统计指标的时间,默认10000
          timeInMilliseconds: 10000
          #将rolling window划分为n个buckets,默认10
          numBuckets: 10

其中execution:isolation:strategy有些区别:

       资源隔离。就是多个依赖服务的调用分别隔离到各自本身的资源池内。避免说对一个依赖服务的调用,由于依赖服务接口调用的失败或者延迟,致使全部的线程资源
都所有耗费在这个接口上。一旦某个服务的线程资源所有耗尽量致使服务的崩溃,甚至故障蔓延。    2.资源隔离的方法
       信号量semaphore,最多能容纳10个请求。一旦超过10个信号量最大容量,那么就会拒绝其余请求。
信号量与线程池资源隔离的区别:
      线程池隔离技术并不是控制tomcat等web容器的线程。更准确的说就是控制tomcat线程的执行。tomcat接到请求以后会调用hystrix线程池的线程去执行。当线程池满了以后会调用fallback降级。
tomcat其余的线程不会卡死,快速返回,而后能够支撑其余事情。同时hystrix处理timeout超时问题。
    信号量隔离只是一个关卡,经过个人关卡的线程是固定的。容量满了以后。fallback降级。
    区别:线程池隔离技术是用本身的线程去执行调用。信号量是直接让tomcat线程去执行依赖服务。

上图是默认的配置,咱们能够对本身的配置进行分组:

    针对不一样的组在配置文件里面加上不一样的配置就行了,在@MyCommand注解里面指定group为abc就行;其余的配置也是这个规则,还有默认的配置是default;这样能够把一个组的配置独立出来,便于配置,并且开发者也会方便不少,代码简洁;

下面是代码:

package cn.chinotan.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: hystrix控制器
 * @author: xingcheng
 * @create: 2018-11-03 19:27
 **/
@RestController
@RequestMapping("/hystrix")
public class HystrixController {

    @HystrixCommand(fallbackMethod = "helloFallback")
    @RequestMapping("/sayHello")
    public String sayHello(String name, Integer time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello, " + name;
    }

    @HystrixCommand(fallbackMethod = "hiFallback")
    @RequestMapping("/sayHi")
    public String sayHi(String name) {
        if (StringUtils.isBlank(name)) {
            throw new RuntimeException("name不能为空");
        }
        return "Good morning, " + name;
    }

    /**
     * fallback
     */
    public String helloFallback(String name, Integer time) {
        System.out.println("helloFallback: " + name);
        return "helloFallback" + name;
    }

    /**
     * fallback
     */
    public String hiFallback(String name) {
        System.out.println("hiFallback: " + name);
        return "hiFallback" + name;
    }
}
package cn.chinotan.config;

import com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @program: test
 * @description: HystrixConfig
 * @author: xingcheng
 **/
@Configuration
public class HystrixConfig {

    /**
     * 用来拦截处理HystrixCommand注解
     * @return
     */
    @Bean
    public HystrixCommandAspect hystrixAspect() {
        return new HystrixCommandAspect();
    }

    /**
     * 用来像监控中心Dashboard发送stream信息
     * @return
     */
    @Bean
    public ServletRegistrationBean hystrixMetricsStreamServlet() {
        ServletRegistrationBean registration = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
        registration.addUrlMappings("/hystrix.stream");
        return registration;
    }
    
}

配置监控后台Hystrix-Dashboard

1.github上下载源码https://github.com/kennedyoliveira/standalone-hystrix-dashboard

2.参考其wiki文档,部署成功后,默认端口是7979;

3.点击 http://127.0.0.1:7979/hystrix-dashboard 打开页面

能够看到sayHello接口请求3次成功,熔断器如今是关闭状态,当咱们调整time参数,使得接口超时后,看看接口的返回:

能够看到接口返回Fallback方法的内容,证实接口超时后跳到这个方法中去了,可是熔断器尚未打开,接下来进行屡次频率高的接口访问:

能够看到熔断器打开了,此时接口会很快就返回失败回调方法的内容,若是过一会再次请求这个接口,time参数变小于超时时间,结果以下:

能够看到熔断器又关闭了,接口能够正常访问,这是为何呢:

能够看到这个参数,rolling window内最小的请求数。若是设为20,那么当一个rolling window的时间内好比说1个rolling window是10秒)收到19个请求,即便19个请求都失败,也不会触发circuit break。默认20

总结

  • 雪崩效应缘由:硬件故障、硬件故障、程序Bug、重试加大流量、用户大量请求。
  • 雪崩的对策:限流、改进缓存模式(缓存预加载、同步调用改异步)、自动扩容、降级。
  • Hystrix设计原则:
    • 资源隔离:Hystrix经过将每一个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩。
    • 熔断开关:服务的健康情况 = 请求失败数 / 请求总数,经过阈值设定和滑动窗口控制开关。
    • 命令模式:经过继承 HystrixCommand 来包装服务调用逻辑。

参考文章:https://segmentfault.com/a/1190000005988895

相关文章
相关标签/搜索