springcloud — 微服务熔断治理之断路器Hystrix解析(二)
Hystrix是一个限流、降级容错框架,它能很好的保护我们的接口、应用。这很大程度上得益于它提供了fallback机制:回退(也叫降级)。本文主要讲述了Hystrix的fallback的主要特性和核心点:Hystrix的fallback回退/降级逻辑Hystrix触发fallback降级逻辑的5种情况Hystrix抛出HystrixBadRequestException异常不熔断1、Hystrix
Hystrix是一个限流、降级容错框架,它能很好的保护我们的接口、应用。这很大程度上得益于它提供了fallback机制:回退(也叫降级)。本文主要讲述了Hystrix的fallback的主要特性和核心点:
- Hystrix的fallback回退/降级逻辑
- Hystrix触发fallback降级逻辑的5种情况
- Hystrix抛出HystrixBadRequestException异常不熔断
1、Hystrix的fallback回退/降级逻辑
xxxCommand系列正常的执行逻辑、以及出现异常的回退逻辑均在HystrixCommand和HystrixObservableCommand二者的抽象父类AbstractCommand里实现的。而对fallback的所有执行逻辑便在方法getFallbackOrThrowException()。
1.1、getFallbackOrThrowException() 源码解读:
该方法是AbstractCommand的一个私有方法,语义是:执行fallback回滚或者抛出异常(一般为HystrixRuntimeException异常类型,当然不是绝对的)。
值得注意的是:若执行到了这一步,Hystrix它能100%保证无论如何都能尽快把你的请求掐断,避免系统被拖死。虽然Hystrix它建议你不要在getFallback()里面写耗时的阻塞的(比如请求网络)逻辑,但是建议归建议,但还是可能有人不这么做怎么办呢?所以fallbackSemaphoreOverride信号量的作用:专门用于fallback的信号量控制,决不让其堆积请求。
需要注意的是:调用此方法必传一个Exception,因为Hystrix认为执行进入到这里肯定是发生了一个异常才行的。
AbstractCommand:
// _cmd:命令对象
// eventType:事件类型
// failureType:失败类型枚举(BAD_REQUEST_EXCEPTION...) 见下面枚举定义
// message:失败的消息。如timed-out、failed、short-circuited等
// Exception:导致失败的异常(一定只有异常才能导致失败),如java.util.concurrent.TimeoutException
private Observable<R> getFallbackOrThrowException(
AbstractCommand<R> _cmd, HystrixEventType eventType,
FailureType failureType, String message,
Exception originalException) {
// 因为fallback也是在线程池里面执行,所以也是需要传递数据的
HystrixRequestContext requestContext = HystrixRequestContext.getContextForCurrentThread();
// 统计相关事件的次数:详见事件计数器
executionResult = executionResult.addEvent((int) latency, eventType);
...
// 如果异常类型是ExceptionNotWrappedByHystrix类型
if (shouldNotBeWrapped(originalException)){
... // 触发executionHook.onError()动作
return Observable.error(e);
// 是否是不可恢复的异常类型:比如StackOverflowError/VirtualMachineError...
// 因为并不是所有的Error都不会恢复,所以这里例举出来。如果是不可恢复的错误,就包装一下抛出
} else if (isUnrecoverable(originalException)) {
...
// 若是不可恢复错误,转未HystrixRuntimeException异常抛出
return Observable.error(new HystrixRuntimeException(failureType, ... );
} else {
// 若是可自己恢复的Error,如IOError,那就输入一句日志即可
if (isRecoverableError(originalException)) { ... }
// =======普通异常类型(比如NPE之类的),那就开始执行fallback函数========
if (properties.fallbackEnabled().get()) { // 显然默认是开启的,不建议关闭它
// 若你没指定,默认使用的是TryableSemaphoreActual 10个信号量
// 你可以通过fallbackIsolationSemaphoreMaxConcurrentRequests指定这个值
// 该信号量用于杜绝你的fallback还非常耗时的case,以防万一
TryableSemaphore fallbackSemaphore = getFallbackSemaphore();
...
// 最终return的可观察对象
// getFallbackObservable是个抽象方法,由子类提供
// 比如command它就是使用Observable.just(getFallback())把任意对象转换为Observable
// 当然它是个defer的Observable
Observable<R> fallbackExecutionChain;
// 若还有信号量资源就继续执行(否则会会做异常处理,见下面)
if (fallbackSemaphore.tryAcquire()) {
// 用户是否自定义了fallback函数
// 也就是是否重写了getFallback()方法嘛
if (isFallbackUserDefined()) {
... // executionHook.onFallbackStart(this);
fallbackExecutionChain = getFallbackObservable();
} else { // 区别是:若没有复写这个方法,就不会触发onFallbackStart()动作
fallbackExecutionChain = getFallbackObservable();
}
// 绑定固定的处理函数
return fallbackExecutionChain
// 作用于每个iter身上:setRequestContextIfNeeded(requestContext);
// 确保上下文是同一个,数据是通的
.doOnEach(setRequestContext)
// 主要是为了触发executionHook相关回调
.lift(new FallbackHookApplication(_cmd))
.lift(new DeprecatedOnFallbackHookApplication(_cmd))
// 发布事件:HystrixEventType.FALLBACK_EMIT
// executionResult.addEvent(HystrixEventType.FALLBACK_EMIT)
.doOnNext(markFallbackEmit)
// 完成了。发布事件:HystrixEventType.FALLBACK_SUCCESS
// 证明fallback成功
.doOnCompleted(markFallbackCompleted)
// fallback失败(出现异常了),发布事件
// HystrixEventType.FALLBACK_MISSING、FALLBACK_FAILURE
.onErrorResumeNext(handleFallbackError)
// 释放信号量:fallbackSemaphore.release();
.doOnTerminate(singleSemaphoreRelease)
// fallbackSemaphore.release();
.doOnUnsubscribe(singleSemaphoreRelease);
} else { // 此else对应逻辑:信号量不够的情况
return handleFallbackRejectionByEmittingError();
}
} else { // 此else对应逻辑:properties禁用了fallback的情况
return handleFallbackDisabledByEmittingError(originalException, failureType, message);
}
}
}
// 定义失败枚举:一个7种类型
HystrixRuntimeException:
public static enum FailureType {
BAD_REQUEST_EXCEPTION, COMMAND_EXCEPTION, TIMEOUT, SHORTCIRCUIT, REJECTED_THREAD_EXECUTION, REJECTED_SEMAPHORE_EXECUTION, REJECTED_SEMAPHORE_FALLBACK
}
1.2、fallback函数执行成功or执行失败的处理
fallback函数执行成功or执行失败的处理
针对以上fallbackExecutionChain被观察对象的执行,还需注意关心它的执行成功or失败,需要做对应的处理:
- doOnNext(markFallbackEmit):每发射一个数据(执行fallback方法之前),给事件计数executionResult.addEvent(HystrixEventType.FALLBACK_EMIT)
- doOnCompleted(markFallbackCompleted):正常执行完成(fallback未抛出异常)时,记录结果事件executionResult.addEvent((int) latency, HystrixEventType.FALLBACK_SUCCESS)
- onErrorResumeNext(handleFallbackError):若执行fallback时候发生错误(重点),就执行如下逻辑:
AbstractCommand:
// 若异常类型是该类型(比如你使用HystrixCommand,但没重写getFallback()方法,执行就抛出此异常)
// 就包装为HystrixRuntimeException给你抛出
// 并且有你熟悉的抛错消息: message + " and no fallback available."
if (fe instanceof UnsupportedOperationException) {
...
// 记录结果:事件类型是FALLBACK_MISSING
executionResult.addEvent((int) latency, HystrixEventType.FALLBACK_MISSING);
return Observable.error(new HystrixRuntimeException(failureType, ...);
} else { // 你的fallback方法里抛出了其它异常
...
// 记录事件,此时事件类型是fallback执行失败FALLBACK_FAILURE
executionResult.addEvent((int) latency, HystrixEventType.FALLBACK_FAILURE);
return Observable.error(new HystrixRuntimeException(failureType ... message + " and fallback failed." ...);
}
对于fallback执行失败的case,请注意错误消息and no fallback available.和and fallback failed.的区别。前者是木有提供fallback函数,后者是提供了但是执行时抛错了;
另外,源码处有两个else逻辑此处也给个简要描述:
- 如果properties.fallbackEnabled() = false显示禁用了fallback功能,最终会触发handleFallbackDisabledByEmittingError()方法(此为唯一调用处):
AbstractCommand:
private Observable<R> handleFallbackDisabledByEmittingError(Exception underlying, FailureType failureType, String message) {
... // 触发executionHook.onError动作
// 转换为一个HystrixRuntimeException抛出
return Observable.error(new HystrixRuntimeException(failureType, ... " and fallback disabled." ... );
}
- 如果执行fallabck时请求信号量资源不够用了,那么执行handleFallbackRejectionByEmittingError()方法(此处为唯一调用处):
AbstractCommand:
private Observable<R> handleFallbackRejectionByEmittingError() {
...
// 这里记录了结果哦。统计对应事件的次数(毕竟出现此case的数据还是蛮有意义的)
executionResult = executionResult.addEvent((int) latencyWithFallback, HystrixEventType.FALLBACK_REJECTION);
return Observable.error(new HystrixRuntimeException(FailureType.REJECTED_SEMAPHORE_FALLBACK,... " fallback execution rejected." ...);
}
1.3、getFallbackOrThrowException回退步骤文字总结
首先需要明确:执行此fallabck步骤肯定是发生了Exception异常的(当然有可能是Error错误),所以异常类型很关键,此处用 e 来表示源生异常类型(如目标方法自己产生的NPE)。
- 若 e 不需要被包装,那就不用使用HystrixRuntimeException去包它了,直接返回:Observable.error(e);
ExceptionNotWrappedByHystrix是个标记接口:若你的异常类型实现了此接口,那么抛出此类型的异常将不会再被 HystrixRuntimeException包起来了 - 若e是不可恢复的异常类型如:StackOverflowError/VirtualMachineError/ThreadDeath/LinkageError,那就直接包装为HystrixRuntimeException类型抛出。
- 到这一步,e的类型“过关”了,着手执行fallabck逻辑。若禁用了fallabck,就执行handleFallbackDisabledByEmittingError()方法 -> 抛出HystrixRuntimeException异常,否则继续。
- 准备执行fallabck函数,先请求fallabck专用的信号量。若无资源了,那就执行handleFallbackRejectionByEmittingError()犯法 -> 抛出HystrixRuntimeException异常,否则继续。否则继续
- 通过抽象方法getFallbackObservable()拿到被观察对象Observable,然后便可开始执行目标fallabck函数了。其中执行目标fallback函数时分为成功or失败。
成功: 执行doOnCompleted放,整成记录FALLBACK_SUCCESS事件到结果即可
失败:分为未提供fallabck函数和fallback函数内部抛出了异常两种case -> 均抛出HystrixRuntimeException异常,对应异常消息是著名的:and no fallback available.和and fallback failed.。
2、Hystrix触发fallback降级逻辑的5种情况
Hystrix把它处理fallabck的全部逻辑都封装在了getFallbackOrThrowException()方法里,从源码处来看只需知道有哪些地方调用了此方法便可得出答案。
从官方的Hystrix原理图中能看到触发fallback回退的地方一共有5处:图中共色字体已经标出:
站在源码的角度看,此问题亦可转换一下,也可这么问:调用getFallbackOrThrowException()的地方有几处呢?如下截图也展示了,恰好也是5处:
针对这5种case,下面一一作出解释并且给出针对性的触发示例;
2.1、short-circuited短路
- 触发条件:断路器HystrixCircuitBreaker已处于打开状态。请求再次进来便直接执行短路逻辑
- 异常类型:new RuntimeException(“Hystrix circuit short-circuited and is OPEN”)
- 对应方法名:handleShortCircuitViaFallback()
AbstractCommand:
// 可以看到异常是它内部new出来的,然后调用
private Observable<R> handleShortCircuitViaFallback() {
Exception shortCircuitException = new RuntimeException("Hystrix circuit short-circuited and is OPEN");
// 设置结果:异常类型为RuntimeException类型
executionResult = executionResult.setExecutionException(shortCircuitException);
// 异常消息关键字:short-circuited
return getFallbackOrThrowException(this, HystrixEventType.SHORT_CIRCUITED, FailureType.SHORTCIRCUIT, "short-circuited", shortCircuitException);
}
2.2、threadpool-rejected线程池拒绝
- 触发条件:当线程池满了,再有请求进来时触发此拒绝逻辑
- 异常类型:new RejectedExecutionException(“Rejected command because thread-pool queueSize is at rejection threshold.”)
该异常由HystrixContextScheduler里去申请线程池资源时抛出 - 对应方法名:handleThreadPoolRejectionViaFallback(Exception underlying)
因异常由方法“外部”抛出,所以此方法有入参
AbstractCommand:
// 标记threadPool#markThreadRejection
// 这个会统计到HystrixThreadPoolMetrics指标信息里去
private Observable<R> handleThreadPoolRejectionViaFallback(Exception underlying) {
...
threadPool.markThreadRejection();
// 异常信息:could not be queued for execution
return getFallbackOrThrowException(this, HystrixEventType.THREAD_POOL_REJECTED, FailureType.REJECTED_THREAD_EXECUTION, "could not be queued for execution", underlying);
}
2.3、semaphore-rejected信号量拒绝
- 触发条件:当信号量木有资源了,再有请求进来时触发信号量拒绝逻辑。
- 异常类型:new RuntimeException(“could not acquire a semaphore for execution”)
请注意它是RuntimeException,和上有不同 - 对应方法名:handleSemaphoreRejectionViaFallback()
它是内部自己new的异常,所以木有入参
AbstractCommand:
private Observable<R> handleSemaphoreRejectionViaFallback() {
Exception semaphoreRejectionException = new RuntimeException("could not acquire a semaphore for execution");
executionResult = executionResult.setExecutionException(semaphoreRejectionException);
// 异常关键字:could not acquire a semaphore for execution
return getFallbackOrThrowException(this, HystrixEventType.SEMAPHORE_REJECTED, FailureType.REJECTED_SEMAPHORE_EXECUTION,
"could not acquire a semaphore for execution", semaphoreRejectionException);
2.4、timed-out超时
- 触发条件:当目标方法执行超时,会触发超时的回退逻辑。
- 异常类型:new HystrixTimeoutException()。最终异常类型为:new TimeoutException()
Hystrix的超时是使用TimerListener来控制实现的。默认超时机制是开启的,时间是1s - 对应方法名:handleTimeoutViaFallback()
AbstractCommand:
// 异常信息关键字:timed-out
// 注意异常类型是new出来的:new TimeoutException()。无任何msg消息哦~
private Observable<R> handleTimeoutViaFallback() {
return getFallbackOrThrowException(this, HystrixEventType.TIMEOUT, FailureType.TIMEOUT, "timed-out", new TimeoutException());
}
2.5、failed执行失败
- 触发条件:command执行失败,也就是你的run方法里执行失败(抛出了运行时异常)时,执行此部分逻辑
- 异常类型:run方法里的任意运行时异常类型,比如NPE异常
- 对应方法名:handleFailureViaFallback()
AbstractCommand:
// 只要是用户自己的代码问题,产生的异常,均到交到此处处理
private Observable<R> handleFailureViaFallback(Exception underlying) {
// 把用户产生的异常输出。debug级别哦~
logger.debug("Error executing HystrixCommand.run(). Proceeding to fallback logic ...", underlying);
eventNotifier.markEvent(HystrixEventType.FAILURE, commandKey);
// 记录异常类型到结果
executionResult = executionResult.setException(underlying);
return getFallbackOrThrowException(this, HystrixEventType.FAILURE, FailureType.COMMAND_EXCEPTION, "failed", underlying);
}
该方法你可以认为是个兜底实现,除了上面4种之外,但凡有其它任何异常均会交给它来处理(当然HystrixBadRequestException类型除外);
思考:若fallback方法内执行时抛出异常了呢?
首先,官方建议fallabck里返回的是常量/缓存里的值(比如Map里的值),所以fallback里出现异常的理应几乎为0。但建议总归是建议,若你真要在里面写复杂逻辑:比如通过RPC去获取数据,那错误率就高了。那么问题来了:万一出现此情况,是何表现呢???
3、Hystrix抛出HystrixBadRequestException异常不熔断
通过前面我们知道了,Hystrix是个强大的熔断降级框架:收集目标方法的成功、失败等指标信息,触发熔断器。其中失败信息通过异常来表示,交给Hystrix进行统计。
但是,有的时候有些异常是并不能触发熔断的,比如请求参数异常等,那怎么办呢?或许你已经知道了结论:目标方法执行抛出异常时,除HystrixBadRequestException之外,其他异常都会认为是Hystrix命令执行失败并触发服务降级处理逻辑。
通过前面分析我们已经了解到了Hystrix触发fallback降级逻辑的5种情况,也就是:
1. short-circuited短路
2. threadpool-rejected线程池拒绝
3. semaphore-rejected信号量拒绝
4. timed-out超时
5. failed执行失败
出现这些类型的失败均会触发Hystrix的fallback机制。但HystrixBadRequestException这类型的异常将不会触发fallabck机制。下面我们将介绍这个场景;
3.1、认识HystrixBadRequestException
这个类本身并没有什么好说的,就是一个非常简单的运行时异常:
public class HystrixBadRequestException extends RuntimeException {
private static final long serialVersionUID = -8341452103561805856L;
public HystrixBadRequestException(String message) {
super(message);
}
public HystrixBadRequestException(String message, Throwable cause) {
super(message, cause);
}
}
不过它的Javadoc对该类的作用描述:用提供的参数或状态表示错误而不是执行失败的异常。与HystrixCommand抛出的所有其他异常不同,这不会触发回退,不会计算故障指标,因此不会触发断路器。
注意:当一个错误是由于用户输入IllegalArgumentException引起时(比如手误),这个只应该使用,否则就会破坏容错和回退行为的目的。总的来说千万别盲目使用,使用得最多的case是:结合Feign错误编码器一起解决客户端400异常而意外熔断的问题。
3.2、熔断器的数据从哪儿收集?
我们知道HystrixCircuitBreaker的健康指标数据来源于HealthCountsStream这个数据流:统计时间窗口里面各桶的值,汇总为HealthCounts对象输出。我们可以看下HealthCounts这个类,它记录着滑动窗口期间的请求数,包括:总数、失败数、失败百分比。它会统计如下事件:
- HystrixEventType.SUCCESS
- HystrixEventType.FAILURE
- HystrixEventType.TIMEOUT
- HystrixEventType.THREAD_POOL_REJECTED
- HystrixEventType.SEMAPHORE_REJECTED
这些事件中除了1,其它均为失败。另外2-4不就正好对应着前面说的触发fallback的前四种情况吗?
触发fallback的情况和熔断器事件类型的对应关系
下面绘制一张表格表达其对应关系:
对此表格做如下几点说明:
- 事件类型均为HystrixEventType类型,本处前缀省略
- 这里指的是原始异常类型,因为最终经过fallback处理后都会被包为HystrixRuntimeException
- 实际上失败情况HystrixBadRequestException和failed失败同属目标方法抛出的异常,只是前者比较特殊而已
结合熔断器统计数据类HealthCounts关心的几个事件类型来说:除了HystrixBadRequestException异常导致的失败,其它均会被收集作为断路器的指标数据。
short-circuited短路这种case就不用收集啦,因为都已经短路了,就没必要再收集了,否则断路器永远都自愈不回来就尴尬了;
3.3、为何不会触发熔断器?
所有的命令执行,最终在executeCommandAndObserve()方法内:
AbstractCommand:
private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
...
return execution.doOnNext(markEmits)
.doOnCompleted(markOnCompleted)
.onErrorResumeNext(handleFallback)
.doOnEach(setRequestContext);
}
其它部分不用关心,仅需关心onErrorResumeNext(handleFallback) 这个函数,它的触发条件是:发射数据时(目标方法执行时)出现异常便会回调此函数,因此需要看看handleFallback的逻辑。
说明:正常执行(成功)时不会回调此函数,而是回调的doOnCompleted(markOnCompleted)
handleFallback:用于处理fallback的函数:
Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
// 当发生异常时,回调此方法,该异常就是t
@Override
public Observable<R> call(Throwable t) {
// 若t就是Exception类型,那么t和e一样
// 若不是Exception类型,比如是Error类型。那就用Exception把它包起来
// new Exception("Throwable caught while executing.", t);
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 {
return handleFailureViaFallback(e);
}
}
};
下面具体看看handleBadRequestByEmittingError()对该异常HystrixBadRequestException的处理;
handleBadRequestByEmittingError():BadRequestException异常类型:
AbstractCommand:
private Observable<R> handleBadRequestByEmittingError(Exception underlying) {
Exception toEmit = underlying;
try {
long executionLatency = System.currentTimeMillis() - executionResult.getStartTimestamp();
// 请注意:这里发送的是BAD_REQUEST事件哦~~~~
eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
executionResult = executionResult.addEvent((int) executionLatency, HystrixEventType.BAD_REQUEST);
// 留个钩子:调用者可以对异常类型进行偷天换日
Exception decorated = executionHook.onError(this, FailureType.BAD_REQUEST_EXCEPTION, underlying);
// 如果调用者通过hook处理完后还是HystrixBadRequestException类型,那就直接把数据发射出去
// 若不是,那就不管,还是发射原来的异常类型
if (decorated instanceof HystrixBadRequestException) {
toEmit = decorated;
} else {
logger.warn("ExecutionHook.onError returned an exception that was not an instance of HystrixBadRequestException so will be ignored.", decorated);
}
} catch (Exception hookEx) {
logger.warn("Error calling HystrixCommandExecutionHook.onError", hookEx);
}
return Observable.error(toEmit);
}
这就是HystrixBadRequestException的特殊对待逻辑,它发出的事件类型是HystrixEventType.BAD_REQUEST,而此事件类型是不会被HealthCounts作为健康指标所统计的,因此它并不会触发熔断器。
3.4、使用场景
了解了HystrixBadRequestException的这个特性后,使用场景可根据具体业务而定喽。比如我们最为常用的场景便是在Feign上自定义一个错误解码器ErrorDecoder,然后针对于错误码是400的响应统一转换为HystrixBadRequestException异常抛出,这样是比较优雅的一种实践方案。
更多推荐
所有评论(0)