注:Hystrix已经停止更新,替代产品有 Sentinel、Resilience4j等。

0.Hystrix简介

当服务间的依赖过多时,很可能回出现某个服务故障的情况,那么就有可能出现一系列的服务挂掉。俗称”服务雪崩“

Hystrix的出现就是为了解决这个问题,它是sprig cloud的一个核心组件,主要提供故障保护功能,在一个服务出现故障的情况下,不会导致整个服务出现问题,提高分布式系统的弹性和稳定性。

Hystrix的主要作用有三个:

(1)服务降级(Fallback):比如当服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,Fallback,会发生降级的几种情况:程序运行异常、超时、服务熔断触发服务降级。

(2)服务熔断(Break):类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。三个步骤先进行服务的降级、进而熔断、恢复调用链路。

(3)实时的监控:会持续地记录所有通过Hystrix发起的请求执行信息,并以统计报表和图形的形式展示给用户,包括没秒执行多少成功,多少失败等。

一开始觉得这玩意的功能try catch也能实现啊,为啥非得要用Hytrix呢?

Hytrix监测到服务一旦不响应,则启用服务降级(断路)的方法,返回默认数据,针对于高并发情景比较适用。(比如双11)

另外服务降级的另一个情形,后台有很多微服务,有些微服务的使用频率不高,就先把这些服务停掉,节省资源。

try catch是监测到代码报错后,才会返回一个报错信息。

本搭建完带有hystrix模块后,整个项目框架变成下面这样:

1.搭建Hystrix模块

我们创建一个生产者的module,这个module带有Hystrix。

参考前面的文章,我们创建了一个module,

POM文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>s-cloud</artifactId>
        <groupId>site.longkui</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>s-cloud-provider-hystrix</artifactId>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--eureka 客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>3.1.8-SNAPSHOT</version>
        </dependency>
        <!--  hystrix  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.10.RELEASE</version>
        </dependency>
    </dependencies>
</project>

配置文件:application.yml

server:
  port: 9003 #服务的端口
#spring相关配置
spring:
  application:
    name: service-provider-hystrix  #服务的名称
#eureka配置
eureka:
  client:
    service-url:
      # 填写注册中心服务器地址
      defaultZone: http://localhost:7001/eureka
    # 是否需要将自己注册到注册中心
    register-with-eureka: true
    # 是否需要搜索服务信息
    fetch-registry: true
  instance:
    # 使用ip地址注册到注册中心
    prefer-ip-address: true
    # 注册中心列表中显示的状态参数
    instance-id: ${spring.cloud.client.ip-address}:${server.port}

注意,此处为了不和原有生产者冲突,单独使用一个服务(service-provider-hystrix)来表示生产者,后面消费者也会单独请求这个生产者来测试(不测试负载均衡)

修改启动类:

package site.longkui.scloudproviderhystrix;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;

@SpringBootApplication
@EnableEurekaClient  //表示是一个Eureka的客户端
@EnableHystrix    //开启Hystrix
public class sCloudProviderHystrix {
    public static void main(String[] args) {
        SpringApplication.run(sCloudProviderHystrix.class, args);
    }

}

其中,@EnableHystrix 表示开始Hystrix,另外 @EnableCircuitBreaker这个注解在spring cloud3.0以后的版本中已经弃用,如果你使用srping cloud3.0以后的版本请使用@EnableHystrix这个注解。

controller层,编写一个新的controller

package site.longkui.scloudproviderhystrix.controller;


import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
public class UserController {


    //第一个测试
    @GetMapping("/getNewUser1")
    @HystrixCommand(fallbackMethod = "myError1")
    public String getUser1() {
        return "I`m provider hystrix1 ,return user1";
    }

    //用于处理服务调用失败时的情况,对应上面的fallbackMethod = "myError1"
    public String myError1(){
        return "default error111111";
    }

    //第二个测试
    @GetMapping("/getNewUser2")
    @HystrixCommand(fallbackMethod = "myError2",commandProperties = {
            //2秒钟以内就是正常的业务逻辑,超过两秒则认为服务调用失败
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
    })
    public String getUser2() {
        try {
            //睡眠3秒,这个服务因为上面2秒的限制就会失败
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "I`m provider hystrix1 ,return user2";
    }

    //用于处理服务调用失败时的情况,对应上面的fallbackMethod = "myError2"
    public String myError2(){
        return "default error22222";
    }

}

这样,hystrix模块就编写完了。

然后,启动所有模块,打开Eureka,看看当前模块有没有上线。

然后我们用一个消费者模块来调用这个带有hystrix断路器的模块。

2.测试服务降级

我们找到前面的消费者1那个模块,然后增加消费者的代码:

package site.longkui.consumer1.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/consumer1")
public class userConsumerController {
    @Autowired
    private RestTemplate restTemplate; // 提供多种便捷访问远程http服务的方法,简单的restful服务模板
    /**
     * 消费者接口
     */
    @GetMapping("/getUser")
    public  String getUser(){
        //指出服务地址
        String url="http://service-provider1/user/getUser";
        //返回值类型和我们的业务返回值一致
        return restTemplate.getForObject(url, String.class);
    }

    /**
     * 测试 带有 hystrix 断路器的接口
     *
     */
    @GetMapping("getNewUser1")
    public String getNewUser1(){
        //指出服务地址
        String url="http://service-provider-hystrix/user/getNewUser1";
        return restTemplate.getForObject(url, String.class);
    }
    @GetMapping("getNewUser2")
    public String getNewUser2(){
        //指出服务地址
        String url="http://service-provider-hystrix/user/getNewUser2";
        return restTemplate.getForObject(url, String.class);
    }

}

这个地方我们增加了两个测试,一个是正常的测试,一个是服务超时后的返回的默认数据。

测试接口:

127.0.0.1:8001/consumer1/getNewUser1
127.0.0.1:8001/consumer1/getNewUser2

效果:

可以看到,接口1正常访问。接口2因为时间的限制,没有正常返回,返回的是服务降级以后的数据。

3.测试服务熔断

什么是服务熔断?

服务熔断就像是保险丝一样,当某个接口数据量过大(或其他原因)时,根据设置的熔断机制就会直接让接口断开,等保险丝恢复后在继续访问这个接口(保险丝实际上就是一堆配置开关,可以设置)

我们可以设置熔断的开关,熔断的次数,重试次数等。

hystrix熔断机模型如下:

熔断类型有三种:

(1)熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态。

(2)熔断关闭:熔断关闭不会对服务进行熔断。

(3)熔断半开: 部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。

服务熔断和服务降级有什么区别?

(1)限流:限制并发的请求访问量,超过阈值则拒绝;
(2)服务降级:服务分优先级,牺牲非核心服务(不可用),保证核心服务稳定;从整体负荷考虑;
(3)服务熔断:依赖的下游服务故障触发熔断,避免引发本系统崩溃;系统自动执行和恢复

服务熔断的基本配置:

@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),		// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),	// 请求次数阈值
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), 	// 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),		// 错误百分比阀值:失败率达到多少后跳闸

(1)circuitBreaker.requestVolumeThreshold:该属性设置滚动窗口中将使断路器跳闸的最小请求数量。如果此属性值为20,则在窗口时间内(如10s内),如果只收到19个请求且都失败了,则断路器也不会开启。

(2)circuitBreaker.sleepWindowInMilliseconds:该属性用来设置当断路器打开之后的休眠时间窗。默认值 5000 毫秒,休眠时间窗结束之后,会将断路器设置为”半开”状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为”打开”状态,如果成功就设置为”关闭”状态。

(3)circuitBreaker.errorThresholdPercentage: 该属性用来设置断路器打开的错误百分比条件。例如,默认值为 50 的情况下,表示在滚动时间窗中,在请求数量超过 circuitBreaker.requestVolumeThreshold 阈值的请求下,如果错误请求数的百分比超过50,就把断路器设置为”打开”状态,否则就设置为”关闭”状态。

我们在hystrix模块增加一个接口,设置熔断器。

package site.longkui.scloudproviderhystrix.controller;


import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
public class UserController {


    //第一个测试
    @GetMapping("/getNewUser1")
    @HystrixCommand(fallbackMethod = "myError1")
    public String getUser1() {
        return "I`m provider hystrix1 ,return user1";
    }

    //用于处理服务调用失败时的情况,对应上面的fallbackMethod = "myError1"
    public String myError1(){
        return "default error111111";
    }

    //第二个测试
    @GetMapping("/getNewUser2")
    @HystrixCommand(fallbackMethod = "myError2",commandProperties = {
            //2秒钟以内就是正常的业务逻辑,超过两秒则认为服务调用失败
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
    })
    public String getUser2() {
        try {
            //睡眠3秒,这个服务因为上面2秒的限制就会失败
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "I`m provider hystrix1 ,return user2";
    }

    //用于处理服务调用失败时的情况,对应上面的fallbackMethod = "myError2"
    public String myError2(){
        return "default error22222";
    }



    //第三个测试,开启断路器
    @GetMapping("/getNewUser3/{id}")
    @HystrixCommand(fallbackMethod = "myError3",commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),		// 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),	// 请求次数阈值
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), 	// 时间窗口期 10秒
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),		// 错误百分比阀值:失败率达到多少后跳闸
    })
    public String getUser3(@PathVariable("id") Integer id){
        long timestamp = System.currentTimeMillis();
        if(id>10){
            System.out.println("exception~"+timestamp);
            throw new RuntimeException("exception~"+timestamp);
        }
        //正常逻辑
        System.out.println("I`m provider hystrix1 ,return user3~"+timestamp);
        return "I`m provider hystrix1 ,return user3~"+timestamp;
    }

    //用于处理服务调用失败时的情况,对应上面的fallbackMethod = "myError2"
    //注意,参数要一致
    public String myError3(Integer id){
        long timestamp = System.currentTimeMillis();
        System.out.println("default error33333~"+timestamp);
        return "default error33333~"+timestamp;
    }
}

然后在消费者处增加一个接口来访问我们刚创建的开启断路器的接口:

package site.longkui.consumer1.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/consumer1")
public class userConsumerController {
    @Autowired
    private RestTemplate restTemplate; // 提供多种便捷访问远程http服务的方法,简单的restful服务模板
    /**
     * 消费者接口
     */
    @GetMapping("/getUser")
    public  String getUser(){
        //指出服务地址
        String url="http://service-provider1/user/getUser";
        //返回值类型和我们的业务返回值一致
        return restTemplate.getForObject(url, String.class);
    }

    /**
     * 测试 带有 hystrix 断路器的接口
     *
     */
    @GetMapping("getNewUser1")
    public String getNewUser1(){
        //指出服务地址
        String url="http://service-provider-hystrix/user/getNewUser1";
        return restTemplate.getForObject(url, String.class);
    }
    @GetMapping("getNewUser2")
    public String getNewUser2(){
        //指出服务地址
        String url="http://service-provider-hystrix/user/getNewUser2";
        return restTemplate.getForObject(url, String.class);
    }

    /**
     * 测试 hystrix开启熔断服务的接口
     */
    @GetMapping("getNewUser3/{id}")
    public String getNewUser3(@PathVariable("id") Integer id){
        String url="http://service-provider-hystrix/user/getNewUser3";
        url=url+"/"+id;
        return restTemplate.getForObject(url, String.class);
    }
}

注意,接口处做了限制,我们单独访问下面两个接口

http://127.0.0.1:8001/consumer1/getNewUser3/1
http://127.0.0.1:8001/consumer1/getNewUser3/13

传递参数1的时候是正常访问,传递参数13的时候是不正常访问。

效果图:

从效果图可以看出,我们一开始访问正常接口时,返回的是正常的数据。然后我们在短时间内狂刷不正常的接口,这个时候回到正常接口时,发现正常接口也不能拿访问了,然后等待一段时间(时间可根据需要自由设置),发现正常的接口恢复了。

这就是熔断机制。