[享学Netflix] 三十一、Hystrix触发fallback降级逻辑的5种情况及代码示例

2020-03-18 19:41:42 浏览数 (3)

压力、挑战,所有“消极的东西”都是成功的催化剂。 代码下载地址:https://github.com/f641385712/netflix-learning

目录
  • 前言
  • 正文
    • 触发fallback的5种情况
      • 第一种:short-circuited短路
        • 示例
      • 第二种:threadpool-rejected线程池拒绝
        • 示例
      • 第三种:semaphore-rejected信号量拒绝
        • 示例
      • 第四种:timed-out超时
        • 示例
      • 第五种:failed执行失败
        • 示例
    • 思考:若fallback方法内执行时抛出异常了呢?
  • 总结
    • 声明

前言

上篇文章详细介绍了Hystrix的fallback降级逻辑,并且深入分析了其源码实现:getFallbackOrThrowException()方法,但我在文末留下了一个小问题:Hystrix中哪些情况会触发它的降级逻辑呢?

带着这个疑问开始这篇文章的内容,本文将详细为你介绍触发Hystrix执行fallback逻辑的5种case,并且分别给出示例代码,让你既能学到,又能用到。


正文

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()
代码语言:javascript复制
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:

代码语言:javascript复制
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,断路器是否打开:%sn", 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,信息:%sn", e.getClass().getSimpleName(), e.getMessage());
        }
        return "this is fallback msg";
    }
}

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

代码语言:javascript复制
@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毫秒发一个请求,可以有很好的效果能看到熔断器打开、半开等状态,运行程序,控制台输出:

代码语言:javascript复制
// 说明:因为输出这句话时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)
    • 因异常由方法“外部”抛出,所以此方法有入参
代码语言:javascript复制
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);
    }

示例

针对上例做出些许改变:

代码语言:javascript复制
1、for循环放任务时,改成queue()异步的:`demo.queue()`

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

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

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

代码语言:javascript复制
健康信息: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的异常,所以木有入参
代码语言:javascript复制
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()
代码语言:javascript复制
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);

然后书写测试用例:

代码语言:javascript复制
@Test
public void fun3() {
    FallabckDemo demo = new FallabckDemo("name");
    String result = demo.execute();
    System.out.println(result);
}

运行程序,输出:

代码语言:javascript复制
异常类型:HystrixTimeoutException,信息:null
this is fallback msg

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


第五种:failed执行失败
  • 触发条件command执行失败,也就是你的run方法里执行失败(抛出了运行时异常)时,执行此部分逻辑
  • 异常类型:run方法里的任意运行时异常类型,比如NPE异常
  • 对应方法名handleFailureViaFallback()
代码语言:javascript复制
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种情况就介绍到这了。本文内容还是比较全面的,针对于各种情况都给出了对应的触发示例代码,相信这样对你理解起来更加的无障碍些。

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

0 人点赞