Hystrix 使用指南(2):Fallback 详解

一、概述

降级是除了熔断以外,Hystrix 的另一个重要功能。简单来说,使用 Hystrix 实现降级功能是通过覆写 HystrixCommand 中的 getFallback() ,在其中实现自定义的降级逻辑实现的。看起来很简单,但我们需要了解 getFallback() 方法的执行原理,以及在不同场景中正确实 Fallback 的方式。本篇文章就来介绍这方面的内容。

二、关于 Fallback 你需要知道的

1. 什么情况下会触发 Fallback

下面三种情况会导致 Hystrix 执行 Fallback

  1. 主方法抛出异常
  2. 主方法执行超时
  3. 线程池拒绝
  4. 断路器打开

主方法抛出异常

通常,当 HystrixCommand 的主方法(run()) 中抛出异常时,便会触发 getFallback()。除了一个例外 —— HystrixBadRequestException。当抛出 HystrixBadRequestException,不论当前 Command 是否定义了 getFallback(),都不会触发,而是向上抛出异常。

如果实现业务时有一些异常希望能够向上抛出,而不是触发 Fallback 策略,便可以封装到 HystrixBadRequestException 中。

关于 HystrixBadRequestException 另一个需要知道的是,它并不会被 Hystrix 计入失败。也就是说,不论一个 Command 抛出多少 HystrixBadRequestException,都不会导致熔断发生。

主方法执行超时

这种情况也很容易理解,在 Hystrix 中,执行超时也是失败的一种,所以同样也会触发 Fallback。

线程池拒绝

线程池拒绝指的是 HystrixCommand 所使用的线程池没有足够的线程执行本次调用。Hystrix 使用的线程池的队列大小默认为 -1。此时,队列类型为 SynchronousQueue。当线程数不足时,任务会就被拒绝。这种情况也算是一种执行失败,所以也会出发 Fallback。

断路器打开

当断路器打开时,Hystrix 会直接执行 Fallback,而不会执行主方法。

2. Fallback 的执行线程

Hystrix 用来执行 getFallback() 方法所使用的线程同执行 run() 方法使用的线程是来自同一个线程池。

因为 getFallback() 中的逻辑虽然通常为降级策略,但仍为程序功能的一部分,依旧需要使用同主逻辑相同的隔离策略。在默认情况下,即线程池隔离。

3. Fallback 是否受超时时间控制

不受。

getFallback() 的执行时间并不受 HystrixCommand 的超时时间的控制。所以,在使用 fallback 机制的时候,要避免 fallback 方法占用过多的线程。

默认情况下,为了避免 getFallback() 方法占用过多的资源,Hystrix 使用了信号量 Semaphore 对其进行资源隔离,默认并发度为10。但信号量的资源隔离的力度是有限的。

假设,如果你在 getFallback() 方法中实现重试逻辑。假如 HystrixCommand 上设置的超时时间是1秒。当熔断发生时,因为你在 getFallback() 中再次调用同一个方法,且因为 getFallback() 不受超时时间的控制。当 TPS 大于 10 时,尽管主方法没有耗尽线程,但 getFallback() 方法也会把线程池耗尽。(假设线程池使用默认10的配置)

三、Fallback 使用模式

在了解了一些关于 Fallback 必须知道的内容之后,我们再来看看在各种场景中如何使用 Hystrix。

1. 简单的 Fallback

有时,Fall back 会很简单。例如,实现用户授权检查功能,如果这个授权检查所保护的资源不是那么关键,你可以这么实现。

class AuthCommand extends HystrixCommand {
  public Boolean run() {
    return authService.authenticate(user);
  }
  
  protected Boolean getFallback() {
    return true;
  }
}

如果 authService 出现错误时,直接返回授权通过。

2. Fallback 中包含网络调用

有些时候,上面那种简单的 Fallback 策略不能满足需要。例如,查看用户订单,当服务故障时,返回一个空的订单列表并不合适,我们可以通过缓存获取订单信息,虽然可能不是最新的,但好过没有任何内容。

class GetOrderListCommand extends HystrixCommand> {
  private Long userId;
  
  GetOrderListCommand(Long userId) {
    super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteService")));
    this.userId = userId;
  }
  
  public List run() {
    return orderQueryService.list(userId);
  }
  
  protected List getFallback() {
    return new ListOrderFromCacheCommand(userId).execute();
  }
}

class ListOrderFromCacheCommand extends HystrixCommand> {
  private Long userId;
  
  ListOrderFromCacheCommand(Long userId) {
    super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteService"))
        .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("RemoteService-Fallback")));
    this.userId = userId;
  }
  
  public List run() {
    return orderListCache.get(userId);
  }
}

在上面的例子中,GetOrderListCommand 的 Fallback 策略是从分布式缓存中查询数据。因为分布式缓存查询需要通过网络,所以不能直接在 Fallback 中执行,而是需要封装为另一个 HystrixCommand。

原因在上面也提到了,HystrixCommand 的 Fallback 不受执行超时的控制,也会有熔断等功能的保护。如果直接执行网络操作,会影响正常功能的执行。

另外,重试 Command ListOrderFromCacheCommand 还有一个细节需要注意,其线程池需要和主 Command(这里是 GetOrderListCommand)的线程池不同。此例中是通过区分 Thread Pool Key 实现的。

3. 重试

重试是一个常见的提高可用性的设计,但不恰当地使用重试,也会降低可用性。例如,当依赖的服务已经发生严重故障时,再进行重试,只会使问题雪上加霜。

通过 Hystrix 实现重试,可以避免上述问题:

class RetryDemoCommand extends HystrixCommand {
  public String run() {
    return doService();
  }
  
  public String getFallback() {
    if (!isCircuitBreakerOpen()) {
      return doService();
    } else {
      return DEFAULT;
    }
  }
}

在上面的代码中,RetryDemoCommand 的 Fallback 方法先通过 isCircuitBreakerOpen() 方法判断当前 Command 断路器的状态,如果不是断路状态,才会进行重试,否则就返回默认值。

这里需要主要,如果 doService 里是一次远程调用,就需要安装上例的方式用另一个 Command 进行封装保护。

四、总结

本文介绍了关于 Hystrix Fallback 机制的细节知识和 Fallback 使用模式。希望能对大家更好地使用 Hystrix 起到帮助。

欢迎大家关注我的个人公众号:

我的技术公众号“编走编想”

你可能感兴趣的:(Hystrix 使用指南(2):Fallback 详解)