vlambda博客
学习文章列表

服务治理-Hystrix触发fallback降级逻辑的5种情况


正文

Hystrix把它处理fallabck的全部逻辑都封装在了getFallbackOrThrowException()方法里,从源码处来看只需知道有哪些地方调用了此方法便可得出答案。


触发fallback的5种情况

首先,哪些情况下可以触发Hystrix的降级逻辑这本身就是一个好问题。那为何是5种情况呢?其实这个答案从官方的Hystrix原理图中能看到触发fallback回退的地方一共有5处:图中共色字体已经标出。

其实,站在源码的角度看,此问题亦可转换一下,也可这么问:调用getFallbackOrThrowException()的地方有几处呢?如下截图也展示了,恰好也是5处:

针对这5种case,下面一一作出解释并且给出针对性的触发示例。


第一种: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);
}

示例

首先自定义一个Command:

private static class FallabckDemo extends HystrixCommand<String> {
private final String name;

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

@Override
protected String run() {
System.out.printf("健康信息:%s,断路器是否打开:%s\n", getMetrics().getHealthCounts(), circuitBreaker.isOpen());
if (name == null) {
throw new NullPointerException();
}
return "Hello " + name + "!";
}

@Override
protected String getFallback() {
Throwable e = getExecutionException(); // 导致目标方法执行失败的异常类型
if (!(e instanceof NullPointerException)) {
System.out.printf("异常类型:%s,信息:%s\n", e.getClass().getSimpleName(), e.getMessage());
}
return "this is fallback msg";
}
}

可以看到run方法中,若name为null就抛出NPE异常。下面模拟请求来触发熔断器:

@Test
public void fun1() throws InterruptedException {
// 10秒钟大于20个请求 失败数超过50%就触发熔断
// 35个请求,还可以看到半开状态哦~~
for (int i = 0; i < 35; i++) {
String name = i % 2 == 0 ? null : "demo"; // 用于模拟50%的错误率
FallabckDemo demo = new FallabckDemo(name);
demo.execute(); //同步执行

// 因为10秒内要至少放20个请求进去
// 因为第一个请求先发出再休眠,所以此处取值500ms是没有问题的
TimeUnit.MILLISECONDS.sleep(500);
}

}

这里500毫秒发一个请求,可以有很好的效果能看到熔断器打开、半开等状态,运行程序,控制台输出:

// 说明:因为输出这句话时run方法还没执行完,所以这里是0。第一个请求其实是失败哦所有抛出异常信息
健康信息:HealthCounts[0 / 0 : 0%],断路器是否打开:false
java.lang.NullPointerException: null
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:41)
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:28)
...
// 因为这次请求是正常的,所以后面就不会根有异常栈了。间隔一次错误一次,交替进行,保证错误率50%以上
健康信息:HealthCounts[1 / 1 : 100%],断路器是否打开:false
健康信息:HealthCounts[1 / 2 : 50%],断路器是否打开:false
java.lang.NullPointerException: null
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:41)
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:28)
...

...
...
健康信息:HealthCounts[9 / 17 : 52%],断路器是否打开:false
健康信息:HealthCounts[9 / 18 : 50%],断路器是否打开:false
java.lang.NullPointerException: null
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:41)
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:28)
...
健康信息:HealthCounts[10 / 19 : 52%],断路器是否打开:false
// 从这开始:断路器就开启了,所以如果请求再次进来,直接抛出异常short-circuited从而进入fallabck的逻辑
异常类型:RuntimeException,信息:Hystrix circuit short-circuited and is OPEN
异常类型:RuntimeException,信息:Hystrix circuit short-circuited and is OPEN
...
异常类型:RuntimeException,信息:Hystrix circuit short-circuited and is OPEN
// 事件过了5秒后,进入半开状态:尝试放一个请求进来。只可惜又报错了,断路器继续保持开启状态
健康信息:HealthCounts[5 / 10 : 50%],断路器是否打开:true
java.lang.NullPointerException: null
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:41)
at com.yourbatman.hystrix.TestFallback$FallabckDemo.run(TestFallback.java:28)
...
异常类型:RuntimeException,信息:Hystrix circuit short-circuited and is OPEN
异常类型:RuntimeException,信息:Hystrix circuit short-circuited and is OPEN
...

这是第一个例子,所以写得比较全面,希望读者可以认真读懂该例子,结合前面所学的熔断器原理加以理解,这才能叫真正掌握了Hystrix的熔断器原理。

顺带说明一句:为何这里的NPE异常会打印到控制台,是因为handleFailureViaFallback方法处有一句logger.debug("Error executing HystrixCommand.run(). Proceeding to fallback logic ...", underlying);,而我的日志级别是debug,所以控制台里都会打印run里面的异常信息~

另外,下面的异常类型因为不是run方法里面的,所以默认是不会打印输出的哦


第二种: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);
}

示例

针对上例做出些许改变:

1for循环放任务时,改成queue()异步的:`demo.queue()`

说明:queue()方法调用后,run方法/fallback方法也都是立马会执行的哦,只是它们是异步去执行,不会阻塞主线程而已

这样子的话,线程池就会被立即打满(比较默认只有10个)

运行测试程序,可以看到控制台就一直抛出RejectedExecutionException异常:

健康信息:HealthCounts[0 / 0 : 0%],断路器是否打开:false
异常类型:RejectedExecutionException,信息:Task java.util.concurrent.FutureTask@52aa2946 rejected from java.util.concurrent.ThreadPoolExecutor@4de5031f[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 0]
...

第三种: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);
}

示例

类同于线程池方式,略。


第四种: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());
}

示例

模拟这个case非常的简单,只需让run方法睡一会即可达到目的:TimeUnit.SECONDS.sleep(2);

然后书写测试用例:

@Test
public void fun3() {
FallabckDemo demo = new FallabckDemo("name");
String result = demo.execute();
System.out.println(result);
}

运行程序,输出:

异常类型:HystrixTimeoutException,信息:null
this is fallback msg

抛出超时异常HystrixTimeoutException,正常fallback。


第五种: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类型除外);


示例

第一个示例演示的就是NPE异常,它最终便会经过handleFailureViaFallback()处理,具体示例代码本处略,建议初学者有兴趣可自行动手书写。


思考:若fallback方法内执行时抛出异常了呢?

首先,官方建议fallabck里返回的是常量/缓存里的值(比如Map里的值),所以fallback里出现异常的理应几乎为0。但建议总归是建议,若你真要在里面写复杂逻辑:比如通过RPC去获取数据,那错误率就高了。那么问题来了:万一出现此情况,是何表现呢???

这道题留给读者自行思考,建议自己动手写个示例来证明你的猜想,比较say say easy,do do hard。至于其原理,请参考前一篇内容的讲解,那里有你想要的原理解释。


总结

关于Hystrix触发fallback降级逻辑的5种情况就介绍到这了。本文内容还是比较全面的,针对于各种情况都给出了对应的触发示例代码,相信这样对你理解起来更加的无障碍些。

针对学习提个小小建议:建议多动手实践才能把知识真正掌握,毕竟很多知识都是一听就会,一做就错的。