SpringCloud —— Hystrix 断路器

前文

SpringCloud 简介

SpringCloud 版本选型

SpringCloud 工程构建

SpringCloud —— Eureka 注册中心

SpringCloud —— Eureka 集群

SpringCloud —— 服务注册进 Eureka 集群

SpringCloud —— Eureka 自我保护

SpringCloud —— SpringCloud Consul 实现服务注册中心

SpringCloud —— 三个注册中心的异同点

SpringCloud —— Ribbon

SpringCloud —— Ribbon 负载均衡算法

扫描二维码关注公众号,回复: 11127888 查看本文章

SpringCloud —— OpenFeign

SpringCloud —— Hystrix 简介

高并发测试

在进行高并发测试之前,先来看下单个请求的,可以发现,请求 http://localhost:8001/payment/hystrix/ok/1
的时候响应很快

在这里插入图片描述
使用 JMeter 工具模仿高并发,设置 20000 个并发去访问 8001,20000 个请求都是访问 http://localhost:8001/payment/hystrix/timeout/1

然后再去访问 http://localhost:8001/payment/hystrix/ok/1

先清空控制台
在这里插入图片描述
启动 JMeter 工具,设置好 200 个线程,循环 100 次,也就是 20000 次请求
在这里插入图片描述
启动高并发访问
在这里插入图片描述
在使用高并发测再去访问的时候可以发现访问 http://localhost:8001/payment/hystrix/ok/1
的时候也需要等待了
在这里插入图片描述
控制台输出:可右边的滚动条可以发现 timeout 确实接收到了很多的请求
在这里插入图片描述

为什么会卡死

也就是说,当一大波请求去访问 http://localhost:8001/payment/hystrix/timeout/1 的时候,http://localhost:8001/payment/hystrix/ok/1 也被拖慢了

tomcat 的默认的工作线程数被打满了( Windows 每个进程中的线程数不允许超过 2000),没有多余的线程来分解压力和处理

上面仅仅是服务提供者 8001 自己测试,假如此时外部的消费者 80 也来访问,那么消费者只能干等,最终导致消费者 80 不满意,服务端 8001 直接崩了

新建 Module 80(消费者)

添加依赖

<dependencies>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--一般基础通用配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

编写 application.yml 文件

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

编写启动类

package com.java.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author Woo_home
 * @create 2020/3/28 13:59
 */

@SpringBootApplication
@EnableFeignClients
public class HystrixOrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixOrderMain80.class,args);
    }
}

编写业务接口

package com.java.springcloud.service;

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;

/**
 * @author Woo_home
 * @create 2020/3/28 14:00
 */

@Component
@FeignClient(name = "DEMO-PROVIDER-HYSTRIX-PAYMENT") // 上述 8001 的服务实例名称
public interface PaymentHystrixService {

    @GetMapping(value = "/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping(value = "/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}

编写 Controller

package com.java.springcloud.controller;

import com.java.springcloud.service.PaymentHystrixService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author Woo_home
 * @create 2020/3/28 14:03
 */

@RestController
@Slf4j
public class OrderHystrixController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_Timeout(id);
        return result;
    }

}

测试

未开启高并发请求的测试(跟上述一样使用 JMeter),正常访问

在这里插入图片描述
开启高并发之后访问,卡顿

在这里插入图片描述
总结:

  • 消费者要么转圈圈等待
  • 消费端要么超时错误
    在这里插入图片描述

服务降级

服务降级容错解决的维度要求

  • 如果出现超时导致服务器变慢(转圈),那么不再等待
  • 出错(宕机或程序运行出错)
  • 解决
    • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须要有服务降级
    • 对方(8001)宕机了,调用者(80)不能一直卡死等待,必须要有服务降级
    • 对方服务(8001)OK 了, 调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者)

服务降级配置

使用 @HystrixCommand 注解(一旦调用服务方法失败并抛出了错误信息后,会自动调用 @HystrixCommand 标注好的 fallbackMethod 调用类中的指定方法

服务提供者超时设置

*设置服务提供者 8001 自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,做服务降级 fallback

修改服务提供者 8001 的 PaymentService
package com.java.springcloud.service;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author Woo_home
 * @create 2020/3/28 12:33
 */

@Service
public class PaymentService {

    /**
     * 正常访问
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id :" + id;
    }
	
	// ########## 新增注解 ##########
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = {
            // 如果是 3 s 内就走正常逻辑,也就是服务自身调用的峰值
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
    })
    public String paymentInfo_Timeout(Integer id) {
    	// 设置比上面大的时间
        int timeNumber = 5;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + " paymentInfo_timeout,id :" + id + "\t" + "耗时 " + timeNumber + " s";
    }

	// 服务降级会执行的方法
    public String paymentInfo_TimeoutHandler(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeoutHandler,id :" + id + "\t" + "进入 paymentInfo_TimeoutHandler 方法";

    }

}
修该服务提供者 8001 的启动类

增加 @EnableCircuitBreaker 注解
在这里插入图片描述

测试访问

可以发现,使用 @HystrixCommand 注解设置峰值为 3 s,超过这个峰值就会走 fallbackMethod 设置的的方法
在这里插入图片描述
结论:标注 @HystrixCommand 的方法如果超时异常或者计算异常都会 fallback

消费端超时设置

修改 application.yml 文件
server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

feign:
  hystrix:
    enabled: true
修改提供者的超时设置为 5 s

在这里插入图片描述

修改消费端控制器代码
package com.java.springcloud.controller;

import com.java.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

/**
 * @author Woo_home
 * @create 2020/3/28 14:03
 */

@RestController
@Slf4j
public class OrderHystrixController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
            // 如果是 1.5 s 内就走正常逻辑,也就是服务自身调用的峰值
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
    })
    public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_Timeout(id);
        return result;
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者 80,对方支付系统繁忙请 10 s 后再试或者自己运行出错检查自己";
    }
}

在这里插入图片描述

修改消费端启动类

加上 @EnableHystrix 注解

package com.java.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @author Woo_home
 * @create 2020/3/28 13:59
 */

@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class HystrixOrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixOrderMain80.class,args);
    }
}
测试访问

在这里插入图片描述

@DefaultProperties(defaultFallback = “”)

  • 1:1 每个方法配置一个服务降级方法,技术上可以,实际上不太可行
  • 1:N 除了个别重要核心业务有专属,其它普通的可以通过 @DefaultProperties(defaultFallback = “”) 统一跳转到统一处理结果页面

通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量

修改消费端 OrderHystrixController
package com.java.springcloud.controller;

import com.java.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author Woo_home
 * @create 2020/3/28 14:03
 */

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_OK(id);
        return result;
    }

    @GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
    /*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
            // 如果是 1.5 s 内就走正常逻辑,也就是服务自身调用的峰值
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
    })*/
    @HystrixCommand
    public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
        String result = paymentHystrixService.paymentInfo_Timeout(id);
        return result;
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
        return "我是消费者 80,对方支付系统繁忙请 10 s 后再试或者自己运行出错检查自己";
    }

    // 下面是全局 fallback
    public String paymentGlobalFallbackMethod() {
        return "Global 异常处理信息,请稍后再试";
    }
}
测试访问

在这里插入图片描述

FeignFallback

由于上面的代码耦合度较高,所以尽量使用接口的形式

新建 PaymentHystrixService 接口的实现类
package com.java.springcloud.service;

import org.springframework.stereotype.Component;

/**
 * @author Woo_home
 * @create 2020/3/28 16:32
 */

@Component // 记得加上注解
public class PaymentHystrixServiceImpl implements PaymentHystrixService {


    @Override
    public String paymentInfo_OK(Integer id) {
        return "------ PaymentHystrixService fallback paymentInfo OK";
    }

    @Override
    public String paymentInfo_Timeout(Integer id) {
        return "------ PaymentHystrixService fallback paymentInfo timeout";
    }
}
修改 PaymentHystrixService 代码
package com.java.springcloud.service;

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;

/**
 * @author Woo_home
 * @create 2020/3/28 14:00
 */

@Component
// 使用实现类的返回信息,降低耦合度
@FeignClient(name = "DEMO-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentHystrixServiceImpl.class)
public interface PaymentHystrixService {

    @GetMapping(value = "/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping(value = "/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}

测试访问

启动服务端 7001,提供者 8001,消费者 80,正常访问
在这里插入图片描述
故意关掉提供者 8001,模仿宕机,可以发现,页面返回了 PaymentHystrixService 实现类定义的信息
在这里插入图片描述

服务熔断

熔断机制概述

熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息

当检测到该节点微服务调用响应正常后,恢复调用链路

在 SpringCloud 框架里,熔断机制通过 Hystrix 实现,Hystrix 会监控微服务间调用的状况,当失败的调用到一定的阀值,缺省是 5 s 内 10 次调用失败,就会启动熔断机制,熔断机制的注解是 @HystrixCommand

修改服务提供者 8001 的 PaymentService 代码

package com.java.springcloud.service;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;

/**
 * @author Woo_home
 * @create 2020/3/28 12:33
 */

@Service
public class PaymentService {

    /**
     * 正常访问
     * @param id
     * @return
     */
    public String paymentInfo_OK(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id :" + id;
    }

    // ########## 新增注解 ##########
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = {
            // 如果是 3 s 内就走正常逻辑,也就是服务自身调用的峰值
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "5000")
    })
    public String paymentInfo_Timeout(Integer id) {
        // 设置比上面小的时间
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + " paymentInfo_timeout,id :" + id + "\t" + "耗时 " + timeNumber + " s";
    }

    // 服务降级会执行的方法
    public String paymentInfo_TimeoutHandler(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + " 系统繁忙,请稍后再试 " + id + "\t" + "进入 paymentInfo_TimeoutHandler 方法";

    }

    // ====== 服务熔断
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
    })
    public String paymentCircuitBreaker(@PathVariable("id") Integer id)
    {
        if(id < 0)
        {
            throw new RuntimeException("****** id 不能负数");
        }
        String serialNumber = IdUtil.simpleUUID();

        return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
    }
    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
        return "id 不能负数,请稍后再试 id: " +id;
    }
}

其中上面的配置是从 HystrixCommandProperties 中拿的, Hystrix 全部配置都在这个类里面
在这里插入图片描述

修改服务提供者 8001 的控制器代码

增加以下代码
在这里插入图片描述

测试访问

在这里插入图片描述
在这里插入图片描述
当多次错误请求后会出现下面的情况(说明启动了服务熔断),得过段时间才能正确访问
在这里插入图片描述

结论

熔断类型:

  • 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为 MTTR (平均故障处理时间,当打开时长达到所设时钟则进入半熔断状态)
  • 熔断关闭:熔断关闭不会对服务进行熔断
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断

断路器在什么情况下开始起作用?

在这里插入图片描述
涉及到断路器的三个重要参数:快照时间窗请求总数阀值错误百分比阀值

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 s
  • 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为 20,意味着在 10s 内,如果该 Hystrix 命令的调用次数不足 20 次,即使所有的请求都超时或其他原因失败,断路器都不会打开
  • 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了 30 次调用,如果在这 30 次调用中,有 15 次发生了超时异常,也就是超过了 50% 的错误百分比,在默认的设定 50% 阀值情况下,这时候就会将断路器打开
断路器开启或者关闭的条件
  • 当满足一定的阀值的时候(默认 10 s 内超过 20 个请求次数)
  • 当失败率达到一定的时候(默认 10 s 内超过 50% 的请求失败)
  • 到达以上阀值,断路器将会开启
  • 当开启的时候,所有请求都不会进行转发
  • 一段时间后(默认为 5 s),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,则继续开启,重复 4 和 5 步骤
断路器打开之后

1、再有请求调用的时候,将不会调用主逻辑,而是直接调用降级 fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果

2、原来的主逻辑要如何恢复?

对于这一问题,Hystrix 也为我们实现了自动恢复功能,当断路器打开,对主逻辑进行熔断之后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时


完整代码已上传至码云,感兴趣的朋友可以下载测试下 代码地址

发布了227 篇原创文章 · 获赞 1032 · 访问量 24万+

猜你喜欢

转载自blog.csdn.net/Woo_home/article/details/105156341