spring-cloud-hystrix服务容错、熔断器、使用详解、请求合并

  Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

  “断路器” 本身是一种开关设置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的,可处理的备选相应(fallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会长时间,不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

  在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),如下图:

   在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等.

   如下图:QPS为50的依赖 I 出现不可用,但是其他依赖仍然可用.

  当依赖I 阻塞时,大多数服务器的线程池就出现阻塞(BLOCK),影响整个线上服务的稳定性.如下图:

  在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险。

  解决问题方案:对依赖做隔离,Hystrix就是处理依赖隔离的框架,同时也是可以帮我们做依赖服务的治理和监控.

服务熔断:

  熔断机制是应对雪崩效应的一种微服务链路保护机制。

  当扇出链路(即上面的图二)的某个微服务(I)不可用或者响应时间太长,会进行服务的降级,进而熔断该节点微服务的调用,快速返回"错误"的响应信息。当检测到该节点微服务调用响应正常回复后恢复调用链路。在springCloud框架里熔断机制通过Hystrix实现。Hystrix会监控微服务服务间调用的状况,当失败的调用到达一定的阈值,缺省是5秒内20次调用失败就会启动熔断机制,熔断机制的注解是 @HystrixCommand

  Hystrix中的三种降级方案

  1. 熔断触发降级
  2. 请求超时触发降级
  3. 资源隔离触发降级

  基于springcloud 学习中 Ribbon的demo进行改造 :https://www.cnblogs.com/wuzhenzhao/category/1529973.html

  熔断触发降级:

1.pom文件修改 添加以下依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2.application.yml 不需要修改修改

3. 修改Controller 接口 ,添加注解 @HystrixCommand

@RestController
public class RibbonController {

    //  private static final String REST_URL_PREFIX="http://localhost:8001"; 单机版
    //集群的时候  需要配置该服务在eureka里注册的名字
    private static final String REST_URL_PREFIX="http://cloud-provider";

    @Autowired
    private RestTemplate restTemplate;
  //同步   @HystrixCommand(commandProperties = {     //HystrixCommandProperties 类中包含配置信息所有     //开启熔断     @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),     //最小请求数     @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),     //熔断5秒     @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),     //10秒内 最少请求 5次。若百分比超过 50 则触发熔断     @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")   }, fallbackMethod = "processHystrix_Get")//熔断机制 @RequestMapping(value ="/hello") public String get(Long id) { Map map =new HashMap<>(); map.put("id",id); return restTemplate.getForObject(REST_URL_PREFIX+"/hello?id={id}", String.class,map); } //消费端可以调用服务发现 @RequestMapping(value ="/discovery") public Object discovery() { return restTemplate.getForObject(REST_URL_PREFIX+"/discovery", Object.class); } public String processHystrix_Get(Long id) { return "hello Hystrix"; } }

  修改原来的cloud-provider服务:

@GetMapping("/hello")
public String helloEureka(String id){
  if (StringUtils.isEmpty(id)) {
    throw new RuntimeException();
  }
  return  "Hello Eureka Provider";
}

4.修改主启动类

@EnableDiscoveryClient
@SpringBootApplication
//自定义负载均衡算法 自定义配置类不能跟主启动类一个包或在子包下面
//name: 表示对哪个服务采用自定义算法
//configuration:负载算法类
@RibbonClient(name="cloud-provider")
@EnableCircuitBreaker // 对Hystrix熔断机制的支持
public class RibbonApp {
    private final static Logger log = LoggerFactory.getLogger(RibbonApp.class);

    public static void main(String[] args) {
        SpringApplication.run(RibbonApp.class,args);
        log.info("服务启动成功");

    }
}

  这样就配置好了服务熔断,当某个接口发生异常时,就会跳转进配置的方法。

  请求超时触发熔断:

1.在客户端新增一个接口:

@HystrixCommand(fallbackMethod = "timeoutFallback", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
    })
    @GetMapping("/hystrix/timeout")
    public String queryTimeout() {
        return restTemplate.getForObject(REST_URL_PREFIX + "/timeout", String.class);
    }

    public String timeoutFallback() {
        return "timeOut Hystrix";
}

2.在服务提供方新增测试接口:

@RequestMapping(value = "/timeout", method = RequestMethod.GET)
    public String timeout() throws InterruptedException {
        Thread.sleep(4000);
        return "Hello Eureka Provider";
    }

  启动测试即可

  资源隔离熔断:资源隔离熔断可以分为 信号量隔离和线程池隔离。

/**
* 信号量隔离实现
* 不会使用Hystrix管理的线程池处理请求。使用容器(Tomcat)的线程处理请求逻辑。
* 不涉及线程切换,资源调度,上下文的转换等,相对效率高。
* 信号量隔离也会启动熔断机制。如果请求并发数超标,则触发熔断,返回fallback数据。
* commandProperties - 命令配置,HystrixPropertiesManager中的常量或字符串来配置。
* execution.isolation.strategy - 隔离的种类,可选值只有THREAD(线程池隔离)和SEMAPHORE(信号量隔离)。默认是THREAD线程池隔离。
* 设置信号量隔离后,线程池相关配置失效。
* execution.isolation.semaphore.maxConcurrentRequests - 信号量最大并发数。默认值是10。常见配置500~1000。
* 如果并发请求超过配置,其他请求进入fallback逻辑。
*   
*/
@HystrixCommand(fallbackMethod = "semaphoreFallback",
            commandProperties = {
                    // 信号量隔离
                    @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
                    // 信号量最大并发数
                    @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "5"),
         @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_TIMEOUT_ENABLED, value = "true"), @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "3000") } ) @GetMapping("/hystrix/semaphore") public String semaphore() { Map map = new HashMap<>(); map.put("id", "1"); return restTemplate.getForObject(REST_URL_PREFIX + "/hello?id={id}", String.class, map); } @HystrixCommand( // groupKey = "order-service", commandKey = "queryOrder", threadPoolKey = "order-service",   threadPoolProperties = { @HystrixProperty(name = "coreSize", value = "30"),//线程池大小 @HystrixProperty(name = "maxQueueSize", value = "100"),//最大队列长度 @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),//线程存活时间 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15")//拒绝请求   },   commandProperties = { // 隔离 @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY, value = "THREAD"), @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_TIMEOUT_ENABLED, value = "true"), @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_INTERRUPT_ON_TIMEOUT, value = "3000"),   },   fallbackMethod = "semaphoreFallback") @GetMapping("/hystrix/thread") public String thread() { Map map = new HashMap<>(); map.put("id", "1"); return restTemplate.getForObject(REST_URL_PREFIX + "/hello?id={id}", String.class, map); } public String semaphoreFallback() { System.out.println("semaphore Hystrix"); return "semaphore Hystrix"; } public String threadFallback() { return "thread Hystrix"; }

  基于资源隔离我们可能需要借助 dashboard 来看效果,首先要开启 management.endpoints.web.exposure.include=refresh,hystrix.stream,然后通过Jmeter进行压测,就可以看到效果

 

  基于Feign 客户端的熔断实现:

@FeignClient(value = "cloud-provider", fallbackFactory = HystrixClientService.class)
public interface ClientService {
    //如果feign代理的是get请求,必须用@RequestMapping 不能用@GetMapping
    // 每个参数必须带上@RequestParam,否则会报post not support!
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    String hello(@RequestParam("id") String id) throws InterruptedException;
}

@Component
public class HystrixClientService implements FallbackFactory<ClientService> {
    @Override
    public ClientService create(Throwable throwable) {
        return new ClientService() {
            @Override
            public String hello(String id) {
                System.out.println("feign 服务降级");
                return "feign 服务降级";
            }
        };
    }
}

  然后在引用该Feign服务的项目中开启feign支持@EnableFeignClients(basePackages = "com.wuzz.demo")、@EnableCircuitBreaker // 对Hystrix熔断机制的支持。这样即可

  基于Feign 客户端的资源隔离熔断实现:引入相关依赖,即Feign Service  的依赖,然后

@Autowired
private ClientService clientService;

@GetMapping("/hystrix/feign")
public String feign() throws InterruptedException {
    return clientService.hello("11115");
}

  接着开启 Feign 熔断、及Feign Service 的方法级别的熔断策略

feign:
  hystrix:
    enabled: true
hystrix:
  command:
    default:  #全局配置, feignclient#method(param)
      execution:
        timeout:
          enable: true
        isolation:
          thread:
            timeoutInMilliseconds: 3000
#    ClientService#hello(String):
#      execution:
#        isolation:
#          strategy: SEMAPHORE
#          semaphore:
#            maxConcurrentRequests: 10
    ClientService#hello(String):
      execution:
        isolation:
          strategy: THREAD
  threadpool:
    cloud-provider:
      coreSize: 2
      maxQueueSize: 1000
      queueSizeRejectionThreshold:  800

  测试结果如下:

使用详解:

  上文中我们通过Hystrix 中的核心注解 @HystrixCommand, 通过它创建了 HystrixCommand 的实现,同时利用 fallback 属性指定了服务降级的实现方法。然而这些还只是 Hystrix 使用的一 小部分,在实现 一 个大型分布式系统时,往往还需要更多高级的配置功能。 接下来我们将详细介绍 Hystrix 各接口和注解的使用方法。创建请求命令:

  Hystrix 命令就是我们之前所说的 HystrixCommand, 它用来封装具体的依赖服务调用逻辑。我们可以通过继承的方式来实现, 比如:

public class HelloCommand extends HystrixCommand<String> {

    private RestTemplate restTemplate;

    private HashMap map;

    public HelloCommand(RestTemplate restTemplate, HashMap paramMap) {
        super(com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(
                HystrixCommandGroupKey.Factory.asKey("")).andCommandPropertiesDefaults(
                HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(5000)));
        this.restTemplate = restTemplate;
        this.map = paramMap;
    }

    @Override
    protected String run() {
        return restTemplate.getForObject("http://cloud-provider/hello?id={id}", String.class, map);
    }
  // 服务降级
    @Override
    protected String getFallback() {
        return "error-err";
    }
}

   通过上面实现的HelloCommand , 我们既可以实现请求的同步执行也可以实现异步执行。除了传统的同步执行与异步执行之外, 我们还可以将 HystrixComrnand 通过Observable 来实现响应式执行方式。通过调用 observe()和toObservable ()方法可以返回 Observable 对象observe ()和toObservable ()虽然都返回了 Observable, 但是它们略有不同,前者返回的是一 个Hot Observable, 该命令会在 observe ()调用的时候立即执行, 当Observable 每次被订阅的时候会重放它的行为;而后者返回的是一 个Cold Observable,toObservable ()执行之后,命令不会被立即执行,只有当所有订阅者都订阅它之后才会执行。

//继承HystrixCommand的实现
@RequestMapping(value = "/helloCommand")
public String helloCommand(Long id) {
  HashMap map = new HashMap<>();
  map.put("id", id);
  //同步
  String result = new HelloCommand(restTemplate, map).execute();
  //异步
//        Future<String> result = new HelloCommand(restTemplate, map).queue();
        //响应式执行方式
//        Observable<String> hotObserve = new HelloCommand(restTemplate, map).observe();
//        Observable<String> coldObservable = new HelloCommand(restTemplate, map).toObservable();
  return result;
}

   异步执行的时候, 可以通过对返回的 result 调用 get 方法来获取结果。另外, 也可以通过 上文@HystrixCommand 注解来更为优雅地实现 Hystrix 命令的定义,虽然 @HystrixCommand 注解可以非常优雅地定义 Hystrix 命令的实现, 但是如上定义的 get 方式只是同步执行的实现,若要实现异步执行则还需另外定义,比如:

//异步
@HystrixCommand(fallbackMethod = "getByidAsyncFailed")//熔断机制
@RequestMapping(value = "/getByidAsync")
public String getUserByidAsync(String id) {
        HashMap map = new HashMap<>();
        map.put("id", id);
        AsyncResult<String> asyncResult = new AsyncResult<String>() {
            @Override
            public String invoke() {
                return restTemplate.getForObject(REST_URL_PREFIX + "/hello?id={id}", String.class, map);
            }

            @Override
            public String get() {
                return invoke();
            }
        };
        return asyncResult.get();
}

  虽然 HystrixCornrnand 具备了 observe ()和toObservable() 的功能,但是它的实现有 一 定的局限性,它返回的 Observable 只能发射 一 次数据,所以 Hystrix 还提供了另外 一 个特殊命令封装 HystrixObservableCornrnand, 通过它实现的命令可以获取能发射多次的 Observable 。如果使用 HystrixObservableCornrnand 来实现命令封装,需要将命令的执行逻辑在construct 方法中重载,这样 Hystrix 才能将具体逻辑包装到 Observable 内,如下所示:

public class HelloObservableCommand extends HystrixObservableCommand<String> {
    private RestTemplate restTemplate;

    private HashMap map;

    public HelloObservableCommand(RestTemplate restTemplate, HashMap paramMap) {
        super(com.netflix.hystrix.HystrixObservableCommand.Setter.withGroupKey(
                HystrixCommandGroupKey.Factory.asKey("")).andCommandPropertiesDefaults(
                HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(5000)));
        this.restTemplate = restTemplate;
        this.map = paramMap;
    }

    @Override
    protected Observable<String> construct() {
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> observer) {
                try {
                    if (!observer.isUnsubscribed()) {
                        String string = restTemplate.getForObject("http://cloud-provider/hello?id={id}", String.class, map);
                        observer.onNext(string);
                        observer.onCompleted();
                    }
                } catch (Exception e) {
                    observer.onError(e);
                }
            }
        });
    }
}

  而对此的注解实现依然是使用 @HystrixCommand, 只是方法定义需要做 一 些变化,具体内容与 construct ()的实现类似,如下所示:

//HystrixObservableCommand
//EAGER 是该参数的模式值, 表示使用 observe ()执行方式。
@HystrixCommand(fallbackMethod = "getByidAsyncFailed", observableExecutionMode = ObservableExecutionMode.EAGER)
//    //表示使用 toObservable() 执行方式。
//    @HystrixCommand(fallbackMethod = "getByidAsyncFailed",observableExecutionMode = ObservableExecutionMode.LAZY)
@RequestMapping(value = "/helloHystrixObservableCommand")
public Observable<String> helloHystrixObservableCommand(String id) {
        HashMap map = new HashMap<>();
        map.put("id", id);
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> observer) {
                try {
                    if (!observer.isUnsubscribed()) {
                        String string = restTemplate.getForObject("http://cloud-provider/hello?id={id}", String.class, map);
                        observer.onNext(string);
                        observer.onCompleted();
                    }
                } catch (Exception e) {
                    observer.onError(e);
                }
            }
        });
}

异常处理:

  异常传播:

  在 HystrixComrnand 实现的 run() 方法中抛出异常时, 除了 HystrixBadRequestException 之外,其他异常均会被 Hystrix 认为命令执行失败并触发服务降级的处理逻辑,所以当需要在命令执行中抛出不触发服务降级的异常时来使用它。而在使用注册配置实现 Hystrix 命令时,它还支持忽略指定异常类型功能, 只需要通过设置 @HystrixComrnand 注解的 ignoreExceptions 参数, 比如:@HystrixCommand(ignoreExceptions = {BadRequestException.class}).当方法抛出了类型为 BadRequestException的异常时, Hystrix 会将它包装在 HystrixBadRequestException 中抛出, 这样就不会触发后续的 fallback 逻辑。

  异常获取:

  当 Hystrix 命令因为异常(除了 HystrixBadRequestException 的异常)进入服务降级逻辑之后, 往往需要对不同异常做针对性的处理, 那么我们如何来获取当前抛出的异常呢?在以传统继承方式实现的 Hystrix 命令中, 我们可以用 getFallback ()方法通过 getExecutionException() 方法来获取具体的异常, 通过判断来进入不同的处理逻辑。

  除了传统的实现方式之外,注解配置方式也同样可以实现异常的获取。 它的实现也非常简单, 只需要在 fallback 实现方法的参数中增加 Throwable e 对象的定义, 这样在方法内部就可以获取触发服务降级的具体异常内容了, 比如:fallbackl(Throwable e)

请求缓存:

  在高并发的场景之下, Hystrix 中提供了请求缓存的功能, 我们可以方便地开启和使用请求缓存来优化系统, 达到减轻高并发时的请求线程消耗、 降低请求响应时间的效果。

  开启请求缓存功能 :Hystrix 请求缓存的使用非常简单, 我们只需要在实现 HystrixCommand 或 HystrixObservableCommand 时, 通过重载 getCacheKey ()方法来开启请求缓存。通过开启请求缓存可以让我们实现的 Hystrix 命令具备下面几项好处:

  • 减少重复的请求数, 降低依赖服务的并发度。
  • 在同一 用户请求的上下文中, 相同依赖服务的返回数据始终保持 一 致。
  • 请求缓存在 run() 和 construct ()执行之前生效, 所以可以有效减少不必要的线程开销。

  清理失效缓存功能

  清除缓存有两个方式

//刷新缓存,根据id进行清理 自己写一个flush方法。通过idzuoweikey清除
HystrixRequestCache.getInstance(GETTER_KEY,HystrixConcurrencyStrategyDefault.getinstance()).clear(String.valueOf(id));
//刷新缓存, 清理缓存中失效的User,直接调用flush方法
HelloCommand.flushCache(id);

请求合并:

  微服务架构中的依赖通常通过远程调用实现, 而远程调用中最常见的问题就是通信消耗与连接数占用。 在高并发的情况之下, 因通信次数的增加, 总的通信时间消耗将会变得不那么理想。 同时, 因为依赖服务的线程池资源有限,将出现排队等待与响应延迟的清况。为了优化这两个问题, Hystrix 提供了 HystrixCollapser 来实现请求的合并,以减少通信消耗和线程数的占用。

  HystrixCollapser 实现 了在 HystrixCommand 之前放置 一 个合并处理器, 将处于一个很短的时间窗(默认 10 毫秒)内对同 一 依赖服务的多个请求进行整合并以批量方式发起请 求 的功能(服 务提供方也需 要 提供相应的批 量实 现 接口)。 通 过HystrixCollapser 的封装, 开发者不需要关注线程合并的细节过程, 只需关注批量化服务和处理。 下面我们从 HystrixCollapser 的使用实例 中对其合并请求的过程 一 探究竟。

public abstract class HystrixCollapser<BatchReturnType, ResponseType, RequestArgumentType> implements HystrixExecutable<ResponseType>, HystrixObservable<ResponseType> {
     //BatchReturnType: 合并后批量请求的返回类型。   // ResponseType: 单个请求返回的类型。   //RequestArgumentType: 请求参数类型。
  //该函数用来定义获取请求参数的方法。   public abstract RequestArgumentType getRequestArgument();
  //合并请求产生批量命令的具体实现方法。   protected abstract HystrixCommand<BatchReturnType> createCommand(Collection<CollapsedRequest<ResponseType, RequestArgumentType>> requests);
  //批量命令结果返回后 的处理, 这里需要实现将批量结果拆分并传递给合并前的各个原子请求命令的逻辑。   protected abstract void mapResponseToRequests(BatchReturnType batchResponse, Collection<CollapsedRequest<ResponseType, RequestArgumentType>> requests); }

   接下来, 我们通过 一 个简单的示例来直观理解实现请求合并的过程。

  首先在我们原来的服务提供者 cloud-provider工程中加入一个批量获取的接口,那么现在两个接口如下:

@GetMapping("/hello")
public String helloEureka(){
  return  "Hello Eureka Provider1";
}

@GetMapping("/hi")
public List<String> hi(String ids) {
  //ids是 , 隔开的字符串
  String[] split = ids.split(",");
  ArrayList<String> objects = new ArrayList<String>();
  for(String s:split){
    objects.add("hi! wuzz:ID: " + s);
  }
  return objects;
}

   创建一个独立的消费者服务,用于通过 RestTemplate 实现了简单的调用

@Service
public class HelloCollapseService {

    @Autowired
    private RestTemplate restTemplate;

    private static final String REST_URL_PREFIX = "http://cloud-provider";

    //同步
    public String hello(String id) {
        return restTemplate.getForObject(REST_URL_PREFIX + "/hello/{1}", String.class, id);
    }

    //同步
    public List<String> hi(List<String> ids) {
        String[] forObject = restTemplate.getForObject(REST_URL_PREFIX + "/hi?ids={1}", String[].class, StringUtils.join(ids, ","));
        return Arrays.asList(forObject);
    }
}

   接着, 我们实现将短时间内多个获取单一对象的请求命令进行合并。第 一 步,为请求合并的实现准备 一 个批量请求命令的实现, 具体如下:

//为请求合并的实现准备 一 个批量请求命令的实现
//批量请求命令实际上就是 一 个简单的HystrixCommand实现
public class HelloBatchCommand extends HystrixCommand<List<String>> {

    private HelloCollapseService helloCollapseService;

    private List<String> ids;

    public HelloBatchCommand(HelloCollapseService helloCollapseService, List<String> ids) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("helloBatchCommand")));
        this.helloCollapseService = helloCollapseService;
        this.ids = ids;
    }

    @Override
    protected List<String> run() {
        //这段打印用域等等测试,查看是否是调用这个接口去服务获取数据的
        System.out.println("finaAll request:---------" + ids + "Thread.currentThread().getName():-------" + Thread.currentThread().getName());
        return helloCollapseService.hi(ids);
    }

    @Override
    protected List<String> getFallback() {
        List<String> users = new ArrayList<String>();
        users.add("失败者");
        return new ArrayList<String>(users);
    }
}

  批量请求命令实际上就是 一 个简单的HystrixCommand实现,从上面的实现中可以看到它通过调用 helloCollapseService.hi(ids) 来批量获取结果。

  第二步, 通过继承HystrixCollapser实现请求合并器,关于这个类的定义以及需要实现的方法已经在上面说明:

//通过继承HystrixCollapser实现请求合并器
public class HelloCollapseCommand extends HystrixCollapser<List<String>, String, String> {

    private HelloCollapseService helloCollapseService;

    private String id;

    public HelloCollapseCommand(HelloCollapseService helloCollapseService, String id) {
        super(Setter.withCollapserKey(HystrixCollapserKey.Factory.asKey("helloCollapseCommand"))
                .andCollapserPropertiesDefaults(
                        HystrixCollapserProperties.Setter()
                                .withTimerDelayInMilliseconds(100)));
        this.helloCollapseService = helloCollapseService;
        this.id = id;
    }

    @Override
    public String getRequestArgument() {
        return id;
    }

    @Override
    protected HystrixCommand<List<String>> createCommand(Collection<CollapsedRequest<String, String>> collapsedRequests) {
        List<String> userids = new ArrayList<>(collapsedRequests.size());
        userids.addAll(collapsedRequests.stream().map(CollapsedRequest::getArgument).collect(Collectors.toList()));
        return new HelloBatchCommand(helloCollapseService, userids);
    }

    @Override
    protected void mapResponseToRequests(List<String> batchResponse, Collection<CollapsedRequest<String, String>> collapsedRequests) {
        int count = 0;
        for (CollapsedRequest<String, String> collapsedRequest : collapsedRequests) {
            String user
                    = batchResponse.get(count++);
            collapsedRequest.setResponse(user);
        }
    }
}

  最后创建测试类,从以下这个测试方法可以看出,我们想要的结果是一共发送了两次请求,一次是6、5、9作为批量的请求。由于程序sleep了 3秒,而我们设置的时间间隔为1秒,所以这里8这个ID的请求会单独发送:

@RequestMapping(value = "/batchHello")
public List<String> batchHello() throws InterruptedException, ExecutionException {
        //需要开启HystrixRequest上下文,合并请求和缓存必须开启
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        List<String> result = new ArrayList<>();
        HelloCollapseCommand bc1 = new HelloCollapseCommand(helloCollapseService, "6");
        HelloCollapseCommand bc2 = new HelloCollapseCommand(helloCollapseService, "9");
        HelloCollapseCommand bc3 = new HelloCollapseCommand(helloCollapseService, "5");
        HelloCollapseCommand bc4 = new HelloCollapseCommand(helloCollapseService, "8");
        Future<String> q1 = bc1.queue();
        Future<String> q2 = bc2.queue();
        Future<String> q3 = bc3.queue();
        String result1 = q1.get();
        String result2 = q2.get();
        String result3 = q3.get();
        Thread.sleep(3000);
        Future<String> q4 = bc4.queue();
        String result4 = q4.get();

        return result;
}

   启动服务调用 http://localhost:9001/batchHello ,然后查看控制台,可以看到结果是我们所预期的:

 

注解的方式实现请求合并:

  在原来的 HelloCollapseService 上做改动,增加find、findAll方法如下:

@Service
public class HelloCollapseService {

    @Autowired
    private RestTemplate restTemplate;

    private static final String REST_URL_PREFIX = "http://cloud-provider";

    //同步
    public String hello(String id) {
        return restTemplate.getForObject(REST_URL_PREFIX + "/hello/{1}", String.class, id);
    }

    //同步
    public List<String> hi(List<String> ids) {
        String[] forObject = restTemplate.getForObject(REST_URL_PREFIX + "/hi?ids={1}", String[].class, StringUtils.join(ids, ","));
        return Arrays.asList(forObject);
    }

    @HystrixCollapser(batchMethod = "findAll",
            collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")})
    public Future<String> find(String id) {
        throw new RuntimeException("This method body should not be executed");
    }

    @HystrixCommand(fallbackMethod = "annotationBatchHelloBack")
    public List<String> findAll(List<String> ids) {
        System.out.println("Annotation---------" + ids + "Thread.currentThread().getName():" + Thread.currentThread().getName());
        String[] users = restTemplate.getForObject(REST_URL_PREFIX + "/hi?ids={1}", String[].class, StringUtils.join(ids, ","));
        return Arrays.asList(users);
    }

    public List<String> annotationBatchHelloBack(List<Long> ids) {
        return Arrays.asList("annotationBatchHelloBack Hystrix" +ids);
    }
}

   增加测试方法:

@RequestMapping(value = "/annotationBatchHello")
public String find(String id) throws ExecutionException, InterruptedException {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        Future<String> stringFuture = helloCollapseService.find(id);
        Future<String> stringFuture2 = helloCollapseService.find("6");
        return stringFuture.get()+"======"+stringFuture2.get();
}

   这个时候我访问的路径变为  http://localhost:9001/annotationBatchHello?id=88 应该看到的结果是 88 +6 两个ID组成一个批量请求发送,如下图所示:

  下图展示了在未使用HystrixCollapser请求合并器之前的线程使用情况。 可以看到, 当服务消费者同时对RIBBON-SERVER的 /hello/{id}接口发起了5个请求时, 会向该依赖服务的独立线程池中申请5个线程来完成各自的请求操作。

  而在使用了HystrixCollapser请求合并器之后, 相同情况下的线程占用如下图所示。由于同一 时间发生的5个请求处于请求合并器的 一 个时间窗内,这些发向/hello/{id}接口的请求被请求合并器拦截下来, 并在合并器中进行组合, 然后将这些请求合并成 一 个请求发向 CLOUD-PROVIDER 的批量接口 /hi/{ids} 。在获取到批量请求结果之后,通过请求合并器再将批量结果拆分并分配给每个被合并的请求。 从图中我们可以看到, 通过使用请求合并器有效减少了对线程池中资源的占用。 所以在资源有效并且短时间内会产生高并发请求的时候, 为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化。

 

  请求合并的额外开销:

  虽然通过请求合并可以减少请求的数量以缓解依赖服务线程池的资源, 但是在使用的时候也需要注意它所带来的额外开销: 用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高。 比如, 某个请求不通过请求合并器访问的平均耗时为5ms, 请求合并的延迟时间窗为10ms (默认值), 那么当该请求设置了请求合并器之后, 最坏情况下(在延迟时间窗结束时才发起请求)该请求需要 15ms才能完成。由于请求合并器的延迟时间窗会带来额外开销, 所以我们是否使用请求合并器需要 根据依赖服务调用的实际情况来选择, 主要考虑下面两个方面。

  • 请求命令本身的延迟。 如果依赖服务的请求命令本身是 一 个高延迟的命令, 那么可以使用请求合并器, 因为延迟时间窗的时间消耗显得微不足道了。
  • 延迟时间窗内的并发量。 如果 一 个时间窗内只有1-2个请求, 那么这样的依赖服务不适合使用请求合并器。 这种情况不但不能提升系统性能, 反而会成为系统瓶颈,因为每个请求都需要多消耗 一 个时间窗才响应。 相反, 如果 一 个时间窗内具有很高的并发量, 并且服务提供方也实现了批量处理接口, 那么使用请求合并器可以有效减少网络连接数量并极大提升系统吞吐量, 此时延迟时间窗所增加的消耗就可以忽略不计了。

属性详解:

  我们可以根据实现 HystrixCommand 的不同方式将配置方法分为如下两类。

  当通过继承的方式实现时, 可使用 Setter 对象来对请求命令的属性进行设置, 比如下面的例子:

public HelloCommand(RestTemplate restTemplate, HashMap paramMap) {
  super(com.netflix.hystrix.HystrixCommand.Setter.withGroupKey(
    HystrixCommandGroupKey.Factory.asKey("helloCommand")).andCommandPropertiesDefaults(
    HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(5000)));
  this.restTemplate = restTemplate;
  this.map = paramMap;
}

  当通过注解的方法实现时, 只需使用 @HystrixCommand 中的 command­ Properties 属性来设置, 比如:

@HystrixCollapser(batchMethod = "findAll",
            collapserProperties = {@HystrixProperty(name = "timerDelayInMilliseconds", value = "100")})

  具体的参数配置还可以分为

  • Command属性:主要用来控制HystrixCommand命令的行为。包括隔离策略配置、超时时间配置等。
  • fallback配置:用来控制HystrixComrnand.getFallback ()的执行。 这些属性同时适用于线程池的信号量的隔离策略。包括getFallback 方法执行的最大并发数、服务降级策略是否启用等。
  • circuitBreaker配置:用来控制HystrixCircuitBreaker的行为。包括熔断最小请求数、休眠时间窗等。
  • metrics配置:与 HystrixCommand 和 HystrixObservableCommand 执行中捕获的指标信息有关。包括滚动时间窗的长度、滚动时间窗统计指标信息时划分“桶”的数量等
  • requestContext配置:涉及HystrixCommand使用的HystrixRequestContext的设置。包括是否开启请求缓存,是否执行和事件记录到日志等。
  • collapser属性:用来控制命令合并相关的行为。包括一 次请求合并批处理中允许的最大请求数、每个命令延迟的时间等。
  • threadPool属性:用来控制Hy strix命令所属线程池的配置。包括执行命令线程池的核心线程数,该值也就是命令执行的最大并发量等。

  更加详细的信息可以参阅 《Spring Cloud微服务实战》。参考 HystrixCommandProperties 类。

原理分析(摘自《Spring Cloud 微服务实战》):

  通过上面的快速入门示例,我们对Hystrix的使用场景 和使用方法已经有了一 个基础的认识。 接下来我们通过解读NetflixHystrix官方的流程图来详细了解一 下:当 一 个请求调用了相关服务依赖之后Hystrix是如何工作的(即如上例中所示,当访问了http://localhost:9001/hello请求之后, 在RIBBON-SERVER中是如何处理的)。

  工作流程:

  下面我们根据图中标记的数字顺序来解释每 一 个环节的 详细内容。

 1. 创建HystrixCommand或HystrixObservableCommand对象:

  首先,构建 一 个HystrixCommand或是HystrixObservableCommand对象,用来表示对依赖服务的操作请求, 同时传递所有需要的参数。 从其命名中我们就能知道它采用了“ 命令模式 ”来实现对服务调用操作的封装。关于命令模式可以参考:https://www.cnblogs.com/wuzhenzhao/p/12557341.html

   而这两个 Command 对象分别针对不同的应用场景。

  • HystrixCommand: 用在依赖的服务返回单个操作结果的时候。
  • HystrixObservableCommand: 用在依赖的服务返回多个操作结果的时候。

  命令模式主要包含四种角色:

  • 接收者角色(Receiver) :该类负责具体实施或执行一个请求;
  • 命令角色(Command) :定义需要执行的所有命令行为;
  • 具体命令角色(Concrete Command) 该类内部维护一个接收者(Receiver) 在其execute()方法中调用Receiver的相关方法;
  • 请求者角色(Invoker) :接收客户端的命令, 并执行命令。

  我们可以看到,调用者Invoker与操作者Receiver通过 Command命令接口实现了解耦。对于调用者来说, 我们可以为其注入多个命令操作, 比如新建文件、复制文件、 删除文件这样三个操作, 调用者只需在需要的时候直接调用即可, 而不需要知道 这些操作命令实际是如何实现的。 而在这里所 提到的 HystrixComrnand 和 HystrixObservableComrnand 则是在 Hystrix中对 Command 的进 一 步抽象定义。

  从上面的命令模式示例中我们也可以发现, Invoker和 Receiver 的关系非常类似于 “ 请求-响应 ” 模式, 所以它比较适用于实现记录日志、 撤销操作、 队列请求等。在下面这些情况下应考虑使用命令模式。

  • 使用命令模式作为“回调 (CallBack) "在面向对象系统中的替代。"CallBack" 讲的便是先将一个函数登记上, 然后在以后调用此函数。
  • 需要在不同的时间指定请求、 将请求排队。一 个命令对象和原先的请求发出者可以有不同的生命期。 换言之, 原先的请求发出者可能已经不在了, 而命令对象本身仍然是活动的。这时命令的接收者可以是在本地, 也可以在网络的另外 一 个地址。命令对象可以在序列化之后传送到另外 一 台机器上去。
  • 系统需要支持命令的撤销。命令对象可以把状态存储起来, 等到客户端需要撤销命令所产生的效果时, 可以调用 undo() 方法, 把命令所产生的效果撤销掉。命令对象还可以提供 redo()方法, 以供客户端在需要时再重新实施命令效果。
  • 如果要将系统中所有的数据更新到日志里,以便在系统 崩溃时,可以根据日志读回所有的数据更新命令, 重新 调用 Execute()方法一 条 一 条执行 这些命令, 从而恢复系统在崩溃前所做的数据更新。

2.命令执行:

  从图中我们可以看到一 共存在4种命令的执行方式,而 Hystrix在执行 时 会根据创建的Command对象以及具体的情况来选择 一 个执行。其中HystrixComrnand实现了下面两个执行方式。

  • execute (): 同步执行,从依赖的服务 返回一 个单 一的结果对象, 或是在发生错误的时候抛出异常。
  • queue (): 异步执行,直接返回一 个Future对象, 其中包含了服务 执行 结束时要返回的单一结果对象。

  而HystrixObservableCommand实现了另外两种 执行方式。

  • observe () : 返回Observable对象,它代表了操作的多个结果,它是一 个Hot Observable。
  • toObservable(): 同样会返回Observable对象, 也代表了操作的多个结果,但它返回的是一 个Cold Observable。

   在Hystrix的底层实现中大量地使用了RxJava,在这里对RxJava的观察者-订阅者模式做 一 个简单的入门介绍。上面我们所提到的Observable对象就是RxJava中的核心内容之 一 ,可以把它理解为“事件源”或是“被观察者 ” , 与其对应的Subscriber对象,可以理解为“订阅者 ”或是“观察者 ” 。 这两个对象 是RxJava响应式编程的重要组成部分。

  • Observable用来向订阅者Subscriber对象 发布事件,Subscriber对象则在接收到事件后对其进行处理, 而在这里所指的事件 通常就是对依赖 服务的调用。
  • 一 个Observable可以发出多个事件, 直到结束或是 发生异常。
  • Observable 对象每发出一 个事件,就会调用对应 观察者 Subscriber 对象的onNext ()方法。
  • 每一 个Observable的执行,最后 一 定会通过调用 Subscriber. onCompleted () 或者Subscriber.onError()来结束该事件的操作流。

  下面我们通过 一 个简单的例子来直观理解 一 下 Observable与Subscribers:

public class RxJavaTest {
    public static void main(String[] args) {
        //例子1
        Observable<String> observable = Observable.create(new Observable.OnSubscribe<String>() {

            @Override
            public void call(Subscriber<? super String> subscriber) {
                subscriber.onNext("Hello RxJava");
                subscriber.onNext("I am程序猿DD");
                subscriber.onCompleted();
            }
        });

        //创建订阅者subscriber
        Subscriber<String> subscriber = new Subscriber<String>() {
            @Override
            public void onCompleted() {
                System.out.println("Subscriber : onCompleted");
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(String s) {
                System.out.println("Subscriber : " + s);
            }
        };
        //订阅
        observable.subscribe(subscriber);
        //例子2
        System.out.println("=========================================");
        //正常情况是先产生数据,再注册,再消费。
        Observable.just(dataProducer()).doOnSubscribe(() -> {
            System.out.println("Subscribe!");
        }).subscribe(s -> {
            System.out.println("Consume Data :" + s);
        });

        //使用了defer之后,是先注册,再生产数据,再消费。
        Observable.defer(() -> {
            return Observable.just(dataProducer());
        }).doOnSubscribe(() -> {
            System.out.println("Subscribe!");
        }).subscribe(s -> {
            System.out.println("Consume Data :" + s);
        });

        System.out.println("=========================================");
        //我们不关心onError和onComplete,所以只需要第一个参数就可以
        //例子3
        Observable<String> myObservable = Observable.just("Hello, world! just");

        Action1<String> onNextAction = new Action1<String>() {
            @Override
            public void call(String s) {
                System.out.println(s);
            }
        };

        myObservable.subscribe(onNextAction);
    }

    private static String dataProducer() {
        String str = "Hello,RxJava!";
        System.out.println("Produce Data :" + str);
        return str;
    }
}

  在该示例中, 创建了 一 个简单的事件源 observable,一 个对事件传递内容输出的订阅者 subscriber, 通过 observable.subscribe(subscriber) 来触发事件的发布。在这里我们对于事件源 observable 提到了两个不同的概念: Hot Observable 和 Cold Observable, 分别对应了上面 command. observe ()和command.toObservable() 的返回对象。 其中 Hot Observable, 它不论” 事件源 ”是否有“ 订阅者 ” , 都会在创建后对事件进行发布, 所以对于 Hot Observable 的每 一 个“ 订阅者 ”都有可能是从“事件源”的中途开始的, 并可能只是看到了整个操作的局部过程。 而 Cold Observable 在没有“ 订阅者 ”的时候并不会发布事件, 而是进行等待, 直到有“ 订阅者 ”之后才发布事件, 所以对于 ColdObservable 的订阅者, 它可以保证从 一 开始看到整个操作的全部过程。大家从表面上可能会认为只是在 HystixObservableCommand 中使用了 RxJava,然而实际上execute()、 queue()也都使用了Rx.Java来实现。 从下面的源码中我们可以看到:

  • execute ()是通过queue()返回的异步对象Future<R>的get()方法来实现同步执行的。 该方法会等待任务执行结束, 然后获得R类型的结果进行返回。
  • queue ()则是通过toObservable()来获得 一 个Cold Observable, 并且通过 toBlocking ()将该Observable转换成BlockingObservable, 它可以把数据以阻塞的方式 发射出来。 而toFuture 方法则是 把BlockingObservable转换为一 个Future, 该方法只是创建 一 个Future 返回并不会阻塞,这使得消费者可以自己决定如何处理异步操作。 而execute()就是直接使用了queue()返回的 Future中的阻塞方法 get()来实现同步操作的。 同时通过这种方式转换的Future要求Observable 只发射 一 个数据,所以 这两个实现都只能返回单 一 结果。
public R execute() {
        try {
            return queue().get();
        } catch (Exception e) {
            throw Exceptions.sneakyThrow(decomposeException(e));
        }
}
public Future<R> queue() {
        /*
         * The Future returned by Observable.toBlocking().toFuture() does not implement the
         * interruption of the execution thread when the "mayInterrupt" flag of Future.cancel(boolean) is set to true;
         * thus, to comply with the contract of Future, we must wrap around it.
         */
        final Future<R> delegate = toObservable().toBlocking().toFuture();
        
        final Future<R> f = new Future<R>() {

        if (f.isDone()) {
            ......
        }

        return f;
}

3.结果是否被缓存:

  若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以Observable 对象的形式 返回。

4.断路器是否打开:

  在命令结果没有缓存命中的时候, Hystrix在执行命令前需要检查断路器是否为打开状态:

  • 如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步)
  • 如果断路器是关闭的, 那么Hystrix跳到第5步,检查是否有可用资源来 执行命令。

5.线程池请求队列信号量是否占满:

  如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(第8步)。需要注意的是,这里Hystrix所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池。 Hystrix为了保证不会因为某个依赖服务的间题影响到其他依赖服务而采用了“ 舱壁模式" (Bulkhead Pattern)来 隔离每个依赖的服务。下面介绍一下依赖隔离

  “舱壁模式”对于熟悉 Docker 的读者 一 定不陌生, Docker 通过“舱壁模式”实现进程的隔离, 使得容器与容器之间不会互相影响。 而 Hystrix 则使用该模式实现线程池的隔离,它会为每 一 个依赖服务创建 一 个独立的线程池, 这样就算某个依赖服务出现延迟过高的情况, 也只是对该依赖服务的调用产生影响, 而不会拖慢其他的依赖服务。通过实现对依赖服务的线程池隔离, 可以带来如下优势:

  • 应用自身得到完全保护, 不会受不可控的依赖服务影响。 即便给依赖服务分配的线程池被填满, 也不会影响应用自身的其余部分。
  • 可以有效降低接入新服务的风险。 如果新服务接入后运行不稳定或存在问题, 完全不会影响应用其他的请求。当依赖的服务从失效恢复正常后, 它的线程池会被清理并且能够马上恢复健康的服务, 相比之下, 容器级别的清理恢复速度要慢得多。
  • 当依赖的服务出现配置错误的时候, 线程池会快速反映出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。 同时, 我们可以在不影响应用功能的情况下通过实时的动态属性刷新(后续会通过Spring Cloud Config与Spring Cloud Bus的联合使用来介绍) 来处理它。
  • 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候, 线程池的监控指标信息会反映出这样的变化。 同时, 我们也可以通过实时动态刷新自身应用对依赖服务的阙值进行调整以适应依赖方的改变。
  • 除了上面通过线程池隔离服务发挥的优点之外, 每个专有线程池都提供了内置的并发实现, 可以利用它为同步的依赖服务构建异步访问。

  总之, 通过对依赖服务实现线程池隔离, 可让我们的应用更加健壮, 不会因为个别依赖服务出现问题而引起非相关服务的异常。 同时, 也使得我们的应用变得更加灵活, 可以在不停止服务的情况下, 配合动态配置刷新实现性能配置上的调整。

6.HystrixObservableCommand.construct()或HystrixCommand.run():

  Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。

  • HystrixCommand.run(): 返回 一 个单 一 的结果,或者抛出异常。
  • Hys 巨 ixObservableCommand.construct(): 返回 一 个Observable对象来发射多个结果,或通过onError发送错误通知。

  如果run()或construet()方法的执行时间超过了命令设置的超时阙值,当前处理线程将会抛出 一 个TimeoutException (如果该命令不在其自身的线程中执行,则会通过单独的计时线程来 抛出)。在这种情况下,Hystrix会转接到fallback处理逻辑(第8步)。同时,如果当前命令没有被取消或中断, 那么它最终会忽略run()或者construct ()方法的返回。如果命令没有抛出异常并返回了结果,那么Hystrix在记录 一 些日志并采集监控报告之后将该结果返回。在使用run()的情况下,Hystrix会返回 一 个Observable, 它发射单个结果并产生onCompleted的结束通知; 而在使用construct ()的情况下,Hystrix会直接返回该方法产生的Observable对象。

7. 计算断路器的健康度:

  Hystrix会将“成功” 、 “失败” 、 “ 拒绝 ” 、 “超时”等信息报告给断路器,而断路器会维护 一 组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行“熔断/短路” ,直到恢复期结束。 若在恢复期结束后,根据统计数据判断如果还是未达到健康指标,就再次“熔断/短路” 。

8. fallback处理:

  当命令执行失败的时候, Hystrix会进入fallback尝试回退处理, 我们通常也称该操作为“服务降级 ” 。而能够引起服务降级处理的清况有下面几种:

  • 第4步, 当前命令处于“熔断I短路”状态, 断路器是打开的时候。
  • 第5步, 当前命令的线程池、 请求队列或者信号量被占满的时候。
  • 第6步,HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。

  在服务降级逻辑中, 我们需要实现一 个通用的响应结果, 并且该结果的处理逻辑应当是从缓存或是根据 一 些静态逻辑来获取,而不是依赖网络请求获取。如果一 定要在降级逻辑中包含网络请求,那么该请求也必须被包装在HystrixCommand或是HystrixObservableCommand中, 从而形成级联的降级策略, 而最终的降级逻辑 一 定不是 一 个依赖网络请求的处理, 而是 一 个能够稳定地返回结果的处理逻辑。在HystrixCommand和HystrixObservableCommand中实现降级逻辑时还略有不同:

  • 当使用HystrixCommand的时候, 通过实现HystrixCommand.getFallback()来实现服务降级逻辑。
  • 当使用 HystrixObservableCommand 的时候, 通过 HystrixObservable­Command.resumeWithFallback()实现服务降级逻辑, 该方法会返回 一 个Observable对象来发射 一 个或多个降级结果。

  当命令的降级逻辑返回 结果之后, Hystrix 就将该结果返回给调用者。 当使用HystrixCommand.getFallback()的时候, 它会返回一 个Observable对象, 该对象会 发 射 getFallback() 的 处 理 结 果 。 而使用 HystrixObservableCommand.resumeWithFallback ()实现的时候, 它会将Observable对象直接返回。如果我们没有为命令实现降级逻辑或者降级处理逻辑中抛出了异常, Hystrix依然会返回 一 个Observable对象, 但是它不会发射任何结果数据, 而是通过onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者。实现 一个有可能失败的降级逻辑是 一 种非常糟糕的做法, 我们应该在实现降级策略时尽可能避免失败的情况。

  当然完全不可能出现失败的完美策略是不存在的, 如果降级执行发现失败的时候,Hystrix会根据不同的执行方法做出不同的处理。

  • execute(): 抛出异常。
  • queue(): 正常返回Future对象,但是当 调用get()来获取结果的时候会抛出异常。
  • observe () : 正常返回Observable对象, 当订阅它的时候, 将立即通过调用订阅者的onError方法来通知中止请求。
  • toObservable(): 正常返回Observable对象, 当订阅它的时候, 将通过调用订阅者的onError方法来通知中止请求。

9. 返回成功的响应:

  当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回。 而具体以哪种方式返回取决于之前第2步中我们所提到的对命令的4种不同执行方式, 下图中总结了这4种 调用方式之间的依赖关系。 我们可以将此图与在第2步中对前两者源码的分析联系起来 , 并且从源头toObservable()来开始分析。

  • toObservable(): 返回最原始的 Observable, 必须通过订阅它才会真正触发命令的执行流程。
  • observe () : 在toObservable()产生原始Observable 之后立即 订阅它, 让命令能够马上开始异步执行 , 并返回 一 个Observable 对象, 当调用它的subscribe 时, 将重新产生结果和通知给订阅者。
  • queue (): 将 toObservable()产生的原始Observable通过toBlocking()方法转换成BlockingObservable对象, 并调用它的toFuture()方法 返回异步的Future对象。
  • execute () : 在queue()产生异步结果Future对象之后,通过调用get()方法阻塞并等待结果的返回。

断路器原理:

  断路器在 HystrixCommand 和 HystrixObservableCommand 执行过程中起到了举足轻重的作用,它是 Hystrix 的核心部件。 那么断路器是如何决策熔断和记录信息的呢?我们先来看看断路器 HystrixCircuitBreaker 的定义:

public interface HystrixCircuitBreaker {

    /**每个 Hystrix 命令的请求都通过它判断是否被执行。
     * Every {@link HystrixCommand} requests asks this if it is allowed to proceed or not.
     * This takes into account the half-open logic which allows some requests through when determining if it should be closed again.
     * @return boolean whether a request should be permitted
     */
    public boolean allowRequest();

    /**返回当前断路器是否打开。
     * Whether the circuit is currently open (tripped).
     * @return boolean state of circuit breaker
     */
    public boolean isOpen();

    /**用来闭合断路器。
     * Invoked on successful executions from {@link HystrixCommand} as part of feedback mechanism when in a half-open state.
     */
    void markSuccess();

    /**静态类 Factory 中维护了 一 个 Hystrix 命令与 HystrixCircuitBreaker 的关系
     * @ExcludeFromJavadoc
     * @ThreadSafe
     */
    public static class Factory {...}

    /**断路器接口 HystrixCircuitBreaker的实现类
     * The default production implementation of {@link HystrixCircuitBreaker}.
     * @ExcludeFromJavadoc
     * @ThreadSafe
     */
    static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {...}

    /**定义了一 个什么都不做的断路器实现,它允许所有请求,并且断路器状态始终闭合。
     * An implementation of the circuit breaker that does nothing.
     * @ExcludeFromJavadoc
     */
    static class NoOpCircuitBreaker implements HystrixCircuitBreaker {...}
}

  HystrixCircuitBreakerImpl 对 HystrixCircuitBreaker 接口的各个方法实现如下所示。

  isOpen (): 判断断路器的打开/关闭状态。 详细逻辑如下所示。

  1. 如果断路器打开标识为true, 则直接返回true, 表示断路器处千打开状态。否则,就从度量指标对象 metrics 中获取 HealthCounts 统计对象做进 一 步判断(该对象记录了一 个滚动时间窗内的请求信息快照,默认时间窗为10秒)。
  2. 如果它的请求总数(QPS)在预设的阙值范围内就返回 false , 表示断路器处于未打开状态。该阙值的配置参数为 circuitBreakerRequestVolumeThreshold,默认值为20。
  3. 如果错误百分比在阑值范围内就返回 false, 表示断路器处于未打开状态。该阙值的配置参数为 circuitBreakerErrorThresholdPercentage, 默认值为50 。
  4. 如果上面的两个条件都不满足,则将断路器设置为打开状态 (熔断/短路)。 同时,如果是从关闭状态切换到打开状态的话,就将当前时间记录到上面提到的circuitOpenedOrLastTestedTirne 对象中。
@Override
        public boolean isOpen() {
            if (circuitOpen.get()) {
                // if we're open we immediately return true and don't bother attempting to 'close' ourself as that is left to allowSingleTest and a subsequent successful test to close
                return true;
            }

            // we're closed, so let's see if errors have made us so we should trip the circuit open
            HealthCounts health = metrics.getHealthCounts();

            // check if we are past the statisticalWindowVolumeThreshold
            if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
                // we are not past the minimum volume threshold for the statisticalWindow so we'll return false immediately and not calculate anything
                return false;
            }

            if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
                return false;
            } else {
                // our failure rate is too high, trip the circuit
                if (circuitOpen.compareAndSet(false, true)) {
                    // if the previousValue was false then we want to set the currentTime
                    circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
                    return true;
                } else {
                    // How could previousValue be true? If another thread was going through this code at the same time a race-condition could have
                    // caused another thread to set it to true already even though we were in the process of doing the same
                    // In this case, we know the circuit is open, so let the other thread set the currentTime and report back that the circuit is open
                    return true;
                }
            }
        }

  allowRequest(): 判断请求是否被允许,这个实现非常简单。 先根据配置对象properties中的断路器判断强制打开或关闭属性是否被设置。 如果强制打开,就直接返回false, 拒绝请求。 如果强制关闭,它会允许所有请求,但是同时也会调用isOpen ()来执行断路器的计算逻辑, 用来模拟断路器打开/关闭的行为。 在默认情况下,断路器并不会进入这两个强制打开或关闭的分支中去,而是通过 !isOpen () II allowSingleTest ()来判断是否允许请求访问。 !isOpen()之前已经介绍过, 用来判断和计算当前断路器是否打开,如果是断开状态就允许请求。 那么allowSingleTest()是用来做什么的呢?

@Override
public boolean allowRequest() {
  if (properties.circuitBreakerForceOpen().get()) {
    // properties have asked us to force the circuit open so we will allow NO requests
    return false;
  }
  if (properties.circuitBreakerForceClosed().get()) {
    // we still want to allow isOpen() to perform it's calculations so we simulate normal behavior
    isOpen();
    // properties have asked us to ignore errors so we will ignore the results of isOpen and just allow all traffic through
    return true;
  }
  return !isOpen() || allowSingleTest();
}

  从allowSingleTest()的实现中我们可以看到,这里使用了在isOpen()函数中当断路器从闭合到打开时候所记录的时间戳。 当断路器在打开状态的时候,这里会判断断开时的时间戳+配置中的circuitBreakerSleepWndowinMilliseconds时间是否小于当前时间,是的话,就将当前时间更新到记录断路器打开的时间对象 circuitOpenedOrLastTestedTime 中,并且允许此次请求。 简单地说, 通过 circuitBreakerSleepWindowinMilliseconds 属性设置了一 个断路器打开之后的休眠时间(默认为5秒),在该休眠时间到达之后,将再次允许请求尝试访问,此时断路器处于“半开”状态,若此时请求继续失败, 断路器又进入打开状态, 并继续等待下 一 个休眠窗口过去之后再次尝试;若请求成功, 则将断路器重新置于关闭状态。所以通过 allowSingleTest()与isOpen ()方法的配合,实现了断路器打开和关闭状态的切换。

public boolean allowSingleTest() {
  long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();
  // 1) if the circuit is open
  // 2) and it's been longer than 'sleepWindow' since we opened the circuit
  if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + properties.circuitBreakerSleepWindowInMilliseconds().get()) {
    // We push the 'circuitOpenedTime' ahead by 'sleepWindow' since we have allowed one request to try.
    // If it succeeds the circuit will be closed, otherwise another singleTest will be allowed at the end of the 'sleepWindow'.
    if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {
      // if this returns true that means we set the time so we'll return true to allow the singleTest
      // if it returned false it means another thread raced us and allowed the singleTest before we did
      return true;
    }
  }
  return false;
}

  markSuccess(): 该函数用来在“半开路”状态时使用。若 Hystrix 命令调用成功,通过调用它将打开的断路器关闭, 并重置度量指标对象。

public void markSuccess() {
    if (circuitOpen.get()) {
     if (circuitOpen.compareAndSet(true, false)) {
      //win the thread race to reset metrics
      //Unsubscribe from the current stream to reset the health counts stream.  This only affects the health counts view,
      //and all other metric consumers are unaffected by the reset
      metrics.resetStream();
    }
  }
}

  下图是 Netflix Hystrix 官方文档中关千断路器的详细执行逻辑,可以帮助我们理解上面的分析内容。

posted @ 2019-04-23 11:38  吴振照  阅读(3408)  评论(0编辑  收藏  举报