熔断器 Hystrix 的原理与使用

前言

       将单体应用迁移到分布式框架后,很大可能会遇到这样的问题:系统仅有一个控制单元,它会调用多个运算单元,如果某一个运算单元(作为服务提供者)不可用,将导致控制单元(作为服务调用者)被阻塞,最终导致控制单元崩溃,进而导致整个系统都面临着瘫痪的风险。

服务雪崩效应形成的原因

我把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:

  1. 服务提供者不可用

  2. 重试加大流量

  3. 服务调用者不可用

服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:

  • 硬件故障

  • 程序Bug

  • 缓存击穿

  • 用户大量请求

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

而形成 重试加大流量 的原因有:

  • 用户重试

  • 代码逻辑重试

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

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

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

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

服务雪崩的应对策略

针对造成服务雪崩的不同原因, 可以使用不同的应对策略:

  1. 流量控制

  2. 改进缓存模式

  3. 服务自动扩容

  4. 服务调用者降级服务

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

  • 网关限流

  • 用户交互限流

  • 关闭重试

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

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

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

  • 缓存预加载

  • 同步改为异步刷新

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

  • AWS的auto scaling

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

  • 资源隔离

  • 对依赖服务进行分类

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

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

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

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

服务化后面临的挑战:

  • 服务管理:敏捷迭代后的微服务可能越来越多,各个业务系统之间的交互也越来越多,如何做高效集群通信方案也是问题。

  • 应用管理: 每个业务系统部署后对应着一个进程,进程可以启停。如果机器掉电或者宕机,如何做无缝切换都需要强大的部署管理机制。

  • 负载均衡:为应对大流量场景及提供系统可靠性,同一个业务系统也会做分布式部署即一个业务实例部署在多台机器上。如果某个业务系统挂掉,如何按需做自动伸缩分布式方案也需要考虑。

  • 问题定位:单体应用的日志集中在一起,出现问题定位很方便,而分布式环境的问题定界定位,日志分析都较为困难。

  • 雪崩问题:分布式系统都存在这样一个问题,由于网络的不稳定性,决定任何一个服务的可用性都不是 100% 的。当网络不稳定的时候,作为服务的提供者,自身可能会被拖死,导致服务调用者阻塞,最终可能引发雪崩效应。

  很多提高系统可用性的模式,其中非常重要的两条是:使用超时策略和使用熔断器

  • 超时策略:如果一个服务会被系统中的其它部分频繁调用,一个部分的故障可能会导致级联故障。例如,调用服务的操作可以配置为执行超时,如果服务未能在这个时间内响应,将回复一个失败消息。然而,这种策略可能会导致许多并发请求到同一个操作被阻塞,直到超时期限届满。这些阻塞的请求可能会存储关键的系统资源,如内存、线程、数据库连接等。因此,这些资源可能会枯竭,导致需要使用相同的资源系统的故障。在这种情况下,它将是优选的操作立即失败。设置较短的超时可能有助于解决这个问题,但是一个操作请求从发出到收到成功或者失败的消息需要的时间是不确定的。

  • 熔断器模式:熔断器的模式使用断路器来检测故障是否已得到解决,防止请求反复尝试执行一个可能会失败的操作,从而减少等待纠正故障的时间,相对与超时策略更加灵活。

雪崩问题的本质:

  考虑到应用容器的线程数目基本都是固定的(比如Tomcat的线程池默认200),当在高并发的情况下,如果某一外部依赖的服务(第三方系统或者自研系统出现故障)超时阻塞,就有可能使得整个主线程池被占满,增加内存消耗,这是长请求拥塞反模式(一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式)。

       更进一步,如果线程池被占满,那么整个服务将不可用,就又可能会重复产生上述问题。因此整个系统就像雪崩一样,最终崩塌掉。

雪崩效应产生的几种场景

  • 流量激增:比如异常流量、用户重试导致系统负载升高;

  • 缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;

  • 程序有Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;

  • 硬件故障:比如宕机,机房断电,光纤被挖断等。

  • 线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;

    常见解决方案

    针对上述雪崩情景,有很多应对方案,但没有一个万能的模式能够应对所有场景。

  • 针对流量激增,采用自动扩缩容以应对突发流量,或在负载均衡器上安装限流模块。

  • 针对缓存刷新,参考Cache应用中的服务过载案例研究

  • 针对硬件故障,多机房容灾,跨机房路由,异地多活等。

  • 针对同步等待,使用Hystrix做故障隔离,熔断器机制等可以解决依赖服务不可用的问题。

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

  • 网关限流

  • 用户交互限流

  • 关闭重试

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

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

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

  • 缓存预加载

  • 同步改为异步刷新

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

  • 资源隔离

  • 对依赖服务进行分类

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

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

服务雪崩的过程

熔断器 Hystrix 的原理与使用

一组简单的服务依赖关系A,B服务同时依赖于基础服务C,基础服务C又调用了服务D

熔断器 Hystrix 的原理与使用

  服务D是一个辅助类型服务,整个业务不依赖于D服务,某天D服务突然响应时间变长,导致核心服务C响应时间变长,其上请求越积越多,C服务也出现响应变慢的情况,由于A,B强依赖于服务C,故而一个无关紧要的服务却影响整个系统的可用。

熔断器 Hystrix 的原理与使用

  

                              影响了整个系统

  雪崩是系统中的蝴蝶效应导致其发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。从源头上无法完全杜绝雪崩源头的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以可以提前评估,做好熔断隔离限流

  熔断器

  第一时间会想到Hystrix。下面来看下熔断器的实现原理

熔断器 Hystrix 的原理与使用

  服务的健康状况 = 请求失败数 / 请求总数,

    熔断器实际上是一个简单的有限状态机(Finite State Machine)

1.请求错误率达到某一阈值,熔断器全开,产生熔断(熔断期间会对所有请求采用降级处理)

2.到熔断时间窗口之后,熔断器会进入半开状态,此时hystrix会放过1个试验性请求

3.如果该试验性请求成功,熔断器进入关闭状态

4.如果该试验性请求失败,熔断器重新进入全开状态

  hystrix官方文档

1.Assuming the volume across a circuit meets a certain threshold (HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())...
2.And assuming that the error percentage exceeds the threshold error percentage 
(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())...
3.Then the circuit-breaker transitions from CLOSED to OPEN.
4.While it is open, it short-circuits all requests made against that circuit-breaker.
5.After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next single request
 is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for 
the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.

  资源隔离
 

  

熔断器 Hystrix 的原理与使用

  船舱分开设计本身就是一种隔离的思想,一个防水仓进水不会导致整艘轮船沉没。如果把整个系统比作海上漂浮的一艘轮船,那么系统的各个服务就好比轮船上的各个密封舱,服务A如果强依赖于服务B那么他们就在一个舱里

  应用界别隔离手段有线程池隔离,信号量隔离,连接池隔离,hystrix实现前2种,其各自优缺点如下

  

熔断器 Hystrix 的原理与使用

  

  在一个高度服务化的系统中,实现的一个业务逻辑通常会依赖多个服务,比如: 商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示:

  

熔断器 Hystrix 的原理与使用

  调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩. 如图所示:

  

熔断器 Hystrix 的原理与使用

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

  

熔断器 Hystrix 的原理与使用

命令模式

  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();
  }
}

  Hystrix有两个请求命令 HystrixCommand、HystrixObservableCommand。

  HystrixCommand用在依赖服务返回单个操作结果的时候。又两种执行方式

    -execute():同步执行。从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。

    -queue();异步执行。直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。

       

public class CommandHelloWorld extends HystrixCommand<String> {

    private String name;

    public CommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("HelloGroup"));
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        return "Hello " + name;
    }

    /**
     * 降级。Hystrix会在run()执行过程中出现错误、超时、线程池拒绝、断路器熔断等情况时,
     * 执行getFallBack()方法内的逻辑
     */
    @Override
    protected String getFallback() {
        return "fail";
    }
}

测试:

/**
     * 同步执行
     */
    @Test
    public void testHystrixCommand() {
        CommandHelloWorld commandHelloWorld = new CommandHelloWorld("world");
        String result = commandHelloWorld.execute();
        log.info("------{}--------", result);
    }

    /**
     * 异步执行
     */
    @Test
    public void testHystrixAysncCommand() throws ExecutionException, InterruptedException {
        CommandHelloWorld commandHelloWorld = new CommandHelloWorld("world");
        log.info("{}", commandHelloWorld.queue().get());
    }

    /**
     * HystrixCommand具备了observe()和toObservable()的功能,但是它的实现有一定的局限性,
     * * 它返回的Observable只能发射一次数据,所以Hystrix还提供了HystrixObservableCommand,
     * * 通过它实现的命令可以获取能发多次的Observable
     */
    @Test
    public void testObserve() {
        /**
         * 返回的是Hot Observable,HotObservable,不论 “事件源” 是否有“订阅者”
         * 都会在创建后对事件进行发布。所以对于Hot Observable的每一个“订阅者”都有
         * 可能从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程
         */
        Observable<String> observe = new CommandHelloWorld("World").observe();
//        log.info("{}", observe.toBlocking().single()); //同步

        observe.subscribe(new Observer<String>() {
            @Override
            public void onCompleted() {
                log.info("==========completed============");
            }

            @Override
            public void one rror(Throwable e) {
                e.printStackTrace();
            }

            @Override
            public void onNext(String s) {
                log.info("======{}=========", s);
            }
        });

        observe.subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                log.info("=========={}", s);
            }
        });

    }

    @Test
    public void testToObservable() {
        /**
         * Cold Observable在没有 “订阅者” 的时候并不会发布时间,
         * 而是进行等待,知道有 “订阅者” 之后才发布事件,所以对于
         * Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程。
         */
        Observable<String> observable = new CommandHelloWorld("World").toObservable();
        log.info(observable.toBlocking().single());
    }

注解方式

@Service
public class UserService {

    @HystrixCommand(fallbackMethod = "helloFallback")
    public String getUserById(String name) {
        return "hello " + name;
    }

    public String helloFallback(String name) {
        return "error " + name;
    }

    @HystrixCommand(fallbackMethod = "getUserNameError")
    public Future<String> getUsername(Long id) {
        AsyncResult<String> asyncResult = new AsyncResult<String>() {
            @Override
            public String invoke() {
                return "名 " + id;
            }
        };
        return asyncResult;
    }

    public String getUserNameError(Long id) {
        return "failed";
    }
}

测试
    @Test
    public void testGetUserById() throws ExecutionException, InterruptedException {
        log.info("==========={}", userService.getUserById("明明"));
        log.info("-----------{}", userService.getUsername(0000007L).get());
    }

  HystrixObservableCommand 用在依赖服务返回多个操作结果的时候。实现了两种执行方式

    -observe():返回Obervable对象,代表了操作的多个结果,是一个HotObservable

    -toObservable():同样返回Observable对象,也代表了操作多个结果,但它返回的是一个Cold Observable。

       HystrixCommand逻辑在run中,HystrixObservableCommand的逻辑在construct中,observe方法触发run是非阻塞的方式,就是新的线程执行run,而触发construct则是阻塞方式,就是调用线程执行construct。toObserve方法本身不会触发run或者construct方法,而是要在subscribe的时候触发这run或者construct方法。触发的方式和observe一样,对于run使用新线程非阻塞的方式,对于construct使用调用线程阻塞的方式。

  observe不管有没有订阅者都会执行run或者construct,toObserve只有有订阅者的时候才会执行run或者construct的方法。

public class ObservableCommandHelloWorld extends HystrixObservableCommand<String> {

    private String name;

    public ObservableCommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("observableGroup"));
        this.name = name;
    }

    @Override
    protected Observable<String> construct() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                if (!subscriber.isUnsubscribed()) {
                    subscriber.onNext("Hello ");
                    subscriber.onNext("Name ");
                    subscriber.onCompleted();
                }
            }
        }).subscribeOn(Schedulers.io());
    }

    @Override
    protected Observable<String> resumeWithFallback() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                try {
                    if (!subscriber.isUnsubscribed()) {
                        subscriber.onNext("失败了!");
                        subscriber.onCompleted();
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        }).subscribeOn(Schedulers.io());
    }
}

测试
    @Test
    public void testObservable() {
        Observable<String> observable = new ObservableCommandHelloWorld("World").observe();
        Iterator<String> iterator = observable.toBlocking().getIterator();
        while (iterator.hasNext()) {
            log.info("--" + iterator.next());
        }
    }

注解方式 

@Service
public class ObservableUserService {
    /**
     *  EAGER参数表示使用observe()方式执行
     */
    @HystrixCommand(observableExecutionMode = ObservableExecutionMode.EAGER, fallbackMethod = "observFailed") //使用observe()执行方式
    public Observable<String> getUserById(final Long id) {
       return Observable.create(new Observable.OnSubscribe<String>() {
           @Override
           public void call(Subscriber<? super String> subscriber) {
               try {
                   if(!subscriber.isUnsubscribed()) {
                       subscriber.onNext("张三的ID:");
                       int i = 1 / 0; //抛异常,模拟服务降级
                       subscriber.onNext(String.valueOf(id));
                       subscriber.onCompleted();
                   }
               } catch (Exception e) {
                   subscriber.onError(e);
               }
           }
       });
    }

    private String observFailed(Long id) {
        return "observFailed---->" + id;
    }

    /**
     * LAZY参数表示使用toObservable()方式执行
     */
    @HystrixCommand(observableExecutionMode = ObservableExecutionMode.LAZY, fallbackMethod = "toObserbableError") //表示使用toObservable()执行方式
    public Observable<String> getUserByName(final String name) {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                try {
                    if(!subscriber.isUnsubscribed()) {
                        subscriber.onNext("找到");
                        subscriber.onNext(name);
                        int i = 1/0; 抛异常,模拟服务降级
                        subscriber.onNext("了");
                        subscriber.onCompleted();
                    }
                } catch (Exception e) {
                    subscriber.onError(e);
                }
            }
        });
    }

    private String toObserbableError(String name) {
        return "toObserbableError--->" + name;
    }
}

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

  

熔断器 Hystrix 的原理与使用

上一篇:Spring 响应式编程 随记 -- C2 Spring 响应式编程基本概念 (三)


下一篇:总结1