Hystrix断路器原理深度解析

1. HystrixCircuitBreaker

Hystrix的熔断器实现在HystrixCircuitBreake中,此接口比较直观明了。整个HystrixCircuitBreaker接口一共有五个方法和三个静态类:
断路器
接口中的类:

  • Factory: 维护了一个Hystrix命令和HystrixCircuitBreaker的关系的集合ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand。其中key通过HystrixCommandKey来定义,每一个Hystrix 命令都需要有一个Key来标识,同时根据这个Key可以找到对应的断路器实例。
  • NoOpCircuitBreaker: 一个啥都不做的断路器,它允许所有请求通过,并且断路器始终处于闭合状态
  • HystrixCircuitBreakerImpl:断路器的一个实现类。

接口中的方法

  • allowRequest()方法表示是否允许指令执行,该方法在新版本中放弃
  • isOpen()方法表示断路器是否为开启状态
  • markSuccess()处于半开状态时,如果尝试请求成功,就调用这个方法。用于将断路器关闭
  • markNonSuccess()处于半开状态时,如果尝试请求成功。 用来打开断路器
  • attemptExecution()判断每个Hystrix命令的请求都通过它是否被执行。这是非幂等的,它可能会修改内部。旧版本中是allowRequest()。
2. 断路器Factory

Factory静态类相当于Circuit Breaker Factory,用于获取相应的HystrixCircuitBreaker。我们来看一下其实现:

class Factory {
    private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap<String, HystrixCircuitBreaker>();
 
    public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
        // this should find it for all but the first time
        HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
        if (previouslyCached != null) {
            return previouslyCached;
        }
        HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
        if (cbForCommand == null) {
            return circuitBreakersByCommand.get(key.name());
        } else {
            return cbForCommand;
        }
    }
    public static HystrixCircuitBreaker getInstance(HystrixCommandKey key) {
        return circuitBreakersByCommand.get(key.name());
    }
 	static void reset() {
        circuitBreakersByCommand.clear();
    }
}

简单的说就是Hystrix为每个commandKey都维护了一个熔断器,保持着对应的熔断器,所以当new XXXHystrixCommand()的时候依然能够保持着原来熔断器的状态。

正如前面分析,Hystrix在Factory类中维护了一个ConcurrentHashMap用于存储与每一个HystrixCommandKey相对应的HystrixCircuitBreaker。每当我们通过getInstance方法从中获取HystrixCircuitBreaker的时候,Hystrix首先会检查ConcurrentHashMap中有没有对应的缓存的断路器,如果有的话直接返回。如果没有的话就会新创建一个HystrixCircuitBreaker实例,将其添加到缓存中并且返回。关于断路器的实例化细节后面分析。

至于断路器的初始化时机,在Hystrix执行原理分析中提到过,在Hystrix实例化的时候,AbstractHystrix的构造函数中会调用断路器的Factory的上面的getInstance方法,一切都是非常合理自然~,下面看断路器的实现类。

3. 实现类HystrixCircuitBreakerImpl

先看实现类的代码:

class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
        private final HystrixCommandProperties properties;
        private final HystrixCommandMetrics metrics;

        enum Status {
            CLOSED, OPEN, HALF_OPEN;
        }

        private final AtomicReference<Status> status = new AtomicReference<Status>(Status.CLOSED);
        private final AtomicLong circuitOpened = new AtomicLong(-1);
        private final AtomicReference<Subscription> activeSubscription = new AtomicReference<Subscription>(null);

        protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, final HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
            this.properties = properties;
            this.metrics = metrics;
            //On a timer, this will set the circuit between OPEN/CLOSED as command executions occur
            Subscription s = subscribeToStream();
            activeSubscription.set(s);
        }
}

在该实现类中定义了断路器的五个核心属性:

  • HystrixCommandProperties properties:断路器对应实例的属性集合对象
  • HystrixCommandMetrics metrics:断路器最核心的东西,通过时间窗的形式,记录一段时间范围内(默认是10秒)的接口请求的健康状况(Command的执行状态,包括成功、失败、超时、线程池拒绝等)并得到对应的度量指标。
  • AtomicReference status: 用来记录断路器的状态,默认是关闭状态
    • 断路器在状态变化时,使用了AtomicReference#compareAndSet来确保当条件满足时,只有一个请求能成功改变状态。
  • AtomicLong circuitOpened:断路器打开的时间戳,用于判断休眠期的结束时间。默认-1,表示断路器未打开。
  • AtomicReference activeSubscription: 这个是通过Rxjava实现的对HystrixCommandMetrics结果的观察者对象,当HystrixCommandMetrics值发生变化时会通知观察者。

HystrixCommandProperties properties该属性保存了断路器对应Command的所有属性。和线程池类似,HystrixCommandProperties也是保存到HashMap中,key为Command的key。下面主要介绍远断路器相关的属性。

  1. circuitBreakerEnabled:熔断器是否启用,默认是true
  2. circuitBreakerErrorThresholdPercentage 错误率阈值,默认50%,比如(10s)内有100个请求,其中有60个发生异常,那么这段时间的错误率是60,已经超过了错误率阈值,熔断器会自动打开。
  3. circuitBreakerForceOpen:熔断器强制打开(可以强制设定状态),始终保持打开状态,默认是false
  4. circuitBreakerForceClosed:熔断器强制关闭,始终保持关闭状态,默认是false
  5. circuitBreakerRequestVolumeThreshold:滑动窗口内(10s)的请求数阈值,只有达到了这个阈值,才有可能熔断。默认是20,如果这个时间段只有19个请求,就算全部失败了,也不会自动熔断。
  6. circuitBreakerSleepWindowInMilliseconds:熔断器打开之后,为了能够自动恢复,每隔默认5000ms放一个请求过去,试探所依赖的服务是否恢复。

断路器HystrixCircuitBreaker有三个状态:

  • CLOSED关闭状态:允许流量通过。
  • OPEN打开状态:不允许流量通过,即处于降级状态,走降级逻辑。
  • HALF_OPEN半开状态:允许某些流量通过,并关注这些流量的结果,如果出现超时、异常等情况,将进入OPEN状态,如果成功,那么将进入CLOSED状态。
4. 自动熔断

HystrixBreaker实例化的时候会订阅HystrixCommandMetrics的 HealthCountsStream,每当HealthCountsStream搜集到数据,都会触发 onNext方法,自动熔断就是这里发生。这个过程也可以叫做健康指标的订阅。

private Subscription subscribeToStream() {
    /*
     * This stream will recalculate the OPEN/CLOSED status on every onNext from the health stream
     */
    return metrics.getHealthCountsStream()
            .observe()
            .subscribe(new Subscriber<HealthCounts>() {
                @Override
                public void onNext(HealthCounts hc) {
                    //首先校验的时在时间窗范围内的请求次数,如果低于阈值(默认是20),不做处理,如果高于阈值,则去判断接口请求的错误率
                    if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
                        
                    } else {
						//判断接口请求的错误率(阈值默认是50),如果高于这个值,则断路器打开
                        if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
                            
                        } else {
                            //打开断路器,同时记录断路器开启时间
                            if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
                                circuitOpened.set(System.currentTimeMillis());
                            }
                        }
                    }
                }
            });
}

在onNext方法中,参数hc保存了当前接口在前10s之内(时间窗口)的请求状态(请求总数、失败数和失败率),其主要逻辑是判断请求总数是否达到阈值requestVolumeThreshold,失败率是否达到阈值errorThresholdPercentage,如果都满足,说明接口的已经足够的不稳定,需要进行熔断,则设置status为熔断开启状态,并更新circuitOpened为当前时间戳,记录上次熔断开启的时间。

5. 是否通过断路器

每一个Command在execute执行后都会判断断路器是否可以通过,此逻辑实现在AbstractCommand#applyHystrixSemantics中:

private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
	//...省略部分代码
 	if (circuitBreaker.attemptExecution()) {
	//...省略部分代码
}

新版本弃用了allowRequest(),取而代之的是attemptExecution()方法。正如方法名的含义,尝试执行。通过此方法的返回值决定执行正常逻辑,还是降级逻辑。

 @Override
public boolean attemptExecution() {
    if (properties.circuitBreakerForceOpen().get()) {
        return false;
    }
    if (properties.circuitBreakerForceClosed().get()) {
        return true;
    }
    if (circuitOpened.get() == -1) {
        return true;
    } else {
        if (isAfterSleepWindow()) {
            if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
                //only the first request after sleep window should execute
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}

此方法的实现也比较直观

  1. 如果circuitBreakerForceOpen=true,说明熔断器已经强制开启,所有请求都会被熔断。
  2. 如果circuitBreakerForceClosed =true,说明熔断器已经强制关闭,所有请求都会被放行。
  3. circuitOpened默认-1,用以保存最近一次发生熔断的时间戳。
    如果circuitOpened不等于 -1,说明已经发生熔断,通过isAfterSleepWindow()判断当前是否需要进行试探。判断逻辑是当前时间是否已经超过上次熔断的时间戳 + 试探窗口5000ms
// 睡眠窗口是否过去
private boolean isAfterSleepWindow() {
    final long circuitOpenTime = circuitOpened.get();
    final long currentTime = System.currentTimeMillis();
    final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
    return currentTime > circuitOpenTime + sleepWindowTime;
}
  1. 接下来进入if分支,通过compareAndSet修改变量status。只有睡眠窗口(5000ms)之后的第一个请求可以执行正常逻辑,且修改当前状态为HALF_OPEN,进入半熔断状态,其它请求执行compareAndSet(Status.OPEN, Status.HALF_OPEN)时都返回false,执行降级逻辑。

上述过程过程就是熔断器自动恢复的逻辑,总结一下:
当进入OPEN状态后,会进入一段睡眠窗口,即只会OPEN一段时间,所以这个睡眠窗口过去,就会从OPEN状态变成HALF_OPEN状态,这种设计是为了能做到弹性恢复,这种状态的变更,并不是由调度线程来做,而是由请求来触发。

5. HALF OPEN

HystrixCommand执行的过程中,当error的时候会触发circuitBreaker.markNonSuccess(),执行成功或者执行完成触发 circuitBreaker.markSuccess()。追溯源码,这两个方法的调用时发生在attemptExecution()调用之后。在
AbstractCommand#executeCommandAndObserve中,下面省略的部分干扰代码。

private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
	final Action1<R> markEmits = new Action1<R>() {
          @Override
          public void call(R r) {
                  circuitBreaker.markSuccess();
          }
      };
      final Action0 markOnCompleted = new Action0() {
          @Override
          public void call() {
                  circuitBreaker.markSuccess();
          }
      };
      final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
          @Override
          public Observable<R> call(Throwable t) {
              circuitBreaker.markNonSuccess();
		  }
	  };
      return execution.doOnNext(markEmits)
              .doOnCompleted(markOnCompleted)
              .onErrorResumeNext(handleFallback)
              .doOnEach(setRequestContext);
}

markSuccess中,若为半开状态时,会放第一个请求去执行,并跟踪它的执行结果,如果是成功,那么将由HALF_OPEN状态变成CLOSED状态,
markNonSuccess中,如果第一个被放去执行的请求执行失败(资源获取失败、异常、超时等),就会由HALP_OPEN状态再变为OPEN状态。

markNonSuccess(),通过compareAndSet修改status为熔断开启状态,并更新当前熔断开启的时间戳。

@Override
public void markNonSuccess() {
    if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
        //This thread wins the race to re-open the circuit - it resets the start time for the sleep window
        circuitOpened.set(System.currentTimeMillis());
    }
}

markSuccess()

 @Override
public void markSuccess() {
    if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
        //This thread wins the race to close the circuit - it resets the stream to start it over from 0
        metrics.resetStream();
        Subscription previousSubscription = activeSubscription.get();
        if (previousSubscription != null) {
            previousSubscription.unsubscribe();
        }
        Subscription newSubscription = subscribeToStream();
        activeSubscription.set(newSubscription);
        circuitOpened.set(-1L);
    }
}

如果断路器状态是半开并且成功设置为关闭状态时,执行以下步骤。1.重置度量指标对象,2.取消之前的订阅,发起新的订阅。3.设置断路器的打开时间为-1。

6. 滑动窗口原理

后面补充~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值