SpringCloud(五)-手把手教你使用Hystrix配置服务熔断和降级以及Hystrix Dashboard

308 阅读10分钟

在分布式系统中,一个子节点的超时或者故障会引起关联节点的故障,从而蔓延到整个系统中,比如库存服务超时,商品服务获取不到库存,订单服务无法获取到商品...因此分布式系统中需要一个容错管理来管理这些容错,SpringCloud提供Hystrix来实现服务降级和熔断,本文将通过一个简单的例子来介绍Hystrix。

系列文章

SpringCloud(一)-手把手教你创建springcloud微服务父子项目
SpringCloud(二)-手把手教你搭建Eureka Server和Eureka Client
SpringCloud(三)-手把手教你通过Rinbbon实现客户端负载均衡
SpringCloud(四)-手把手教你使用OpenFeign
SpringCloud(五)-手把手教你使用Hystrix配置服务熔断和降级以及Hystrix Dashboard
SpringCloud(六)-手把手教你搭建SpringCloud Config配置中心
SpringCloud(七)-手把手教你使用消息总线Bus实现动态刷新
SpringCloud(八)-手把手教你使用Stream消息驱动

1. Hystrix简介

Hystrix提供了服务降级,服务熔断,服务限流等服务。

2. 降级

 降级是指,当请求超时、资源不足等情况发生时进行服务降级处理,不调用真实服务逻辑,而是使用快速失败(fallback)方式直接返回一个托底数据,保证服务链条的完整,避免服务雪崩。

2.1 pom.xml引入依赖

找到消费者8200项目,修改pom.xml配置

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

2.2 主启动类加上@EnableFeignClients注解

package com.elio.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class ProductConsumer8200 {

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

2.3 controller接口中加上fallback函数

ProductConsumerController中以selectById方法为例,假设服务提供者根据传入的id为正则正常,为负数抛出一个异常来模拟。那么在selectById方法上加上@HystrixCommand(fallbackMethod = "getErroInfo")注解,其中getErroInfo就是降级函数

package com.elio.springcloud.controller;

import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class ProductConsumerController {

    @Resource
    RestTemplate restTemplate;

    @Resource
    ProductService productService;

    //public static String url = "http://localhost:8100/";
    public static String url = "http://springcloud-product-provider/";

    /**
     * 查询
     * @return
     */
    @GetMapping("product/consumer/get/info")
    public Result getServiceInfo(){
        return productService.getServiceInfo();
        /*return new Result(200, "查询成功",
                restTemplate.getForObject(url+"product/provider/get/info", Result.class));*/
    }

    /**
     * 查询
     * @param id
     * @return
     */
    @HystrixCommand(fallbackMethod = "getErroInfo")
    @GetMapping("product/consumer/get/{id}")
    public Result selectById(@PathVariable("id") Long id){
        return productService.selectById(id);
       /* return new Result(200, "查询成功",
                restTemplate.getForObject(url+"product/provider/get/"+id, Result.class));*/
    }

    public Result getErroInfo(){
        return new Result(500, "服务器内部出现错误", null);
    }
}

2.4 测试

启动相应服务后,访问消费者API,结果出现异常页面,这不是我们期望的。

image.png

查看控制台日志,发现降级函数要与被处理的函数参数一致,这个也是个小坑

image.png

因此修改上面的getErroInfo加上参数

package com.elio.springcloud.controller;

import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class ProductConsumerController {

    @Resource
    RestTemplate restTemplate;

    @Resource
    ProductService productService;

    //public static String url = "http://localhost:8100/";
    public static String url = "http://springcloud-product-provider/";

    /**
     * 查询
     * @return
     */
    @GetMapping("product/consumer/get/info")
    public Result getServiceInfo(){
        return productService.getServiceInfo();
        /*return new Result(200, "查询成功",
                restTemplate.getForObject(url+"product/provider/get/info", Result.class));*/
    }

    /**
     * 查询
     * @param id
     * @return
     */
    @HystrixCommand(fallbackMethod = "getErroInfo")
    @GetMapping("product/consumer/get/{id}")
    public Result selectById(@PathVariable("id") Long id){
        return productService.selectById(id);
       /* return new Result(200, "查询成功",
                restTemplate.getForObject(url+"product/provider/get/"+id, Result.class));*/
    }

    public Result getErroInfo(Long id){
        return new Result(500, "服务器内部出现错误", null);
    }
}

最终id传入-1,和1的结果如下。我们可以看到,当传入为-1时,参数不正常,服务提供者会抛出异常,因为我们在服务消费者方定义了降级函数,所以会调用降级函数getErroInfo 的逻辑 ,这比上面我们出错抛出错误异常也面要友好的多。当传入为1时,参数正常,返回结果也正常执行正常逻辑。

image.png

image.png

3. 使用OpenFegin 处理降级

通过我们上面的例子我们虽然实现了服务的降级,但是问题就是业务代码和公共代码耦合在一起了,这对后续的阔这和维护增加了困难,因此我们需要另外的将降级逻辑提取出来。我们上篇文章介绍的OpenFeign其实就已经集成了Hystrix,我们可以直接使用OpenFeign来实现服务的降级。

image.png

3.1 修改application.yml配置文件

server:
  port: 8200

spring:
  application:
    name: springcloud-product-consumer
    

eureka:
  instance:
    instance-id: ${spring.application.name}:${server.port}
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8300/eureka/,http://localhost:8301/eureka/
      
feign:
  hystrix:
    enabled: true # fegin默认关闭hystrix服务

3.2 主启动类加上@EnableCircuitBreaker注解

package com.elio.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class ProductConsumer8200 {

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

3.3 新增降级服务类

因为我们要对OpenFeign接口进行服务降级,所以我们需要新增一个回调类实现ProductService接口,对每个方法进行回调处理。新增ProductFallbackServieImpl.java

package com.elio.springcloud.service.impl;

import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import org.springframework.stereotype.Component;

@Component
public class ProductFallbackServieImpl implements ProductService {

    public Result getServiceInfo() {
        return new Result(500,"服务器内部出现错误,导致getServiceInfo接口异常",null);
    }

    public Result selectById(Long id) {
        return new Result(500,"服务器内部出现错误,导致selectById接口异常",null);
    }
}

3.4 商品接口类添加降级类

我们在ProductService接口上的@FeignClient注解,指定对应的降级处理类为ProductFallbackServieImpl

package com.elio.springcloud.service;

import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.impl.ProductFallbackServieImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Component
@FeignClient(name="springcloud-product-provider", fallback = ProductFallbackServieImpl.class)
public interface ProductService {

    /**
     * 查询
     * @return
     */
    @GetMapping("product/provider/get/info")
    public Result getServiceInfo();

    /**
     * 查询
     * @param id
     * @return
     */
    @GetMapping("product/provider/get/{id}")
    public Result selectById(@PathVariable("id") Long id);
}

3.5 测试

接下来就是测试了,首先我们注释掉之前写的@HystrixCommand 注解

package com.elio.springcloud.controller;

import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
public class ProductConsumerController {

    @Resource
    RestTemplate restTemplate;

    @Resource
    ProductService productService;

    //public static String url = "http://localhost:8100/";
    public static String url = "http://springcloud-product-provider/";

    /**
     * 查询
     * @return
     */
    @GetMapping("product/consumer/get/info")
    public Result getServiceInfo(){
        return productService.getServiceInfo();
        /*return new Result(200, "查询成功",
                restTemplate.getForObject(url+"product/provider/get/info", Result.class));*/
    }

    /**
     * 查询
     * @param id
     * @return
     */
    //@HystrixCommand(fallbackMethod = "getErroInfo")
    @GetMapping("product/consumer/get/{id}")
    public Result selectById(@PathVariable("id") Long id){
        return productService.selectById(id);
       /* return new Result(200, "查询成功",
                restTemplate.getForObject(url+"product/provider/get/"+id, Result.class));*/
    }

    public Result getErroInfo(Long id){
        return new Result(500, "服务器内部出现错误", null);
    }
}

依次启动8300,8100,8101,8200,然后测试接口 http://localhost:8200/product/consumer/get/1 ,发现成功进入降级函数,这比将降级逻辑和业务逻辑混杂在一起好很多。

image.png

image.png

4. 熔断

熔断很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力。熔断器一般是在服务提供者配置的,而服务降级是在服务消费端使用的,接下来通过一个简单的例子来了解熔断机制。

4.1 修改pom.xml引入依赖

找到服务提供者项目8100,然后找到对应的pom.xml引入Hystrix依赖

      <!--hystrix -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!-- actuator监控信息完善 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

4.2 主启动类加上注解

我们找到服务提供者项目8100,然后添加注解 @EnableCircuitBreaker,由于我们需要由Hystrix Dashboard实时监控,所以还需要配置一个Servlet。具体仪表盘如何配置,请求第五步。

package com.elio.springcloud;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@SpringBootApplication
@MapperScan("com.elio.springcloud.dao")
@EnableDiscoveryClient
@EnableCircuitBreaker
@Configuration
public class ProductProvider8100 {

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

    @Bean
    public ServletRegistrationBean getServlet(){
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/actuator/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

4.3 servie配置熔断

以service实现类ProductServiceImpl中的selectById方法为例子,来配置熔断信息。其中fallbackMethod配置的降级函数,

  • "circuitBreaker.enabled" 配置是否开启服务熔断
  • "circuitBreaker.requestVolumeThreshold"为时间窗口内的请求阈值,只有达到这个阈值,才会判断是否打开断路器。比如配置为10次,那么在时间窗口内请求9次,9次都失败了也不会打开断路器
  • "circuitBreaker.sleepWindowInMilliseconds"为时间窗口,当断路器打开后,会根据这个时间继续尝试接受请求,如果请求成功则关闭断路器。
  • "circuitBreaker.errorThresholdPercentage"为配置的失败比率,在时间窗口内请求次数达到请求阈值,并且失败比率达到配置的50%,才会打开断路器。
package com.elio.springcloud.service.impl;

import com.elio.springcloud.dao.ProductMapper;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.entity.Product;
import com.elio.springcloud.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class ProductServiceImpl implements ProductService{

    @Resource
    private ProductMapper productMapper;

    @HystrixCommand(fallbackMethod = "selectByIdFallbackHandler", commandProperties = {
            // 是否启用服务熔断
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
            // 请求阈值
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
            // 时间窗口
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
            // 错误比率
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
    })
    public Product selectById(Long id) throws Exception {
        if(id < 0){
            throw new Exception("id为负数");
        }
        return productMapper.selectById(id);
    }

    public Product selectByIdFallbackHandler(Long id){
        return null;

    }
    public int deleteById(Long id) {
        return productMapper.deleteById(id);
    }

    public int updateById(Long id, String name) {
        return productMapper.updateById(id, name);
    }

    public int insertOne(Product product) {
        return productMapper.insertOne(product);
    }
}

4.4 测试

现在配置好了,启动服务提供者8100项目,然后不断地请求http://localhost:8100/product/provider/get/-1接口,这是仪表盘的信息如下:

请求8次,失败率为100%,没有达到请求阈值,断路器还是关闭状态

image.png

请求13次,失败率为100%,达到请求阈值,断路器处于打开状态

image.png

5. Hystrix Dashboard

Hystrix 熔断工作流程就是收集接口的访问情况,从而判断是否打开熔断来保护服务。在开发或者运维的过程中,我们需要一个图形界面实时观察这些信息,接下来就通过搭建Hystrix Dashboard来实时观察Hystrix熔断信息。

5.1 新增springcloud-hystrix-dashboard-8400子项目

新增一个名为springcloud-hystrix-dashboard-8400的dashboard子项目来进行信息监控。

image.png

5.1 修改pom.xml引入依赖

<?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>springcloudtest</artifactId>
        <groupId>com.elio.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-hystrix-dashboard</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <!-- Ribbon相关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <!-- feign相关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
        <!-- hystrix和 hystrix-dashboard相关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
    </dependencies>
</project>

5.2 新增主启动类

新增主启动类 HystrixDashboard8400,主要是加上@EnableHystrixDashboard这个注解

package com.elio.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboard8400 {

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

5.3 测试

接下俩就是启动项目来判断dashboard是否搭建成功,启动后浏览器中输入http://localhost:8400/hystrix

image.png

  • 第一行输入栏就是我们要监控的微服务的地址
  • delay 参数用来控制服务器上轮询监控信息的延迟时间,默认是2000毫秒,可以通过配置该属性来降低客户端的网络和cpu消耗。
  • title 就是监控信息的标题,这个我们一般填写微服务名

5.4 消费者8200修改配置

因为8200开启了熔断配置,因此我们需要监控8200接口的访问信息,此时我们需要修改8200项目的pom.xml新增以下依赖

 <!-- actuator监控信息完善 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

5.5 测试监控消费者8200的信息

现在消费者8200配置好了依赖,然后在dashboard主界面输入我们需要监控的信息,如下图

image.png

点击 monitor stream按钮,结果出现*Unable to connect to Command Metric Stream.*报错信息,但是我们该配置的都配置好了,怎么还会出现这个错误呢,原来因为我们使用的SpringCloud版本是H版本,版本太高了,我们需要配置一个servlet。

image.png

5.6 配置HystrixMetricsStreamServlet

找到消费者端8200的RestConfig配置类,配置HystrixMetricsStreamServlet这个Servlet

package com.elio.springcloud.config;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

    @Bean
    public ServletRegistrationBean getServlet(){
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/actuator/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }

}

5.7 测试

现在该有的配置都配置好了,接下来就是重启服务提供方测试了,再进进入dashboard界面,效果如下

image.png

image.png 现在我们尝试着不断访问接口,效果如下

image.png

仪表盘的数据也会跟着变动,出现的错误信息如下,出现5个错误,100%失败率。

image.png

6 总结

这篇文章简单的介绍了如何在消费端配置服务降级和熔断,但是熔断原理和一些概念性的支持本文没有涉及到,因为优点复杂,需要另开一篇文章来讲解了。