(六)Spring Cloud Hystrix的使用

  • SpringCloud学习(一)之Eureka搭建

  • SpringCloud学习(二)之Eureka集群

  • SpringCloud学习(三)之服务提供者(Eureka Client)

  • SpringCloud学习(五)之RestTemplate几种请求方式的使用

Hystrix 叫做断路器/熔断器。微服务系统中,整个系统出错的概率非常高,因为在微服务系统中,涉及到的模块太多了,每一个模块出错,都有可能导致整个服务出现故障,当所有模块都稳定运行时,整个服务才算是稳定运行。

我们希望当整个系统中,某一个模块无法正常工作时,能够通过我们提前配置的一些东西,来使得整个系统正常运行,即单个模块出问题,不影响整个系统。

学习到这笔者已经搭建了三个服务了。eureka、app-user、app-pay,接下来笔者将创建一个新的服务app-hystrix。废话不多说,开干。

创建项目

 
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        

先加入web依赖和Erueka客户端依赖。暂时不加入Hystrix依赖,我们先来实现没有Hystrix的时,发送异常情况。

项目创建成功之后,在配置文件加入以下配置:

spring:
  application:
    name: app-hystrix
server:
  port: 3000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1999/eureka
  instance:
    hostname: localhost
    instance-id:  ${eureka.instance.hostname}:${server.port}
    prefer-ip-address: true

然后,在项目启动类上添加如下注解,提供一个 RestTemplate 实例:


@SpringBootApplication
@EnableEurekaClient
public class HystrixApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixApplication.class, args);
    }

    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

接下来提供 Hystrix 的接口。

@Service
public class HystrixService {

    @Autowired
    RestTemplate resTtemplate;

  //调用appUser提供的接口
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }
}



@RestController
public class HystrixController {

    @Autowired
    HystrixService hystrixService;

    @GetMapping("/getUser")
    public String getUser() {
        return hystrixService.getUser();
    }
}

我们在这里需要启动的工程有如下一些。
• eureka工程: 服务注册中心, 端口为1999。
• appUser工程:两个实例启动端口2000和2999。
• appHystrix工程:使用 ribbon 实现的服务消费者, 端口为3000。
在未加入断路器之前, 关闭2999的实例, 发送GET请求到http://localhost:3000/getUser, 可以获得下面的输出:

image.png

下面我们开始引入Spring Cloud Hystrix

  • 在appHystrix工程的pom.xml中引入spring-cloud-starter-netflix-hystrix依赖:
        
            org.springframework.cloud
            spring-cloud-starter-netflix-hystrix
        
  • 在appHystrix工程的主类使用@EnableCircuitBreaker注解开启断路器功能
@SpringBootApplication
@EnableCircuitBreaker
@EnableEurekaClient
public class HystrixApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixApplication.class, args);
    }

    @Bean
    @LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

注意:启动类上的注解,也可以使用 @SpringCloudApplication 代替, 该注解的具体定义如下所示。 可以看到, 该注解中包含了上述我们所引用的三个注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {

}
  • 改造服务消费方式,在HystrixService类的getUser函数上增加 @HystrixCommand注解来指定回调方法:
/**
      * 在这个方法中,我们将发起一个远程调用,去调用 appUser 中提供的接口
      *
      * 但是,这个调用可能会失败。
      *
      * 我们在这个方法上添加 @HystrixCommand 注解,配置 fallbackMethod 属性,这个属性表示
      * 该方法调用失败时的临时替代方法
      */
    @HystrixCommand(fallbackMethod = "error")
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }
    /**
    * 注意,这个方法名字要和 fallbackMethod 一致
    * 方法返回值也要和对应的方法一致
    * @return
    */
    public String error(){

        return "error";
    }

重新启动 appHystrix 服务,调用http://localhost:3000/getUser。当轮询到 2999 服务端时,输出内容为 error, 不再是之前的错误内容,Hystrix 的服务回调生效。除了通过断开具体的服务实例来模拟某个节点无法访问的情况之外, 我们还可以模拟一下服务阻塞(长时间未响应)的情况。 我们对 appUser 的/getUser接口做一些修改,具体如下:

 @GetMapping
  public String getUser() throws InterruptedException {
        System.out.println(new Date() + ">>>" );
        //让处理线程等待几秒钟
        int sleepTime = new Random().nextInt(3000);
        System.out.println("sleepTime:" + sleepTime);
        Thread.sleep(sleepTime);
        return "调用成功: =====" + port;
    }

重启appUser,这次笔者就不启动多个实例了,就启动2020这个实例。
通过 Thread. sleep ()函数可让/getUser接口的处理线程不是马上返回内容,而是在
阻塞几秒之后才返回内容。 由于 Hystrix 默认超时时间为 1000 毫秒, 所以这里采用了 0至3000 毫秒的随机数以让处理过程有一定概率发生超时来触发断路器。

工作原理

流程图

image.png

一、创建HystrixCommand或HystrixObservableCommand对象

首先,构建一个HystrixCommand或是HystrixObservableCommand对象,用来表示对依赖服务的操作请求, 同时传递所有需要的参数。 从其命名中我们就能知道它采用了 “命令模式” 来实现对服务调用操作的封装。 而这两个 Command 对象分别针对不同的应用场景。
• HystrixCommand: 用在依赖的服务返回单个操作结果的时候。
• HystrixObservableCommand: 用在依赖的服务返回多个操作结果的时候。

二、命令执行

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

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

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

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

3、结果是否被缓存

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

4、断路器是否打开

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

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

5、线程池\请求队列\信号量是否占满

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

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

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

  • HystrixCommand.run(): 返回一个单一的结果,或者抛出异常。
  • HystrixObservableCommandconstruct(): 返回一个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步, 当前命令处于 “熔断\短路 ” 状态, 断路器是打开的时候。
  • 第5步, 当前命令的线程池、 请求队列或者信号量被占满的时候。
  • 第6步,HystrixObservableCommand..construct()或HystrixCommand.run()
    抛出异常的时候。

9.、返回成功的响应

当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式
返回。

请求命令

Hystrix 命令就是我们之前所说的 HystrixCommand, 它用来封装具体的依赖服务调用逻辑。
我们以继承类的方式来替代前面的注解方式, 比如:
先在appUser服务中新增方法getName()

@GetMapping("/getName")
    public String getName(String name) {
        System.out.println(new Date() + ">>>" + name);
        return "调用成功: =====" + name;
    }

在appHystrix服务创建MyHystrixCommand类并继承HystrixCommand

public class MyHystrixCommand extends HystrixCommand {

    private RestTemplate restTemplate;
    private String name;
    public MyHystrixCommand(Setter setter,RestTemplate restTemplate,String name) {
        super(setter);
        this.restTemplate = restTemplate;
        this.name = name;
    }

   @Override
    protected Object run() {
        String forObject = restTemplate.getForObject("http://appUser/api/user/getName?name={1}", String.class, name);
        return forObject;
    }
}

通过上面实现的 MyHystrixCommand, 我们既可以实现请求的同步执行也可以实现异步执用逻辑。

@GetMapping("/getUser2")
    public void getUser2() throws ExecutionException, InterruptedException {
        //同步执行
        MyHystrixCommand myHystrixCommand = new MyHystrixCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("gongj")), restTemplate,"哈哈");
        String execute = (String) myHystrixCommand.execute(); //直接执行
        System.out.println(execute);
        //异步执行
        MyHystrixCommand myHystrixCommand2 = new MyHystrixCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("gongj")), restTemplate,"哈哈哈哈");
        Future queue = myHystrixCommand2.queue();  //先入队,后执行
        //调用get 方法来获取结果
        String o = (String) queue.get();
        System.out.println(o);
    }

另外, 也可以通过@HystrixCommand 注解来更为优雅地实现 Hystrix 命令的定义,比如:

  @HystrixCommand
    public String getName(){
        String result = resTtemplate.getForObject("http://appUser/api/user/getName?name={1}", String.class,"嘿嘿");
        return result;
    }

虽然@HystrixCommand 注解可以非常优雅地定义 Hystrix 命令的实现, 但是如上定义的 getName 方式只是同步执行的实现,若要实现异步执行则还需另外定义,比如:
定义如下方法,返回 Future

 //注解异步执行
    @HystrixCommand
    public Future getName2(){
         return new AsyncResult() {
             @Override
             public String invoke() {
                 String forObject = resTtemplate.getForObject("http://appUser/api/user/getName?name={1}", String.class, "嘿嘿");
                 return forObject;
             }
         };
    }

然后,调用该方法:

 @GetMapping("/getUser3")
    public void getUser3() throws ExecutionException, InterruptedException {
        //异步执行
        Future name2 = hystrixService.getName2();
        //调用get方法来获取结果
        String o =  name2.get();
        System.out.println(o);
    }

通过继承的方式使用 Hystrix,如何实现服务容错/降级?重写继承类的 getFallback 方法即可:

public class MyHystrixCommand extends HystrixCommand {

   private RestTemplate restTemplate;
    private String name;
    public MyHystrixCommand(Setter setter,RestTemplate restTemplate,String name) {
        super(setter);
        this.restTemplate = restTemplate;
        this.name = name;
    }

    @Override
    protected Object run() {
        String forObject = restTemplate.getForObject("http://appUser/api/user/getName?name={1}", String.class, name);
        return forObject;
    }

    @Override
    protected Object getFallback() {
        return "error";
    }
}

在 HystrixObservableCommand 实现的 Hystrix 命令中, 我们可以通过重载resumeWithFallback 方法来实现服务降级逻辑。 该方法会返回 一个 Observable 对
象, 当命令执行失败的时候, Hystrix 会将 Observable中的结果通知给所有的订阅者。

若要通过注解实现服务降级只需要使用@HystrixCommand 中的 fallbackMethod参数来指定具体的服务降级实现方法, 如下所示:

@HystrixCommand(fallbackMethod = "error")
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }

public String error(){

        return "error" ;
    }

在使用注解来定义服务降级逻辑时, 我们需要将具体的Hystrix 命令与 fallback 实现函数定义在同一个类中, 并且 fallbackMethod 的值必须与实现fallback 方法的名字相同。 由于必须定义在一个类中, 所以对于 fallback 的访问修饰符没有特定的要求, 定义为private、 protected、 public 均可。

在上面的例子中,error方法将在 getUser执行时发生错误的情况下被执
行。若 error方法实现的并不是一个稳定逻辑,它依然可能会发生异常, 那么我们也可以为它添加@HystrixCommand 注解以生成 Hystrix 命令, 同时使用 fallbackMethod来指定服务降级逻辑, 比如:

 @HystrixCommand(fallbackMethod = "error")
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }

@HystrixCommand(fallbackMethod = "error1")
    public String error(){
        return resTtemplate.getForObject("http://appUser/api/user", String.class);
    }

    public String error1(){
        return "error1";
    }
   @HystrixCommand(fallbackMethod = "error",ignoreExceptions = {ArrayIndexOutOfBoundsException.class})
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }

异常处理

异常传播

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

 @HystrixCommand(fallbackMethod = "error",ignoreExceptions = {ArrayIndexOutOfBoundsException.class})
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }

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

  @Override
    protected Object getFallback() {
        Throwable executionException = getExecutionException();
        System.out.println(executionException.getMessage() + "===");
        executionException.printStackTrace();
        return "error";
    }

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

public String error1(Throwable t){
    return "error1" + t.getMessage();
 }

命令名称、 分组以及线程池划分

一、继承方式

以继承方式实现的 Hystrix 命令使用类名作为默认的命令名称,我们也可以通过 Setter 静态类来设置, 比如:

// 本例中 commandKey为MyHystrixCommand  
// threadPoolKey为gongj
// commandGroupKey为gongj
MyHystrixCommand myHystrixCommand = new MyHystrixCommand(HystrixCommand.Setter.withGroupKey(
                HystrixCommandGroupKey.Factory.asKey("gongj"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("gongjie")), restTemplate,"哈哈");

从上面 Setter 的使用中可以看到, 我们并没有直接设置命令名称, 而是先调用了
withGroupKey 来设置命令组名, 然后才通过调用 andCommandKey来设置命令名称。 这是因为在 Setter 的定义中, 只有 withGroupKey 静态函数可以创建 Setter 的实例,所以 GroupKey 是每个 Setter 必需的参数, 而 CommandKey 则是一个可选参数。

通过设置命令组, Hystrix 会根据组来组织和统计命令的告警、 仪表盘等信息。 那么为什么一定要设置命令组呢?因为除了根据组能实现统计之外, Hystrix 命令默认的线程划分也是根据命令分组来实现的。默认情况下, Hystrix 会让相同组名的命令使用同一个线程池,所以我们需要在创建 Hystrix 命令时为其指定命令组名来实现默认的线程池划分。(组名与线程池的名称是一致的)

如果 Hystrix 的线程池分配仅仅依靠命令组来划分, 那么它就显得不够灵活了, 所以Hystrix 还提供了 HystrixThreadPoolKey 来对线程池进行设置, 通过它我们可以实现更细粒度的线程池划分, 比如:

// 本例中 commandKey为gongjie
// threadPoolKey为gongjiePoolKey
// commandGroupKey为gongj
MyHystrixCommand myHystrixCommand = new MyHystrixCommand(
                HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("gongj"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("gongjie"))
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("gongjiePoolKey")), restTemplate,"");

如果在没有特别指定 HystrixThreadPoolKey 的情况下,依然会使用命令组的方式
来划分线程池。通常情况下,尽量通过 HystrixThreadPoolKey 的方式来指定线程池的划分, 而不是通过组名的默认方式实现划分, 因为多个不同的命令可能从业务逻辑上来看属于同一个组, 但是往往从实现本身上需要跟其他命令进行隔离。

二、注解方式

默认情况下,@HystrixCommand 注解标注的方法名即为命令名称。命令组名为 @HystrixCommand 注解方法所在类的名称。线程池名与命令组名一致。如果需要进行修改只需设置@HystrixCommand 注解的 commandKey、 groupKey 以及 threadPoolKey 属性即可, 它们分别表示了命令名称、 分组以及线程池划分, 比如我们可以像下面这样进行设置:

@HystrixCommand(fallbackMethod = "error",
            ignoreExceptions = {ArrayIndexOutOfBoundsException.class},
            commandKey = "getUsercommandKey",
            groupKey = "getUsergroupKey",
            threadPoolKey="getUserthreadPoolKey")
    public String getUser(){
        String result = resTtemplate.getForObject("http://appUser/api/user", String.class);
        return result;
    }

你可能感兴趣的:((六)Spring Cloud Hystrix的使用)