说明

原创不易,如若转载 请标明来源!

欢迎关注本人微信公众号:壹枝花算不算浪漫

更多内容也可查看本人博客:一枝花算不算浪漫

前言

前情回顾

上一讲我们讲解了Hystrix在配合feign的过程中,一个正常的请求逻辑该怎样处理,这里涉及到线程池的创建、HystrixCommand的执行等逻辑。

如图所示:

高清大图:https://www.processon.com/view/link/5e1c128ce4b0169fb51ce77e

本讲目录

这一讲开始讲解Hystrix的看家本领:熔断+降级。

熔断功能是Hystrix最核心的组件,当然也是最复杂的一块。

源码中细节太多,本讲我们主要还是专注于它的设计思想去学习。

目录如下:

  1. HystrixCircuitBreaker初始化过程
  2. Hystrix熔断机制(CLOSED/OPEN/HALF_OPEN)
  3. fallback降级机制

源码分析

HystrixCircuitBreaker初始化过程

我们还是会以AbstractCommand为突破口,这里继续看它的构造函数,其中里面有初始化熔断器initCircuitBreaker()的过程,具体代码如下:

abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {

	private static HystrixCircuitBreaker initCircuitBreaker(boolean enabled, HystrixCircuitBreaker fromConstructor,
HystrixCommandGroupKey groupKey, HystrixCommandKey commandKey,
HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
if (enabled) {
if (fromConstructor == null) {
// 构建默认的HystrixCircuitBreaker
return HystrixCircuitBreaker.Factory.getInstance(commandKey, groupKey, properties, metrics);
} else {
return fromConstructor;
}
} else {
return new NoOpCircuitBreaker();
}
}
} public interface HystrixCircuitBreaker {
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
// circuitBreakersByCommand是一个map,key为commandKey,也就是FeignClient中定义的方法名
// 类似于ServiceAFeignClient.sayHello(String)
HystrixCircuitBreaker previouslyCached = circuitBreakersByCommand.get(key.name());
if (previouslyCached != null) {
return previouslyCached;
} // 每个commandKey都对应着自己的熔断器,如果没有则会构造一个HystrixCircuitBreaker
HystrixCircuitBreaker cbForCommand = circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreakerImpl(key, group, properties, metrics));
if (cbForCommand == null) {
return circuitBreakersByCommand.get(key.name());
} else {
return cbForCommand;
}
} class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
private final HystrixCommandProperties properties;
private final HystrixCommandMetrics metrics; private Subscription subscribeToStream() {
// 对HealthCounts进行订阅
// HealthCounts中包含 总请求次数、总失败次数、失败率
// HealthCounts 统计数据有变化则会回调到这里来
return metrics.getHealthCountsStream()
.observe()
.subscribe(new Subscriber<HealthCounts>() {
@Override
public void onCompleted() { } @Override
public void onError(Throwable e) { } // 判断是否要降级的核心逻辑
@Override
public void onNext(HealthCounts hc) {
// 一个时间窗口(默认10s钟)总请求次数是否大于circuitBreakerRequestVolumeThreshold 默认为20s
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { } else {
// 错误率(总错误次数/总请求次数)小于circuitBreakerErrorThresholdPercentage(默认50%)
if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) { } else {
// 反之,熔断状态将从CLOSED变为OPEN,且circuitOpened==>当前时间戳
if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
circuitOpened.set(System.currentTimeMillis());
}
}
}
}
});
}
}
}

上面就是熔断器初始化过程,这里面做了几件事:

  1. 每个commandKey都有自己的一个熔断器

    commandKey表现形式为:ServiceAFeignClient#sayHello(String)

  2. 如果commandKey不存在熔断器,则构建默认熔断器

    默认熔断器会对HealthCounts进行订阅。HealthCounts中包含时间窗口内(默认10s钟)请求的总次数、失败次数、失败率

  3. HealthCounts中统计数据有变化则会回调subscribe.onNext()方法进行熔断开启判断

  4. 熔断开启条件:

  • 时间窗口内(默认10s钟)总请求次数大于20次
  • 时间窗口内(默认10s钟)失败率大于50%
  • 满足上述两个条件后熔断器状态从CLOSED变成OPEN

熔断器在第一次请求时会初始化AbtractCommand,同时也会创建对应commandKey的熔断器 ,熔断器默认都是关闭的(可配置为强制开启),只有满足触发条件才会被开启。下面就一起来看下熔断、半开等状态是如何触发的吧。

Hystrix熔断机制(CLOSED/OPEN/HALF_OPEN)

这里我们以AbstractCommand.applyHystrixSemantics() 为入口,一步步往下探究,这个方法在上一讲已经提到过,一个正常的Feign请求都会调用此方法。

abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
// 如果熔断了,这这里返回为false
// 这里也包含HALF_OPEN逻辑
if (circuitBreaker.attemptExecution()) {
final Action1<Throwable> markExceptionThrown = new Action1<Throwable>() {
@Override
public void call(Throwable t) {
eventNotifier.markEvent(HystrixEventType.EXCEPTION_THROWN, commandKey);
}
}; if (executionSemaphore.tryAcquire()) {
try {
executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis());
return executeCommandAndObserve(_cmd)
.doOnError(markExceptionThrown)
.doOnTerminate(singleSemaphoreRelease)
.doOnUnsubscribe(singleSemaphoreRelease);
} catch (RuntimeException e) {
return Observable.error(e);
}
} else {
return handleSemaphoreRejectionViaFallback();
}
} else {
return handleShortCircuitViaFallback();
}
}
}

circuitBreaker.attemptExecution() 这个逻辑就是判断,如果熔断了,那么返回false。而且这里还包含HALF_OPEN的逻辑,我们先看如何触发熔断的,这个后面再接着看。

接着往下跟进executeCommandAndObserve() 方法:

abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
final HystrixRequestContext currentRequestContext = HystrixRequestContext.getContextForCurrentThread(); // 省略部分代码... // 运行过程中,出现异常等都会进入此回调函数
final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
@Override
public Observable<R> call(Throwable t) {
circuitBreaker.markNonSuccess();
Exception e = getExceptionFromThrowable(t);
executionResult = executionResult.setExecutionException(e);
if (e instanceof RejectedExecutionException) {
return handleThreadPoolRejectionViaFallback(e);
} else if (t instanceof HystrixTimeoutException) {
return handleTimeoutViaFallback();
} else if (t instanceof HystrixBadRequestException) {
return handleBadRequestByEmittingError(e);
} else {
/*
* Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException.
*/
if (e instanceof HystrixBadRequestException) {
eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
return Observable.error(e);
} return handleFailureViaFallback(e);
}
}
}; Observable<R> execution;
if (properties.executionTimeoutEnabled().get()) {
// 这里创建一个 HystrixObservableTimeoutOperator
execution = executeCommandWithSpecifiedIsolation(_cmd)
.lift(new HystrixObservableTimeoutOperator<R>(_cmd));
} else {
execution = executeCommandWithSpecifiedIsolation(_cmd);
} return execution.doOnNext(markEmits)
.doOnCompleted(markOnCompleted)
.onErrorResumeNext(handleFallback)
.doOnEach(setRequestContext);
}
}

当我们服务调用中出现异常都会进入handleFallback()中,里面的方法我们就不继续跟入了,猜测里面会有更新HealthCounts中的属性,然后触发 HystrixCircuitBreaker中的onNext()方法,当满足熔断条件时 则会将熔断状态从CLOSED变成OPEN

这里我们会跟进下HystrixObservableTimeoutOperator 代码,这个是对我们执行过程中判断是否超时。

上面代码中,执行executeCommandWithSpecifiedIsolation() 方法时也会创建一个超时监视器:

private static class HystrixObservableTimeoutOperator<R> implements Operator<R, R> {

    final AbstractCommand<R> originalCommand;

    public HystrixObservableTimeoutOperator(final AbstractCommand<R> originalCommand) {
this.originalCommand = originalCommand;
} @Override
public Subscriber<? super R> call(final Subscriber<? super R> child) {
TimerListener listener = new TimerListener() { @Override
public void tick() {
// 判断command的timeOut状态,如果是未执行状态,则更新为已超时
if (originalCommand.isCommandTimedOut.compareAndSet(TimedOutStatus.NOT_EXECUTED, TimedOutStatus.TIMED_OUT)) {
originalCommand.eventNotifier.markEvent(HystrixEventType.TIMEOUT, originalCommand.commandKey);
s.unsubscribe(); final HystrixContextRunnable timeoutRunnable = new HystrixContextRunnable(originalCommand.concurrencyStrategy, hystrixRequestContext, new Runnable() {
@Override
public void run() {
child.onError(new HystrixTimeoutException());
}
}); timeoutRunnable.run();
}
} @Override
public int getIntervalTimeInMilliseconds() {
return originalCommand.properties.executionTimeoutInMilliseconds().get();
}
}; final Reference<TimerListener> tl = HystrixTimer.getInstance().addTimerListener(listener);
originalCommand.timeoutTimer.set(tl); // 省略部分代码...
s.add(parent); return parent;
}
} public class HystrixTimer {
public Reference<TimerListener> addTimerListener(final TimerListener listener) {
startThreadIfNeeded();
Runnable r = new Runnable() {
@Override
public void run() {
try {
// 执行上面的tick方法,改变command timeout状态
listener.tick();
} catch (Exception e) {
logger.error("Failed while ticking TimerListener", e);
}
}
}; // 执行调度任务,延迟加载,延迟时间和调度时间默认都为1s钟
// 这里使用线程池,coreSize=cpu核心数 maxSize为Integer.Max
ScheduledFuture<?> f = executor.get().getThreadPool().scheduleAtFixedRate(r, listener.getIntervalTimeInMilliseconds(), listener.getIntervalTimeInMilliseconds(), TimeUnit.MILLISECONDS);
return new TimerReference(listener, f);
}
}

这里面核心业务是起一个调度任务,默认每秒钟执行一次,然后调用tick()方法,如果当前command状态还是NOT_EXECUTED状态,那么将command状态改为TIMED_OUT 。此时会进入到之前的handleFallback回调函数中,这里又会更新HealthCounts中的数据,对应的触发之前熔断的判断条件:

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);
} private Subscription subscribeToStream() {
//这里会在每次执行onNext()事件的时候来评估是否需要打开或者关闭断路器
return metrics.getHealthCountsStream()
.observe()
.subscribe(new Subscriber<HealthCounts>() {
@Override
public void onCompleted() { } @Override
public void onError(Throwable e) { } @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());
}
}
}
}
});
}

如果符合熔断条件,那么command熔断状态就会变为OPEN,此时熔断器打开。

如果我们command执行成功,那么就会清理掉这个timeout timer schedule任务。

abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
private void handleCommandEnd(boolean commandExecutionStarted) {
Reference<TimerListener> tl = timeoutTimer.get();
// 如果timeOutTimer不为空,这里则clear一下
// clear会关闭启动的调度任务
if (tl != null) {
tl.clear();
} long userThreadLatency = System.currentTimeMillis() - commandStartTimestamp;
executionResult = executionResult.markUserThreadCompletion((int) userThreadLatency);
if (executionResultAtTimeOfCancellation == null) {
// metrics统计数据
metrics.markCommandDone(executionResult, commandKey, threadPoolKey, commandExecutionStarted);
} else {
metrics.markCommandDone(executionResultAtTimeOfCancellation, commandKey, threadPoolKey, commandExecutionStarted);
} if (endCurrentThreadExecutingCommand != null) {
endCurrentThreadExecutingCommand.call();
}
}
}

如上所属,我们已经知道了熔断开启的触发时机,那么如果一个commandKey开启了熔断,下次的请求是该如何直接降级呢?我们来看下代码:

abstract class AbstractCommand<R> implements HystrixInvokableInfo<R>, HystrixObservable<R> {
private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) { // 这个if条件就代表是否开启熔断
if (circuitBreaker.attemptExecution()) {
// 执行业务逻辑代码...
} else {
return handleShortCircuitViaFallback();
}
}
} class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
public boolean attemptExecution() {
// 如果熔断配置的为强制开启,那么直接返回false执行熔断逻辑
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
// 如果熔断配置为强制关闭,那么永远不走熔断逻辑
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
// 熔断开启时 circuitOpened设置为当前时间戳
if (circuitOpened.get() == -1) {
return true;
} else {
// 如果当前时间距离熔断小于5s钟,那么将熔断状态从OPEN改为HALF_OPEN
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;
}
}
}
} private boolean isAfterSleepWindow() {
final long circuitOpenTime = circuitOpened.get();
final long currentTime = System.currentTimeMillis();
// circuitBreakerSleepWindowInMilliseconds 默认为5s钟
final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
// 当前熔断距离熔断是否超过5s钟
return currentTime > circuitOpenTime + sleepWindowTime;
}
}

我们可以看到,在applyHystrixSemantics()这个核心的方法中,先判断是否熔断,如果熔断则直接走fallback逻辑。

attemptExecution()判断条件中还涉及到HALF_OPEN的逻辑,如果熔断开启,下一次请求的时候,会判断当前时间距离上一次时间是否超过了5s钟,如果没有超过,则会将熔断状态从OPEN变为HALF_OPEN,此时会放一个请求按照正常逻辑去执行:

  1. 执行失败,熔断状态又会从HALF_OPEN变成OPEN
  2. 执行成功,熔断状态从HALF_OPEN变成CLOSED,并清除熔断相关设置

执行成功后代码:

class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
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);
}
}
}

上面对整个熔断的状态:CLOSED、OPEN、HALF_OPEN梳理的已经很清楚了,下面看看降级是该如何处理的吧。

fallback降级机制

上面已经讲解了Hystrix 熔断开启的机制等内容,这里主要是说如果一个请求失败(线程池拒绝、超时、badRequest等),那么Hystrix是如何执行降级的呢?

还是回到我们最初的代码 HystrixInvocationHandler类中,看看其invoke()方法中的getFallback回调函数:

protected Object getFallback() {
if (fallbackFactory == null) {
return super.getFallback();
}
try {
// 通过我们配置好的fallbackFactory找到对应的FeignClient,这里是获取ServiceAFeignClient
Object fallback = fallbackFactory.create(getExecutionException());
// fallbackMap中key为ServiceAFeignClient.sayHello(Integer)
// 获取具体的降级method方法
Object result = fallbackMethodMap.get(method).invoke(fallback, args);
if (isReturnsHystrixCommand(method)) {
return ((HystrixCommand) result).execute();
} else if (isReturnsObservable(method)) {
// Create a cold Observable
return ((Observable) result).toBlocking().first();
} else if (isReturnsSingle(method)) {
// Create a cold Observable as a Single
return ((Single) result).toObservable().toBlocking().first();
} else if (isReturnsCompletable(method)) {
((Completable) result).await();
return null;
} else {
return result;
}
} catch (IllegalAccessException e) {
// shouldn't happen as method is public due to being an interface
throw new AssertionError(e);
} catch (InvocationTargetException e) {
// Exceptions on fallback are tossed by Hystrix
throw new AssertionError(e.getCause());
}
}
};

这里很简单,其实就是先获取到我们自己在FallbackFactory中配置的的降级方法,然后执行降级逻辑。

总结

这一讲核心逻辑主要是Hystrix熔断状态的变化,主要是CLOSED、OPEN、HALF_OPEN几种状态触发的时间,互相转变的流程,以及执行降级逻辑的原理。

我们仍然是用一个流程图来总结一下:

高清大图链接:

https://www.processon.com/view/link/5e1ee0afe4b0c62462aae684

(点击原文可以直接查看大图哦

【一起学源码-微服务】Hystrix 源码三:Hystrix核心流程:Hystix降级、熔断等原理剖析的更多相关文章

  1. 【一起学源码-微服务】Nexflix Eureka 源码十:服务下线及实例摘除,一个client下线到底多久才会被其他实例感知?

    前言 前情回顾 上一讲我们讲了 client端向server端发送心跳检查,也是默认每30钟发送一次,server端接收后会更新注册表的一个时间戳属性,然后一次心跳(续约)也就完成了. 本讲目录 这一 ...

  2. 【一起学源码-微服务】Nexflix Eureka 源码十三:Eureka源码解读完结撒花篇~!

    前言 想说的话 [一起学源码-微服务-Netflix Eureka]专栏到这里就已经全部结束了. 实话实说,从最开始Eureka Server和Eureka Client初始化的流程还是一脸闷逼,到现 ...

  3. 【一起学源码-微服务】Ribbon源码五:Ribbon源码解读汇总篇~

    前言 想说的话 [一起学源码-微服务-Ribbon]专栏到这里就已经全部结束了,共更新四篇文章. Ribbon比较小巧,这里是直接 读的spring cloud 内嵌封装的版本,里面的各种config ...

  4. 【一起学源码-微服务】Eureka+Ribbon+Feign阶段性总结

    前言 想说的话 这里已经梳理完Eureka.Ribbon.Feign三大组件的基本原理了,今天做一个总结,里面会有一个比较详细的调用关系流程图. 说明 原创不易,如若转载 请标明来源! 博客地址:一枝 ...

  5. springcloud微服务实战:Eureka+Zuul+Ribbon+Hystrix+SpringConfig

    原文地址:http://blog.csdn.net/yp090416/article/details/78017552 springcloud微服务实战:Eureka+Zuul+Ribbon+Hyst ...

  6. 通过Dapr实现一个简单的基于.net的微服务电商系统(三)——一步一步教你如何撸Dapr

    目录:一.通过Dapr实现一个简单的基于.net的微服务电商系统 二.通过Dapr实现一个简单的基于.net的微服务电商系统(二)--通讯框架讲解 三.通过Dapr实现一个简单的基于.net的微服务电 ...

  7. 【一起学源码-微服务】Hystrix 源码一:Hystrix基础原理与Demo搭建

    说明 原创不易,如若转载 请标明来源! 欢迎关注本人微信公众号:壹枝花算不算浪漫 更多内容也可查看本人博客:一枝花算不算浪漫 前言 前情回顾 上一个系列文章讲解了Feign的源码,主要是Feign动态 ...

  8. 【一起学源码-微服务】Nexflix Eureka 源码二:EurekaServer启动之配置文件加载以及面向接口的配置项读取

    前言 上篇文章已经介绍了 为何要读netflix eureka源码了,这里就不再概述,下面开始正式源码解读的内容. 如若转载 请标明来源:一枝花算不算浪漫 代码总览 还记得上文中,我们通过web.xm ...

  9. 【一起学源码-微服务】Nexflix Eureka 源码八:EurekaClient注册表抓取 精妙设计分析!

    前言 前情回顾 上一讲 我们通过单元测试 来梳理了EurekaClient是如何注册到server端,以及server端接收到请求是如何处理的,这里最重要的关注点是注册表的一个数据结构:Concurr ...

随机推荐

  1. H3C Basic NAT配置示例

  2. 走过的laravel-admin 的坑

    一.http://laravel-admin.org/docs/#/zh/  大家可以根据这个安装1.5 版本的laravel后台管理, 他很方便哦,有很多方法他都自己自己封装了. 二.大家如果想好好 ...

  3. linux一些重要数据结构

    如同你想象的, 注册设备编号仅仅是驱动代码必须进行的诸多任务中的第一个. 我们将很 快看到其他重要的驱动组件, 但首先需要涉及一个别的. 大部分的基础性的驱动操作包括 3 个重要的内核数据结构, 称为 ...

  4. H3C DHCP服务器基本配置示例

  5. vue项目上滑滚动加载更多&下拉刷新

    上滑滚动时获取内容高度.屏幕高度和滚动高度(此处#sslist要为内容是id) 内容高度  let innerHeight = document.querySelector("#sslist ...

  6. Linux 内核 中断 urb

    函数 usb_fill_int_urb 是一个帮忙函数, 来正确初始化一个 urb 来发送给 USB 设备的 一个中断端点: void usb_fill_int_urb(struct urb *urb ...

  7. vue-learning:23 - js - leftcycle hooks

    vue 生命周期钩子函数 每一个Vue实例在创建时都需要经过一系列初始化.根据vue实例化过程中执行的逻辑,可以分为5个阶段: 初始化阶段 模板编译阶段 虚拟DOM挂载阶段 响应更新阶段 卸载阶段 这 ...

  8. CF 453C. Little Pony and Summer Sun Celebration

    CF 453C. Little Pony and Summer Sun Celebration 构造题. 题目大意,给定一个无向图,每个点必须被指定的奇数或者偶数次,求一条满足条件的路径(长度不超\( ...

  9. dotnet 控制台 Hangfire 后台定时任务

    本文告诉大家如何在 dotnet core 的控制台通过 Hangfire 开启后台定时任务 首先需要安装 HangFire 这个 Nuget 库,通过这个库可以用来做定时任务,虽然很多时候都是在 A ...

  10. P3803 FFT求多项式系数

    P3803 FFT求多项式系数 传送门:https://www.luogu.org/problemnew/show/P3803 题意: 这是一道FFT模板题,求多项式系数 题解: 对a和b的系数求一个 ...