《Spring Cloud微服务架构实战》--集群保护机制--Hystrix

《Spring Cloud微服务架构实战》--集群保护机制--Hystrix

6.1概述

在很多系统架构中都需要考虑横向扩展、单点故障等问题,对于一个庞大的应用集群, 部分服务或者机器出现问题不可避免。在出现故障时,如何减少故障的影响、保障集群的 高可用,成为一个重要的课题。

在微服务集群中,不管是服务器,还是客户端,都支持集 群部署,本章将讲述Spring Cloud中所使用的集群保护框架:Hystrix。

6.1.1实际问题

假设有一个应用程序,调用关系如图6-1所示

 

图6-1中用户访问服务A模块,服务通过Web接口或者其他方式访问基础服务模块, 基础服务模块访问数据库。

如果数据库因为某些原因变得不可用,基础服务将会得到“数据库无法访问”的信息, 并且会将此信息告知服务A模块。

在出现问题时,用户不断地请求服务A模块,而服务A 模块则继续请求基础服务模块,基础服务模块仍然不停地连接有问题的数据库直到超时, 大量的用户请求(包括重试的请求)会发送过来,整个应用不堪重负。

实际情况可能更加糟糕,用户的请求不停地发送给服务A模块,而由于数据库的原因, 基础服务模块退迟无法响应,有可能造成整个机房的网络阻塞,受害的不仅仅是该应用程序,机房中的所有服务都有可能因为网络原因而瘫痪。

 

6.1.2传统的解决方式

  对于前面遇到的实际问题,可以选择在连接数据库时加上超时的配置,让基础服务模 块快速响应。但这仅仅算是解决了其中一种情况,在实际情况中,基础服务模块有可能出 现问题,

例如部分线程阻塞、进程假死等,在这些情况下,对外的服务A模块面对大量的 用户与有故障的基础服务模块,仍然无法独善其身,前面所说的问题依然会出现。

  笔者曾就职于某电影院售票系统供应商,在某一年的春节档期,几大互联网巨头发起 了观影优惠活动,大量的用户请求涌入我们中心端的系统。由于其中某些服务节点(Tomcat) 处理缓慢,很多重试、新接入的请求不断访问我们的服务。

在这个时候,传说中的“人肉 运维”出现了,值班的运维同事,通过手工重启Tomcat来试图缓解这种情况,然而挣扎了 几个小时后,以失败告终。

最终,整个集群网络阻塞,不得不停止对外服务,公司损失惨重。

这件事过后,公司方面加强了对服务节点的监控,加入了故障报告、紧急故障处理 等机制,期望能减少或者避免这些问题所带来的影响。

当今的互联网时代,面对大量的用户请求,传统或者单一的解决方式在复杂的集群 面前显得有点力不从心,我们需要更优雅而且更完善的方案来解决这些问题。

 

6.1.3集群容错框架Hystrix

  在分布式环境中,总会有一些被依赖的服务会失效,例如像网络短暂无法访问、服务 器宕机等情况。

  Hystrix是Netflix下的一个Java库,Spring Cloud将Hystrix整合到Netflix 项目中,Hystrix通过添加延迟阈值以及容错的逻辑,来帮助我们控制分布式系统间组件的 交互。

Hystrix通过隔离服务间的访问点、停止它们之间的级联故障、提供可回退操作来实 现容错。

例如我们前面所讲到的问题,如果数据库层面出现问题,服务A模块在访问基础模块 时必定会出现超时的情况,此时可以将基础模块隔离开来,服务A在短时间内不再调用基 础模块,并且快速响应用户的请求,

  从而保证服务A自身乃至整个集群的稳定性,这是 Hystrix可以解决的问题。加入容错机制,当出现前面所说的问题时,原来的应用程序将变 为图6-2所示的结构。

如图6-2所示,当前基础服务模块或者数据库不可用时,服务A将对其进行“熔”, 在一定的时间内,服务A都不会再调用基础服务,以维持本身的稳定。

 

  

图6-2加入容错机制

 

  除了服务间的依赖会导致整个集群不可用外,在其他情况下,我们同样需要集群容错。 假设集群中存在30个服务,每个服务在99.99%的时间内是正常运行的,计算下来,整个集群在99.7%的时间内是正常运行的。

如果该集群接受10亿次请求,那么将会有300万次 请求会失败。在现实中,情况可能更加严重,每个服务有99.99%的正常服务时间,已经是 一个很乐观的数字。

  网络连接失败、超时,服务器硬件故障,部署引起的问题,应用程序 的bug,所有这些情况都可能会出现,不能因为单点故障而降低整个集群的可用性,容错 机制变得尤为重要。

6.1.4 Hystrix 的功能

Hystrix主要实现以下功能:

  • 当所依赖的网络服务发生延迟或者失败时,对访问的客户端程序进行保护,就像前 面例子中对服务A模块进行保护一样。
  • 在分布式系统中,停止级联故障。
  • 网络服务恢复正常后,可以快速恢复客户端的访问能力。
  • 调用失败时执行服务回退。
  • 可支持实时监控、报警和其他操作。

接下来,我们将讲述Hystrix的相关功能。

 

6.2 第一个Hystrix程序

  先编写一个简单的Hello World程序,展示Hystrix的基本功能。注意:6.2节与6.3节, Hystrix均没有与Spring Cloud整合使用

 6.2.1 准备工作

使用Spring Boot的spring-boot-starter-web项目,建立一个普通的Web项目,发布两个 测试服务用于测试,控制器的代码请见代码:

@RestController
public class MyController {
  @GetMapping("/normalHello")
  public String normalHello(HttpServletRequest request) {
    return "Hello World";
  }
  @GetMapping("/errorHello")   public String errorHello(HttpServletRequest request) throws Exception {     //模拟需要处理10秒     Thread.sleep (10000);     return "Error Hello World";   } }

  一个正常的服务,另外一个服务则需要等待10秒才有返回。本例的Web项目对应的 代码目录为 codes\06\6.2\first-hystrix-server,启动类是 Server Application o

6.2.2客户端使用Hystrix

结合Hystrix来请求Web服务,可能与原来的方式不太一样。新建项目first-hystrix-client, 在pom.xml中加入以下依赖:

<dependency>
  <groupld>com.netflix.hystrix</groupld>
  <artifactld>hystrix-core</artifactld>   <version>l.5.12</version> </dependency> <dependency>   <groupld>org.slf4j</groupld>   <version>l.7.25</version>   <artifactld>slf4j-log4j12</artifactld> </dependency> <dependency>   <groupld>commons-logging</groupld>
  <artifactld>commons-logging</artifactld>
  <version>l.2</version> </dependency> <dependency>   <groupld>org.apache.httpcomponents</groupld>   <artifactld>httpclient</artifactld>   <version>4.5.2</version> </dependency>

  本书Spring Cloud所使用的Hystrix的版本为1.5.12,我们也使用与其一致的版本。客 户端项目除了要使用Hystrix夕卜,还会使用HttpClient模块访问Web服务,因此要加入相应 的依赖。

新建一个命令类,实现请见代码:

public class HelloCommand extends HystrixCommand<String> {
  private String url;   CloseableHttpClient httpclient;
  public HelloCommand(String url) {     //调用父类的构造器,设置命令组的key,默认用来作为线程池的key     super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));     // 创建 HttpClient客户端     this.httpclient = HttpClients.createDefault();
    this.url = url;   }
  protected String run() throws Exception {     try {       //调用GET方法请求服务       HttpGet httpget = new HttpGet(url);       //得到服务响应       HttpResponse response = httpclient.execute(httpget); //解析并返回命令执行结果       return EntityUtils.toString(response.getEntity());     } catch (Exception e) (       e.printStackTrace();     }     return "";
  }
}

  新建运行类,执行HelloCommand,如代码清单6-3所示:

public class HelloMain {
  public static void main(String[] args) {
    //请求正常的服务
    String normalUrl = "http://localhost:8080/normalHello";
    HelloCommand command = new HelioCommand(normalUrl);
    String result = command.execute(); 
    System.out.printin("请求正常的服务,结果:" + result);   } }

  

正常情况下,直接调用HttpClient的API来请求Web服务,而前面的命令类与运行类 则通过命令来执行调用的工作。在命令类HelloCommand中,实现了父类的run方法,使 用HttpClient调用服务的过程,都放到了该方法中。

运行HelloMain类,可以看到,结果与 平常调用Web服务无异。接下来,测试使用Hystrix的情况下调用有问题的服务。 

6.2.3调用错误服务

假设我们所调用的Hello服务发生故障,导致无法正常访问,那么对于客户端来说, 如何自保呢?本例将调用延时的服务,为客户端设置回退方法。

修改HelloCommand, 加入回退方法,请见代码清单6-4。

protected String getFallback () {
  System.out.printIn("执行 HelloCommand 的回退方法");
  return "error";
}

  

在运行类中,调用发生故障的服务,请见代码:

public class HelloErrorMain {
  public static void main(String[] args) {
    //请求异常的服务
    String normalUrl = "http://localhost:8080/errorHello";
    HelloCommand command = new HelloCommand(normalUrl);
    String result = command.execute();
    System.out.printin ("请求异常的服务,结果:"+ result);
  }
}

  

运行HelloErrorMain类,输出如下:

执行HelloCommand的回退方法
请求异常的服务,结果:error

  

  根据结果可知,回退方法被执行。本例中调用的errorHello服务,会阻塞10秒才有返 回。默认情况下,如果调用的Web服务无法在1秒内完成,那么将会触发回退。

回退更像是一个备胎,当请求的服务无法正常返回时,就调用该“备”的实现。 样做可以很好地保护客户端,服务端所提供的服务受网络等条件的制约,如果有服务真的 需要10秒才能返回结果,

  而客户端又没有容错机制,后果就是,客户端将一直等待返回, 直到网络超时或者服务有响应,而外界会一直不停地发送请求给客户端,最终导致的结果 就是,客户端因请求过多而瘫痪。

 

6.2.4 Hystrix的运作流程

  在前面的例子中,使用Hystrix时仅仅创建命令并予以执行。看似简单,实际上,Hystrix 有一套较为复杂的执行逻辑,为了能让大家大致了解该执行过程,笔者将整个流程进行了 简化。

Hystrix的运作流程请见图6-3。

简单说明一下运作流程。

  •  第一步:在命令开始执行时,会做一些准备工作,例如为命令创建相应的线程池(后面章节讲述)等。
  • 第二步:判断是否打开了缓存,打开了缓存就直接查找缓存并返回结果。
  • 第三步:判断断路器是否打开,如果打开了,就表示链路不可用,直接执行回退方 法。结合本章开头的例子,可理解为基础服务模块不可用,服务A模块直接执行回 退,响应用户请求。
  • 第四步:判断线程池、信号量(计数器)等条件,例如像线程池超负荷,则执行回 退方法,否则,就去执行命令的内容(例如前面例子中的调用服务)。
  • 第五步:执行命令,计算是否要对断路器进行处理,执行完成后,如果满足一定条 件,则需要开启断路器。如果执行成功,则返回结果,反之则执行回退。

整个流程最主要的地方在于断路器是否被打开,后面会讲解断路器的相关内容。我们 的客户端在使用Hystrix时,表面上只是创建了一个命令来执行,实际上Hystrix已经为客 户端添加了几层保护。

图6-3所示的流程图对Hystrix的运作流程做了最简单的描述,对于部分细节,在此不 进行讲述,读者大致了解运作流程即可 

6.3 Hystrix的使用

本节将详细讲述Hystrix的使用方法

6.3.1 命令执行

在前面的例子中,使用了 execute方法执行命令,一个命令对象可以使用以下方法来执 行命令。

  • toObservable:返回一个最原始的可观察的实例(Observable) , Observable RxJava 的类,使用该对象可以观察命令的执行过程,并且将执行信息传递给订阅者。
  • observe:调用toObservable方法,获得一个原始的Observable实例后,使用 ReplaySubject作为原始Observable的订阅者。
  • queue:通过toObservable方法获取原始的Observable实例,再调用Observable toBlocking 方法得到一个 BlockingObservable 实例,最后调用 BlockingObservable toFuture方法返回Future实例,调用Future的get方法得到执行结果。
  • execute:调用queue的get方法返回命令的执行结果,该方法同步执行。

以上4个方法,除execute方法外,其他方法均为异步执行。observe与toObservable 方法的区别在于,toObservable被调用后,命令不会立即执行,只有当返回的Observable 实例被订阅后,才会真正执行命令。

而在observe方法的实现中,会调用toObservable得到 Observable实例,再对其进行订阅,因此调用observe方法后会立即执行命令(异)。代 码清单6-6使用了 4个执行命令的方法。

public class RunTest {
  public static void main(String[] args) throws Exception {
    //使用execute方法
    RunCommand cl = new RunCommand ("使用 execute 方法执行命令");
    cl.execute();
    //使用queue方法
    RunCommand c2 = new RunCommand ("使用 queue 方法执行命令");
    c2.queue();
    //使用observe方法
    RunCommand c3 = new RunCorranand ("使用 observe 方法执行命令");
    c3.observe();
    // 使用 toObservable 方法
    RunCommand c4 = new RunCommand ("使用 toObservable 方法执行命令"); 
    //调用toObservable方法后,命令不会马上执行     Observable<String> ob = c4.toObservable ();     //进行订阅,此时会执行命令     ob.subscribe(new Observer<String>() {       public void onCompleted() {         System.out.printIn ("命令执行完成");       }
      public void onError(Throwable e) {       }       public void onNext(String t) {         System, out .printin (n 命令执行结果:"+ t);       }     });     Thread.sleep(100);   }
  //测试命令   static class RunCommand extends HystrixCommand<String> {     String msg;     public RunCommand(String msg) (       super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
      this.msg = msg;     }     protected String run() throws Exception {       System.out.printIn(msg);       return "success";     }
  }
}

   

对于4个执行命令的方法,读者需要知道toObservable与observe方法的区别,这两个 方法将会在Spring Cloud中使用。

 

6.3.2属性配置

  使用Hystrix时,可以为命令设置属性,以下的代码片断为一个命令设置了执行的超时 时间:

public MyCommand(boolean isTimeout) {
  super(
    Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
      .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
      .withExecutionTimeoutlnMilliseconds(500))   ); }

  以上的配置仅对该命令生效,设置了命令的超时时间为500毫秒。

该配置项的默认值 为1秒,如果想对全局生效,可以使用以下代码片断:

ConfigurationManager
.getConfiglnstance()
.setProperty("hystrix.command.default.execution.isolation.thread.timeoutlnMilliseconds", 500);

  

以上代码片断同样设置了命令超时时间为500毫秒,但对全局有效。除了超时的配置 外,还需要了解一下命令的相关名称,可以为命令设置以下名称。

  • 命令组名称(GroupKey):必须提供命令组名称,默认情况下,全局维护的线程池 Map以该值作为key,该Map的value为执行命令的线程池。
  • 命令名称(CommandKey):可选参数。
  • 线程池名称(ThreadPoolKey):指定了线程的key后,全局维护的线程池Map 以该值作为key。

以下的代码片断分别设置以上的3个Key:

public RunCommand(String msg) {
  super(
    Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("group-key"))
        .andCommandKey (HystrixCommandKey.Factory.asKey("command-key"))
        .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("pool-key”))
  );
}

  

Hystrix的配置众多,后面章节的案例中会涉及部分配置。读者如果想了解更多配置, 可到以下地址査看 https://github.com/Netflix/Hystrix/wiki/Configurationo 

6.3.3 回退

根据6.2节的流程图可知,至少会有3种情况触发回退(fallback):

  • 断路器被打开。
  • 线程池、队列、信号量满载。
  • 实际执行命令失败。

在命令中实现父类(HystrixCommand)的getFal!back()方法,即可实现回退。当以上 情况发生时,将会执行回退方法。在前面的例子中已经展示了 “执行命令失败”的回退,

下面测试一下断路器被打开时的回退,详情请见代码:

public class FallbackTest {
  public static void main(String[] args) {
    //断路器被强制打开
    ConfigurationManager.getConfiglnstance() .setProperty(
        "hystrix.command.default.circuitBreaker.forceOpen", "true");
    FallbackCommand c = new FallbackCommand();
    c.execute ();
    //创建第二个命令,断路器关闭
    ConfigurationManager.getConfiglnstance().setProperty(
        "hystrix.command.default.circuitBreaker.forceOpen", "false");
    FallbackCommand c2 = new FallbackCommand();
    c2.execute();
  }
  static class FallbackCommand extends HystrixCommand<String> {     public FallbackCommand() (       super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));     }     /**      * 断路器被强制打开,该方法不会执行      */     protected String run () throws Exception {       System.out.printin("命令执行");       return "";     }     /**      * 回退方法,断路器打开后会执行回退      */     protected String getFallback() {       System.out.printIn(”执行回退方法”);       return "fallback";     }   } }

如果让断路器打开,需要符合一定的条件。本例为了简单起见,在代码清单中使用了 配置管理类(ConfigurationManager)将断路器强制打开与关闭。在打开断路器后, FallbackCommand总会执行回退(getFallback)方法将断路器关闭,命令执行正常。

如果断 路器被打开,而命令中没有提供回退方法,将抛出以下异常:

com.netflix.hystrix.exception.HystrixRuntimeException: Fallbackcommand short-circuited and no fallback available.  

  另外,需要注意的是,命令执行后,不管是否会触发回退,都会去计算整个链路的健 康状况,根据健康状况来判断是否要打开断路器。

如果命令仅仅失败了一次,是不足以打 开断路器的,关于断路器的逻辑将在后面章节讲述。

6.3.4回退的模式

  Hystrix的回退机制比较灵活,你可以在A命令的回退方法中执行B命令,如果B 令也执行失败,同样也会触发B命令的回退,这样就形成一种链式的命令执行,例如以下 代码片断:

static class CommandA extends HystrixCommand<String> { ……省略其他代码
  protected String run() throws Exception {
    throw new RuntimeException();
  )
  protected String getFallback() {
    return new CommandB().execute();
  }
}

还有其他较为复杂的例子,例如银行转账。假设一个转账命令包含调用A银行扣款、 B银行加款两个命令,其中一个命令失败后,执行转账命令的回退,如图6-4所示。

  

要做到图6-4所示的多命令只执行一次回退的效果,CommandA与CommandB不能有 回退方法。如果CommandA命令执行失败,并且该命令有回退方法,此时将不会执行 MainCommand的回退方法。

除了上面所提到的链式的回退以及多命令回退,读者还可以根 据实际情况来设计回退。

 

6.3.5断路器开启

断路器一旦开启,就会直接调用回退方法,不再执行命令,而且也不会更新链路的健 康状况。断路器的开启要满足两个条件:

  • 整个链路达到一定阈值,默认情况下,10秒内产生超过20次请求,则符合第一个 条件。
  • 满足第一个条件的情况下,如果请求的错误百分比大于阈值,则会打开断路器,默认为50%

Hystrix的逻辑是先判断是否满足第一个条件,再判断是否满足第二个条件。如果两个 条件都满足,则开启断路器。断路器开启的测试代码请见代码清单6-8。

public class OpenTest {
  public static void main(String[] args) throws Exception {
    // 10秒内有10个请求,则符合第一个条件
    ConfigurationManager.getConfiglnstance().setProperty(
        "hystrix. command. default. metrics. rollings tats. timelnMilliseconds ”,10000); 
    ConfigurationManager.getConfiglnstance().setProperty(         "hystrix. command. default. circuitBreaker. requestVolumeThreshold" , 10);
    ConfigurationManager.getConfiglnstance().setProperty(         "hystrix.command.default.circuitBreaker.errorThresholdPercentage”,50);
    for(int i = 0; i < 15; i++) {       //执行的命令全部都会超时       MyCommand c = new MyCommand();       c.execute();       //断路器打开后输岀信息       if(c.isCircuitBreakerOpen()) {         System, out. printin (n断路器被打开,执行第"+ (i + 1) + ”个命令”);       )     }   }
  /**    * 模拟超时的命令    * @author杨恩雄    */   static class MyCommand extends HystrixCommand<String> {     //设置超时的时间为500毫秒     public MyCommand() {       super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))             .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
            .withExecutionTimeoutInMilliseconds(500))       );     }
    protected String run() throws Exception {       //模拟处理超时       Thread.sleep(800);
      return "";     }
    @Override     protected String getFallback() {       return "";     }   } }

  注意代码清单6-8中的三个配置,第一个配置了数据统计的时间,第二个配置了请求 的阈值,第三个配置了错误百分比。

如果在10秒内,有大于10个请求发生,并且请求的 错误率超过50%,则开启断路器。

  在命令类MyCommand中,设置了命令执行的超时时间为500毫秒,命令执行需要800毫秒,换言之,该命令总会超时,命令模拟了现实环境中所依赖的服务瘫痪(超时响应) 的情况。

  在运行类中,循环15次执行命令,调用isCircuitBreakerOpen方法,如果断路器打开, 则输出信息。运行代码清单6-8所示的OpenTest类,输出如下:

断路器被打开,执行第11个命令
断路器被打开,执行第12个命令
断路器被打开,执行第13个命令
断路器被打开,执行第14个命令
断路器被打开,执行第15个命令

根据结果可知,前面执行的10个命令没有开启断路器,而到了第11个命令,断路器 被打开,命令不再执行。

 

6.3.6断路器关闭

  断路器打开后,在一段时间内,命令不会再执行(一直触发回退),这段时间我们称作 “休眠”。休眠期的默认值为5秒,休眠期结束后,Hystrix会尝试性地执行一次命令,

时断路器的状态不是开启,也不是关闭,而是一个半开的状态,如果这一次命令执行成功, 则会关闭断路器并清空链路的健康信息;

如果执行失败,断路器会继续保持打开的状态。 断路器的打开与关闭测试,请见代码:

public class CloseTest {
  public static void main(String[] args) throws Exception {
    // 10秒内有3个请求就满足第一个开启断路器的条件
    ConfigurationManager.getConfiglnstance().setProperty(
          "hystrix.command.default.metrics.rollingStats.timelnMilliseconds", 10000); 
    ConfigurationManager.getConfiglnstance().setProperty(           "hystrix.command.default.circuitBreaker.requestVolumeThreshold", 3);
    //请求的失败率,默认值为50%     ConfigurationManager.getConfiglnstance().setProperty(           "hystrix.command.default.circuitBreaker.errorThresholdPercentage", 50);
    //设置休眠期,断路器打开后,这段时间不会再执行命令,默认值为5秒,此处设置为3秒
    ConfigurationManager.getConfiglnstance().setProperty(           "hystrix.command.default.circuitBreaker.sleepWindowInMi1liseconds" , 3000);


    //该值决定是否执行超时     boolean isTimeout = true;     for(int i = 0; i < 10; i++) (       //执行的命令全部都会超时       MyCommand c = new MyCommand(isTimeout);       c.execute();       //输出健康状态等信息
      Healthcounts he = c.getMetrics().getHealthCounts();
      System.out.printIn("断路器状态:"+ c. isCircuitBreakerOpen () +",请求总数:n + hc.getTotalRequests ());       if(c.isCircuitBreakerOpen()) {         //断路器打开,让下一次循环成功执行命令         isTimeout = false;         System.out.printIn (" =====断路器打开了,等待休眠期结束====");         //休眠期会在3秒后结束,此处等待4秒,确保休眠期结束         Thread.sleep(4000);       }     }
}
  /**
   * 模拟超时的命令    * @author杨恩雄    */   static class MyCommand extends HystrixCominand<String> {     private boolean isTimeout;     //设置超时的时间为500毫秒     public MyCommand(boolean isTimeout) {       super(         Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))             .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
            .withExecutionTimeoutInMilliseconds(500))       );       this.isTimeout = isTimeout;     }
    protected String run () throws Exception {       //让外部决定是否超时       if(isTimeout) (         //模拟处理超时         Thread.sleep(800);       } else {         Thread.sleep(200);       }       return "";     }
    @Override     protected String getFallback() {
      return "";     }
  }
}

   代码清单中配置了休眠期为3秒,循环10次,创建10个命令并执行。在执行完第4 个命令后,断路器会被打开,此时我们等待休眠期结束,让下一次循环的命令执行成功。

代码清单中使用了一个布尔值来决定是否执行成功,第5次命令会执行成功,此时断 路器将会被关闭,剩下的命令全部都可以正常执行。在循环体中,使用了 HealthCounts 象,该对象用于记录链路的健康信息。

如果断路器关闭(链路恢复健康),HealthCounts 面的健康信息将会被重置。运行代码清单6-9,效果如下:

断路器状态:false,请求总数:0
断路器状态:false,请求总数:1
断路器状态:false,请求总数:2
断路器状态:true,请求总数:3
=====断路器打开了,等待休眠期结束 =====
断路器状态:false,请求总数:0 断路器状态:false,请求总数:1 断路器状态:false,请求总数:1 断路器状态:false,请求总数:3
断路器状态:false,请求总数:3
断路器状态:false,请求总数:5

  

6.3.7隔离机制

  命令的真正执行,除了断路器要关闭外,还需要再过一关:执行命令的线程池或者信 号量是否满载。如果满载,命令就不会执行,而是直接触发回退,这样的机制,在控制命 令的执行上,实现了错误的隔离。

Hystrix提供了两种隔离策略:

  • THREAD:默认值,由线程池来决定命令的执行,如线程池满载,则不会执行命令。 Hystrix使用了 ThreadPoolExecutor来控制线程池行为,线程池的默认大小为10。
  • SEMAPHORE:由信号量来决定命令的执行,当请求的并发数高于阈值时,就不再 执行命令。相对于线程策略,信号量策略开销更小,但是该策略不支持超时以及异 步,除非对调用的服务有足够的信任,否则不建议使用该策略进行隔离。

 

接下来,使用代码测试线程隔离与信号隔离两个策略,编写共用的命令类,请见代码:

public class MyCommand extends HystrixCommand<String> {
  int index;
  public MyCommand(int index) {
    super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")));
    this.index = index;
  }
  protected String run() throws Exception {     Thread.sleep(500);     System.out.printIn("执行方法,当前索引:"+ index);     return "";   }
  @Override   protected String getFallback() (     System.out.printIn("执行 fallback,当前索弓|: " + index);     return "";   } }

  

编写线程隔离的运行方法,请见代码:

public class Threadlso {
  public static void main(String[] args) throws Exception {
    //配置线程池大小为3
    ConfigurationManager.getConfiginstance().setProperty(
        "hystrix.threadpool.default.coreSize", 3);
    for (int i = 0; i < 6; i++) {
      MyCommand c = new MyCommand(i);
      c.queue();
    }
    Thread.sleep(5000);
  }
}

  

在使用线程隔离策略的运行类中,配置了线程池大小为3,进行6次循环,意味着有3 次命令将会触发回退,运行后可看到效果。代码清单6-12测试信号量隔离策略。

public class Semaphorelso {
  public static void main(String[] args) throws Exception {
    //配置使用信号量的策略进行隔离
    ConfigurationManager.getConfiglnstance()・ setProperty(
          "hystrix.command.default.execution.isolation.strategy",
               Executionisolationstrategy.SEMAPHORE);
    //设置最大并发数,默认值为10,本例设置为2 
    ConfigurationManager.getConfiglnstance().setProperty(
          "hystrix.command.default.execution.isolation.semaphore.maxConcurrentReques",2);     //设置执行回退方法的最大并发,默认值为10,本例设置为20
    ConfigurationManager.getConfiglnstance().setProperty(
          "hystrix.command.default.fallback.isolation.semaph ore.maxConcurrentRequests",20);
    for (int i = 0; i < 6; i++) {       final int index = i;       Thread t = new Thread() (         public void run() {           MyCommand c = new MyCommand(index);           c.execute();         }       )       t.start();     }     Thread.sleep(5000);
  }
}

注意代码中的3个配置项,指定使用信号量作为隔离策略,分别设置了命令执行的最 大并发数,以及执行回退的最大并发数。

运行代码清单6-12,最终只有两个命令正常执行, 其余命令都会触发回退,可见信号量隔离的相关配置生效。

线程(线程池)与信号量两种隔离策略各有优缺点,如果对于所调用的服务有足够的 信任,可以使用信号量策略,以减少系统开销。

  

6.3.8合并请求

  根据前面小节的介绍可知,默认情况下,会为命令分配线程池来执行命令实例,线程 池会消耗一定的性能。对于一些同类型的请求(URL相同,参数不同),Hystrix提供了合 并请求的功能,在一次请求的过程中,

可以将一个时间段内的相同请求(参数不同),收集 到同一个命令中执行,这样就节省了线程的开销,减少了网络连接,从而提升了执行的性 能。这个功能有点像数据库的批处理功能。

实现合并请求的功能,至少包含以下3个条件:

  • 需要有一个执行请求的命令,将全部参数进行整理,然后调用外部服务。
  • 需要有一个合并处理器,用于收集请求,以及处理结果。
  • 外部接口提供支持,例如外部的服务提供了/person/{personName}的服务用于查找一 个Person,如果合并请求,夕卜部还需要提供一个/persons的服务,用于查找多个Person=

接下来,实现一个简单的查找逻辑。假设有以下场景:客户端多次调用查找单个Person 的Web服务,而服务端提供了一个新的服务,可以传入多个名字,查找并返回多个Person 实例,此时,可以考虑使用合并请求。

编写一个命令类,用于收集请求参数以及调用服务, 请见代码清单6-13。

public class CollapseTest{
  static class CollapserCommand extends HystrixCommanci<Map<String, Person>> { 
    //请求集合,第一个类型是单个请求返回的数据类型,第二是请求参数的类型
    Collection<CollapsedRequest<Person, String>> requests;
    private CollapserCommand(       Collection<CollapsedRequest<Person, String» requests) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")));         this.requests = requests;     )
    @Override
    protected Map<String, Person> run() throws Exception {       System.out.printin (n收集参数后执行命令,参数数量:” + requests .size ());       //处理参数       List<String> personNames = new ArrayList<String>();       for(CollapsedRequest<Person, String> request : requests) {
        personNames.add(request.getArgument());       }       //调用服务(此处模拟调用),根据名称获取Person的Map       Map<String, Person> result = callservice(personNames);       return result;     }
    //模拟服务返回     private Map<String, Person> callservice(List<String> personNames) {
      Map<String, Person> result = new HashMap<String, Person>();
      for(String personName : personNames) {         Person p = new Person();         p.id = UUID. randomUUID () .toStringO ;         p.name = personName;         p.age = new Random().nextlnt(30);         result.put(personName, p);       }       return result;
    }
  }
  static class Person {     String id;     String name;     Integer age;     public String toString() {       // TODO Auto-generated method stub return "id: " + id + ”,name: '* + name + age: ” + age;     }   } }

  在命令类 CollapserCommand 中,维护着一个 CollapsedRequest 集合,一个 CollapsedRequest 实例表示一个请求,该类指定的第一个类型为“单请求返回的类型”,第二个类型为请求的 参数类型。

例如,CollapsedRequest<Person, String>,表示单次请求将会以String作为参数, 返回一个Person实例。代码清单中的粗体部分模拟调用查询多个Person的服务。

接下来,编写合并处理器,将请求进行合并,请见代码:

/**
 * 合并处理器
 * 第一个类型为批处理返回的结果类型
 * 第二个为单请求返回的结果类型
 * 第三个是请求参数类型
 * @author杨恩雄
 */
static class MyHystrixCollapser extends HystrixCollapser<Map<String, Person>, Person, String> {   String personName;   public MyHystrixCollapser(String personName) {     this.personName = personName;   }
  @Override   public String getRequestArgument() {     return personName;   }
  @Override   protected HystrixCommand<Map<String, Person>> createCommand (Collection<CollapsedRequest<Person, String>> requests) {
    return new CollapserCommand(requests);   }
  @Override   protected void mapResponseToRequests( Map<String, Person> batchResponse, Collection<CollapsedRequest<Person, String>> requests) {     //让结果与请求进行关联     for (CollapsedRequest<Person, String> request : requests) {       //获取单个响应返回的结果       Person singleResult = batchResponse.get(request.getArgument());
      //关联到请求中       request.setResponse(singleResult);
    }
  }
}

  合并处理器中实现了父类的3个方法,getRequestArgument用于返回请求的参数,本例需要根据名称查询Person,因此该参数为字符串;

createCommand返回实际执行的命令 (查找多个Person的批处理命令);

mapResponseToRequests方法将会在返回结果后执行' 可在该方法中设置结果与请求之间的关联。

 接下来,编写运行类,请见代码清单6.15。

public class CollapseTest{
  public static void main(String[] args) throws Exception {
    //收集1秒内发生的请求,合并为一个命令执行
    ConfigurationManager.getConfiglnstance().setProperty(
          "hystrix.collapser.default.timerDelaylnMilliseconds", 1000); 
    //请求上下文
    HystrixRequestContext context = HystrixRequestContext.initializeContext();
    //创建请求合并的处理器
    MyHystrixCollapser c1 = new MyHystrixCollapser("Angus");
    MyHystrixCollapser c2 = new MyHystrixCollapser("Crazyit");
    MyHystrixCollapser c3 = new MyHystrixCollapser("Sune");
    MyHystrixCollapser c4 = new MyHystrixCollapser("Paris");
    //异步执行
    Future<Person> f1 = c1.queue();
    Future<Person> f2 = c2.queue();
    Future<Person> f3 = c3.queue();
    Future<Person> f4 = c4.queue();

    System.out.printin(fl.get()); 
    System.out.printin(f2.get()); 
    System.out.printin(f3.get()); 
    System.out.printin(f4.get());
    context.shutdown();

  }
}  

意粗体部分,设置了时间,在1秒内执行的请求将会被合并到一起执行,该间段的默认值为10毫秒。运行代码清单6-15,可以看到输出如下:

收集参数后执行命令,参数数量:4
id: 57821cac-d925-4a8e-9cd4-067c8171a626, 
id: 9f984af6-0e7d-41b0-96f9-941863c3e093,
id: c8483f21-9382-436a-b01b-3aelc8ecfb68,
id: 5514a5e4-8af6-4730-ab5c-080affcecc2e,

根据结果可知,创建了 4个合并处理器,最终只执行了 1次命令。

 

虽然合并请求后只执行了一个命令,只启动了一个线程,只进行了一次 网络请求,但是在收集请求、合并请求、处理结果的过程中仍然会耗费一定 的时间。

总的来说,一般情况下,合并请求进行批处理,比发送多个请求快, 对于一些服务的URL相同、参数不同的请求,笔者推荐使用合并请求的功能。

6.3.9请求缓存

Hystrix支持缓存功能,如果在一次请求的过程中,多个地方调用同一个接口,可以考 虑使用缓存。缓存打开后,下一次的命令不会执行,直接到缓存中获取响应并返回,具体 可参见6.2.4节介绍的运作流程。

开启缓存较为简单,在命令中重写父类的getCacheKey 可。代码清单6-16测试开启缓存和清空缓存 

public static void main(String[] args) {
  //初始化请求上下文
  HystrixRequestContext context = HystrixRequestContext.initializeContext(); 
  //请求正常的服务   String key = "cache-key";   MyCommand cl = new MyCommand(key);   MyCommand c2 = new MyCommand(key);   MyCommand c3 = new MyCommand(key);   //输出结果   System.out.printIn(cl.execute() + "cl 是否读取缓存:"+cl. isResponseFromCache());   System.out.printIn(c2.execute() + "c2 是否读取缓存:"+c2.isResponseFromCache());   System.out.printIn(c3.execute() + "c3 是否读取缓存:"+c3.isResponseFromCache());   //获取缓存实例   HystrixRequestCache cache = HystrixRequestCache.getlnstance(     HystrixCommandKey.Factory.asKey("MyCommandKey"),
    HystrixConcurrencyStrategyDefault.getlnstance());
  //清空缓存   cache.clear(key);   //重新执行命令   MyCommand c4 = new MyCommand(key);   System.out .printIn (c4 .execute () + ”c4 是否读取缓存:*' +   c4.isResponseFromCache()); context.shutdown(); }
static class MyCommand extends HystrixCommand<String> {   private String key;   public MyCommand(String key) {     super(       Setter.withG^oupKey(HystrixCommandGroupKey.Factory;asKey("ExampleGroup"))       .andCommandKey (HystrixCommandKey.Factory.asKey("MyCommandKey"))     );     this.key = key;   }
  protected String run() throws Exception {     System.out.printIn(”执行命令");     return "";   }
  @Override   protected String getCacheKey() {
    return this.key;   }
}

在代码清单的运行类中,使用了同一个缓存的key来创建命令实例。注意粗体代码,使 HystrixRequestCache 来清空缓存。获取 HystrixRequestCache 实例需要传入 CommandKey,

因此在命令的构造器中,也设置了 CommandKeyo运行代码清单6-16,输出结果如下:

执行命令
C1是否读取缓存:false
c2是否读取缓存:true
c3是否读取缓存:true
执行命令
c4是否读取缓存:false

  根据输出结果可知,命令实际上只执行了两次,C2与C3这两个命令执行时都读取了缓 存,而C4在执行前清空了缓存。

 

注意: 合并请求、请求缓存,在一次请求的过程中才能实现,因此需要先初始化请求上下文。

  Hystrix的使用到此结束,下面将讲解在Spring Cloud中如何使用Hystrixo Spring Cloud对Hystrix进行了封装,我们只要懂得Hystrix的机制或者原理,

在使用Spring Cloud 的Hystrix注解时,就会变得非常轻松,也可以大概知道Spring Cloud帮我们做了哪些 工作。 

6.4 Spring Cloud 中使用 Hystrix

  Hystrix主要用于保护调用服务的一方,如果被调用的服务发生故障,符合一定条件, 就开启断路器,对调用的程序进行隔离。在开始讲述本节的内容之前,先准备测试项目, 本节中的例子所使用的项目如下所述。

  • spring-hystrix-server: Eureka 服务器,端口 8761, 代码目录为 codes\06\6.4\spring- hystrix-servero
  • spring-hystrix-provider:服务提供者,本例只需要启动一个实例,端口为8080,默认 提Wperson/{personld}服务。根据personld参数返回一个Person实例,另外再提供一 个/hello服务,返回普通的字符串。

    代码目录为codes\06\6.4\spring-hystrix-provider

  • spring-hystrix-invoker:服务调用者,端口为 9000, 代码目录为 codes\06\6.4\spring - hystrix- invoker

 

6.4.1 整合 Hystrix

为服务调用者(spring-hystrix-invoker)项目添加依赖,添加的依赖如下:

<dependency>
  <groupld>org.springframework.cloud</groupld>
  <artifactld>spring-cloud-starter-config</artifactld>
</dependency>
<dependency>
  <groupld>org.springframework.cloud</groupld>
  <artifactld>spring-cloud-starter-eureka</artifactld>
</dependency>
<dependency>
  <groupld>org.springframework.cloud</groupld>
  <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
  <groupld>org.springframework.cloud</groupld>
  <artifactld>spring-cloud--starter-hystrix</artifactld> </dependency>

  

在服务调用者的应用启动类中,加入启用断路器的注解,请参见以下代码片断:

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class InvokerApplication {
  @LoadBalanced   @Bean   public RestTemplate getRestTemplate() {     return new RestTemplate();   }
  public static void main(String[] args) {     SpringApplication.run(InvokerApplication.class, args);
  }
}

  

新建服务类,在服务方法中调用服务,请见代码:

public class PersonService {

  @Autowired   private RestTemplate restTemplate;
  @HystrixCommand(fallbackMethod = "getPersonFallback")   public Person getPerson(Integer id) {     // 使用 RestTemplate 调用 Eureka 服务     Person p = restTemplate.getForObj ect(             "http://spring-hystrix-provider/person/(personld}", Person.class, id);     return p;   }
  /**    * 回退方法,返回一个默认的Person    */   public Person getPersonFallback(Integer id) {     Person p = new Person();     p.setld(0);     p.setName("Crazyit");     p.setAge(-1);     p.setMessage("request error");     return p;   } }

  服务类中注入了 RestTemplate,服务方法使用@HystrixCommand注解进行修饰,并且 配置了回退方法。@HystrixCommand注解由Hystrix的javanica项目提供,该项目主要是 为了 简化 Hystrix 的使用。

被@HystrixCommand修饰的方法,Hystrix(javanica)会使用 AspectJ 对其进行代理,Spring会将相关的类转换为Bean放到容器中,在Spring Cloud中,我们无 须过多关心Hystrix的命令管理。

接下来,编写控制器,调用服务类的方法,请见代码

@RestController
@Configuration
public class InvokerController {
  @Autowired
  private PersonService personService;
  @RequestMapping(value = "/router/(personId}", method = RequestMethod.GET,
                produces = MediaType.APPLICATION_JSON_VALUE)   public Person router(@PathVariable Integer personld) {     Person p = personservice.getPerson(personld);     return p;   } }

  

控制器的实现较为简单,直接注入PersonService,然后调用方法即可,按以下步骤启动集群:

  • 启动 spring-hystrix-server,本例中配置端口 8761
  • 启动 spring-hystrix-provider,启动一个实例,端口 8080
  • 启动 spring-hystrix-invoker,端口 9000

打开浏览器访问http://localhost:9000/router/l,输出如下:

("id":1,"name": '*Crazyitn,"age":33,"message":"http://localhost:8080/person/l" }

  

停止服务提供者(spring・hystrix・provide),即停止8080端口,再访问9000端口的地址, 输出如下:

("id”:0,”name”:"Crazyit","age”:-1,"message":"request error**}

  

根据输出可知,由于调用失败,触发了回退方法。

6.4.2命令配置

  在Spring Cloud中使用@HystrixCommand来声明一个命令,命令的相关配置也可以在该注解中进行,

以下的代码片断配置了几个属性:

/**
* 测试配置,对3个key进行命名 * 设置命令执行超时时间为1000毫秒 * 设置命令执行的线程池大小为1 */ @HystrixCommand( fallbackMethod="testConfigFallback", groupKey="MyGroup", commandKey="MyCommandKey", threadPoolKey="MyCommandPool",
   cominandProperties= {         @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "l000")    },    threadPoolProperties= {         @HystrixProperty(name = "coreSize", value = "1")    }
)

  

除了以上的几个配置外,@HystrixCommand注解还可以使用ignoreExceptions来处理 异常的传播,请见以下代码片断:

/**
 * 声明了忽略MyException,如果方法抛出MyException,则不会触发回退
 */
@HystrixCommand(ignoreExceptions = {MyException.class}, fallbackMethod="testExceptionFallBack")
public String testException() {
  throw new MyException();
}

  

  Hystrix的命令、线程配置较多,由于篇幅所限,本小节仅简单地列举几个,读者可举 一反三,按需要进行配置。

6.4.3默认配置

对于一些默认的配置,例如命令组的key等,可以使注解,这样就 减少了@HystrixCommand注解的代码量。以下代码片断展示了如何使用:

@DefaultProperties:
@DefaultProperties(groupKey="GroupPersonKey")
public class PersonService {
  @HystrixCommanci 
  // groupkey 将使用 GroupPersonKey   public String hello() {     return "";   } }

  除了定义GroupKey外,还支持@HystrixCommand的其余配置,例如线程属性、命令属性等。

 6.4.4缓存注解

在6.3节中讲述了 Hystrix的缓存功能,在Spring Cloud中,同样支持使用缓存,并且 可以通过注解来实现。根据前面章节的介绍可知,缓存与合并请求功能需要先初始化请求上下文才能实现。

建一个javax.sei-vlet.Filter,用于创建与销毁Hystrix的请求上下文, 请见代码:

@WebFilter(urlPatterns = f ilterName = "hystrixFilter")
public class HystrixFilter implements Filter {
  public void init(FilterConfig filterConfig) throws ServletException {
  }

  public void doFilter(ServletRequest request, ServletResponse response,Filterchain chain) throws lOException, ServletException {     HystrixRequestContext context = HystrixRequestContext.initializeContext();     try {       chain.doFilter(request, response);     } finally {       context.shutdown();     }   }
  public void destroy() {   } }

  

编写服务方法,使用@CacheResult注解进行修饰,请见代码 :

@Component
public class CacheService {
  @CacheResult
  @HystrixCommand
  public Person getPerson(Integer id) {
    System.out.printIn("getPerson 方法");
    Person p = new Person();
    p.setld(id);
    p.setName("angus");
    return p;
  }
}

  

注意,在服务方法中,被调用一次就会进行一次控制台输出。在控制器的方法中,调 用多次getPerson方法,控制器代码请见代码 :

public class lnvokerController{

  @RequestMapping(value = "/cachel/{personld}", method = RequestMethod.GET,
            produces = MediaType.APPLICAT10N_JSON_VALUE)
  public Person testCacheResult(@PathVariable Integer personld) {
    //调用多次服务
    for(int i = 0; i < 3; i++) (
      Person p = cacheService.getPerson(personld);
      System.out.printin("控制器调用服务"+ i);
    }
    return new Person();
  }
}

  

控制器中调用了多次服务方法,也就是用户发送请求后,会执行多次服务方法,启动

“服务调用者”,访问以下地址http://localhost:9000/cachel/l, 控制台输岀如下:

执行getPerson方法
控制器调用服务0
控制器调用服务1
控制器调用服务2

  

根据输出结果可知,在一次用户请求的过程中,服务方法只执行了一次,缓存生效。

 

缓存的注解主要有以下3个。

  • @CacheResult:该注解修饰方法,表示被修饰的方法返回结果将会被缓存,需要与 @HystrixCommand —起使用。
  • @CacheRemove:用于修饰方法让缓存失效,需要与@CacheResult的缓存key关联。
  • @CacheKey:用于修饰方法参数,表示该参数作为缓存的key。

前面的例子使用了@CacheResult注解,下面的代码片断,@CacheResult 一起使用:

@CacheResult()
@HystrixCommanci(commandKey = "removeKey")
public String cacheMethod(String name) {
  return "hello";
}
@CacheRemove (commandKey = "removeKey") @HystrixCommand public String updateMethod(String name) {   return nupdateH; }

  以上代码片断中的cacheMethod方法,使用的缓存key为removeKey,方法updateMethod 被调用后,将会删除key为updateMethod的缓存。

关于3个缓存注解更深入的使用,本小节不进行讲述,读者可以自行测试。

 

6.4.5合并请求注解

在Spring Cloud中同样支持合并请求,在一次HTTP请求的过程中,收集一段时间内 的相同请求,放到一个批处理命令中执行

实现合并请求,同样需要先初始化请求上下文, 具体请参见6.4.4节中的Filter;接下来,编写服务类,请见代码清单6-22。

@Component
public class CollapseService {
  //配置收集1秒内的请求
  @HystrixCollapser(batchMethod = "getPersons", collapserProperties = 
               {
                @HystrixProperty (name = "timerDelaylnMilliseconds" , value = "1000")
               }   )   public Future<Person> getSinglePerson(Integer id) {     System.out .printin ("执行单个获取的方法”);     return null;   }

  @HystrixCommand   public List<Person> getPersons(List<Integer> ids) {     System.out.printIn("收集请求,参数数量:" + ids.size());     List<Person> ps = new ArrayList<Person> ();     for (Integer id : ids) {       Person p = new Person();       p.setld(id);       p.setName("crazyit");       ps.add(p);     }
    return ps;
  }
}

  在代码清单中,最后真实执行的方法为getPersons, getSinglePerson方法使用了 @HystrixCollapser注解来修饰,会收集1秒内调用getSinglePerson的请求,放到getPersons 方法中进行批处理。

控制器中多次调用getSinglePerson方法,如代码清单6-23所示。

public class lnvokerController{
  @RequestMapping(value = "/collapse**, method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) 
  public String testCollapse() throws Exception {     //连续执行3次请求     Future<Person> fl = collapseService.getSinglePerson(1);
    Future<Person> f2 = collapseService.getSinglePerson(2);
    Future<Person> f3 = collapseService.getSinglePerson(3);
    Person pl = fl.get();     Person p2 = f2.get();     Person p3 = f3.get();     System.out .printIn (pl.getId () + "" + pl.getName ());     System.out.printIn(p2.getId() + " " + p2.getName());     System.out .printIn (p3 .getId () + " " + p3 .getName ());
    return "";   } }

  

异步执行了 3次getSinglePerson方法,启动*'月艮务调用者",访问以下地址http://localhost: 9000/collapse,控制台输出如下:

收集请求,参数数量:3
1	crazyit
2	crazyit
3	crazyit

  

根据输出结果可知,最终只执行了 getPersons方法。相对于直接使用Hystrix,在Spring Cloud中合并请求较为简单,合并处理器已经由@HystrixCollapser注解帮我们实现,我们 仅关心真正命令的执行即可。 

6.4.6 Feign Hystrix 整合

Feign对Hystrix提供了支持,为“服务调用者”加入以下Feign依赖:

<dependency>
  <groupld>org.springframework.cloud</groupld>
  <artifactId>spring-cloud-starter-feign</artifactId> </dependency>

  

在application.yml中打开Feign的Hystrix开关,请见以下配置:

feign:
hystrix:	.
enabled: true

  

在应用启动类中加入Feign的开关,本小节的“服务调用者”应用启动类所使用的注 解如下:

@SpringBootApplication

@EnableDiscoveryClient

@ Enabled rcuitBreaker

@ServletComponentScan

@EnableFeignClients

新建Feign接口,调用“服务提供者(spring-hystrix-provider)”的/hello服务,请见代码

@FeignClient (name = "spring-hystrix-provider", fallback = HelloClientFallback.class)
public interface HelloClient {
  @RequestMapping(method = RequestMethod.GET, value = "/hello") 
  public String hello();
  @Component   static class HelloClientFallback implements HelloClient {     public String hello() {       System.out.printIn ("hello 方法的回退");       return "error hello";     }   } }

  

与普通的Feign客户端无异,仅仅设置了处理回退的类,回退类实现了客户端接口。 为了能测试效果,修改服务器端的/hello服务,让其有800毫秒的延时。

根据前面章节的介 绍可知,默认情况下,Hystrix的超时时间为1秒,

因此,还需要修改超时设置。请见代码 清单6-25,在application.yml中修改命令配置。

hystrix:
  command:
    HelloClient#hello():
      execution:
        isolation:
          thread:
            timeoutlnMilliseconds: 500 
      circuitBreaker:         requestVolumeThreshold: 3

  

注意,如果是针对全局配置,则使用与下面类似的配置片断:

H默认时间段内发生的请求数
hystrix.command.default.circuitBreaker.requestVolumeThreshold
//超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
如果针对某个客户端,使用下面的配置片断:
hystrix.command.CommandKey.circuitBreaker.requestVolumeThreshold

  

  Feign与Hystrix整合使用时,会自动帮我们生成CommandKey, 格式为:Feign客户端 接口名#方法名()。

例如本例中的客户端为HelloClient, 方法为hello, 生成的CommandKey HelloClient#hello();而默认情况下,生成的 GroupKey 为@FeignClient 注解的 name 属性。

  在以上的配置中,我们针对hello方法设置了超时时间为500毫秒,而/hello服务超时 时间为800毫秒,换言之,hello方法总会超时。

另外,如果请求超过3次并且失败率超过 50%,断路器将被打开。编写控制器,调用hello服务,并查看断路器的情况,请见代码:

@RestController
public class HelloController {
  @Autowired
  HelloClient helloClient;
  @RequestMapping(value = "/feign/hello", method = RequestMethod.GET)
  public String feignHello () {     // hello方法会超时     String helloResult = helloClient.hello(); 1     //获取断路器     HystrixCircuitBreaker breaker = HystrixCircuitBreaker.Factory.getlnstance(
                                HystrixCommandKey.Factory.asKey("HelloClient#hello()"));     System.out.printin("断路器状态:" + breaker.isOpen());     return helloResult;   } }

  

在控制器的方法中,获取了 hello方法的断路器,并输出其状态。接下来,编写一个测 试客户端,多线程访问 http://localhost:9000/feign/hello/{index),

就是控制器的 feignHello方法,客户端请见代码:

public class TestFeignClient {
  public static void main(String[] args) throws Exception {
    //创建默认的HttpClient
    final CloseableHttpClient httpclient = HttpClients.createDefault ();
    //调用多次服务并输出结果
    for(int i = 0; i < 6; i++) {
      //建立线程访问接口
      Thread t = new Thread() {
        public void run() {
          try {
            String url = "http://localhost:9000/feign/hello";
            //调用GET方法请求服务
            HttpGet httpget = new HttpGet(url);
            //获取响应
            HttpResponse response = httpclient.execute(httpget);
            //根据响应解析出字符串
            System.out.printin(EntityUtils.toString(response.getEntity()));
          } catch (Exception e) {
            e.printStackTrace();  
          }
        }
      );
      t.start();
    }
    //等待完成
    Thread.sleep(15000);
  }
}

  完成后,依次启动Eureka服务器、服务提供者、服务调用者,运行代码清单6-27, 看到“服务调用者”的控制台输出如下:

断路器状态:false
断路器状态:false
断路器状态:false
断路器状态:false
断路器状态:true
断路器状态:true

 根据输出可知,断路器己经被打开。 

6.4.7 Hystrix 监控

 为服务调用者加入Actuator,可以对服务调用者的健康情况进行实时监控。例如可以 看到某个方法的断路器是否打开、当前负载等情况,

为服务调用者(spring-hystrix-invoker) 加入以下依赖:

<dependency>
  <groupld>org.springframework.boot</groupld>
  <artifactld>spring-boot-starter-actuator</artifactld>
  <version>l.5.3.RELEASE</version> </dependency>

  

重新启动 spring-hystrix-invoker (9000 端口),访问 http://localhost:9000/hystrix.stream, 可以看到Hystrix输出的stream数据。

接下来,新建一个监控的Web项目,名称为 hystrix-dashboard,对应的代码路径为codes\06\6.4\hystrix-dashboard,

为该项目加入以下依赖: 

<dependency>
  <groupld>org.springframework.cloud</groupld>
  <artifactld>spring-cloud-starter-hystrix-dashboard</artifactld>
</dependency> <dependency>   <groupld>org.springframework.cloud</groupld>
  <artifactld>spring-cloud-starter-hystrix</artifactld> </dependency> <dependency>   <groupld>org.springframework.boot</groupld>   <artifactld>spring-boot-starter-actuator</artifactld>   <version>l.5.3.RELEASE</version> </dependency>

  

该项目的启动类代码如下:

@SpringBootApplication
@EnableHystrixDashboard
public class MyApplication {
  public static void main(String[] args) {
    //设置启动的服务器端口
    new SpringApplicationBuilder(MyApplication.class).properties("server.port=8082").run(args);
  }
}

  

使用了@EnableHystrixDashboard注解开启Hystrix控制台,启动的端口为8082;

完成后,启动整个集群,最后再启动监控项目,在浏览器输入以下地址http://localhost:8082/hystrix, 可以看到界面如图6.5所示

 

 

 

在文本框中输入需要监控的地址,本例需要监控的地址是http://localhost:9000/ hystrix.streamo单击监控按钮后,可看到界面如图所示。

 

如图6-6所示,名称为HelloClient#hello()的命令,断路器被打开,该命令主要用于测 试Feign的断路器(请见6.4.6)。

其他断路器状态正常,读者可以访问本节案例中的各 个URL来测试这些命令的健康情况。

本节案例所监测的是单节点的健康情况,如果需要监控整个集群的情况,可以使用 Turbine框架,该框架将在后面章节中讲述。

  

6.5 本章小结

   本章主要讲解了 Hystrix框架,包括Hystrix的使用、运行机制等内容。如果你希望改 造旧系统,让其拥有更好的容错能力,可以学习本章的6.3节。如果你正在搭建新的项目, 想让项目拥有更好的性能,

可结合Spring Cloud 一起使用Hystrix;

  由于Spring Cloud对Hystrix的封装,使得我们平时在使用Spring Cloud时,几乎感觉 不到Hystrix的存在,但在高并发的场景下,Hystrix的作用就会很明显。

  如果能理解Hystrix 的工作原理,将会对解决实际环境中的复杂问题有很好的帮助。

 

posted @ 2022-05-22 23:21  IT6889  阅读(37)  评论(0编辑  收藏  举报