Spring Cloud Hystrix服务降级熔断限流 , Hystrix是一个用于处理分布式系统的延迟和容错的开源库, 在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

1. 前言

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的"扇出”。 如果扇出的链路.上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的”雪崩效应”。

对于高流量的应用来说,单-的后端依赖可能会导致所有服务 器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

2. Hystrix简介

Hystrix是一个用于处理分布式系统的延迟和容错的开源库, 在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下, 不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝) ,向调用方返回一个符合预期的、可处理的备选响应(FallBack) ,而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

3. Hystrix重要概念

3.1 服务降级

当发生服务异常先立即返回一个提示:服务器器繁忙,请稍后再试,不让客户端一直等待。

下面这些情况可能会导致服务降级;

  1. 程序允许异常
  2. 超时
  3. 服务熔断触发服务降级
  4. 线程线、信号量满导致服务降级

3.2 服务熔断

类似于保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级返回友好提示。

3.3 服务限流

秒杀高并发等操作,限制流量,让请求进行排队,每分钟来N个等,有序进行。

4. 新服务提供者

新建一个cloud-provider-hystrix-payment8001 服务提供者

4.1 项目依赖

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

    <artifactId>cloud-provider-hystrix-payment8001</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </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>
        <dependency>
            <groupId>live.yremp</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

4.2 application.yml

server:
  port: 8001

spring:
  application:
    name: cloud-payment-hystrix-service

eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #集群则注册到多个Eureka 服务端
      defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka
  instance:
    instance-id: payment8001
    prefer-ip-address: true
    #Eureka客户端每隔多久向服务端发送一次心跳
    lease-renewal-interval-in-seconds: 1
    #服务端在距离最后一次收到心跳等待时间,超过就会剔除该微服务
    lease-expiration-duration-in-seconds: 2

4.3 主启动类

package live.yremp.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

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

4.4 service模拟

在services写两个方法模拟一下正常访问和超时访问的情况

package live.yremp.springcloud.services;

import org.springframework.stereotype.Service;

@Service
public class PaymentService {
    //正常方法访问的情况
    public String paymentInfo_OK(Integer id){
        return "线程池"+Thread.currentThread().getName()+"paymentInfo_OK, ID="+id+"\t";
    }
    
    //模拟超时
    public String paymentInfo_Timeout(Integer id) throws InterruptedException {
        Thread.sleep(3000);
        return "线程池"+Thread.currentThread().getName()+"paymentInfo_OK, ID="+id+"\t"+"耗时3S";
    }
}

4.5 Controller调用测试

package live.yremp.springcloud.controller;

import live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
import live.yremp.springcloud.services.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

import static java.lang.Thread.sleep;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    PaymentService paymentService;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        log.info("***************paymentInfo_OK");
        return paymentService.paymentInfo_OK(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id) throws InterruptedException {
        log.info("***************paymentInfo_OK");
        return paymentService.paymentInfo_Timeout(id);
    }
}

测试结果如下:

5. 压力测试

使用Jemeter测试 http://localhost:8001/payment/hystrix/timeout/1 ,但是我们请求发现 http://localhost:8001/payment/hystrix/ok/1 也会变慢,即受到了前一个接口压力的影响。

在这种情况下如果80也有请求这个微服务,速度肯定也是会收到影响的。

6. 新建消费者服务

再新建一个 cloud-consumer-feign-hystrix-order80 ,作为消费者服务。

6.1 项目依赖

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

    <artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <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>
        <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>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>live.yremp</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

6.2 application.yml

server:
  port: 80

spring:
  application:
    name: cloud-order-service

eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: false
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #集群则注册到多个Eureka 服务端
      defaultZone: http://eureka7001.com:7001/eureka, http://eureka7002.com:7002/eureka

ribbon:
  #指的是连接后从服务器读取可用资源的时间
  ReadTimeout: 5000
  #建立连接需要的时间
  Connecttimeout: 5000

logging:
  level:
    #feign以什么级别监听什么接口
    live.yremp.springcloud.services.*: debug

6.3 主启动类

package live.yremp.springcloud;

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

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

6.4 Feign接口

package live.yremp.springcloud.services;

import live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
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(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE")
public interface PaymentFeignHystrixService {

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

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

6.5 Controller

package live.yremp.springcloud.controller;

import live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
import live.yremp.springcloud.services.PaymentFeignHystrixService;
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;

@RestController
@Slf4j
public class OrderHystrixController {
    @Resource
    private PaymentFeignHystrixService paymentFeignHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_OK(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_Timeout(id);
    }
}

6.6 测试访问

此次我们通过消费者服务 cloud-consumer-feign-hystrix-order80 调用服务提供者 cloud-provider-hystrix-payment8001 的两个接口,测试访问情况:

也都是正常访问

6.7 再次测压

再次使用Jemeter对 http://localhost:8001/payment/hystrix/timeout/1 进行测压的时候,我们在消费者服务 cloud-consumer-feign-hystrix-order80 中请求 cloud-provider-hystrix-payment8001 中的接口,也会出现访问缓慢的问题。

下面通过服务降级、服务熔断、服务限流几种方式来解决这种现象。

7. 服务降级优化

7.1 服务提供者服务降级优化

使用 @HystrixCommand注解优化 ,设置Fallback方法,commandProperties:设置请求方法的正常超时时间,超过则执行fallbackMethod方法,避免长时间等待。

package live.yremp.springcloud.services;

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

@Service
public class PaymentService {
    /*
    正常方法访问的情况
     */
    public String paymentInfo_OK(Integer id){
        return "线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"\t";
    }

   /*
   模拟超时
    fallbackMethod:异常 处理方法
     commandProperties:设置请求方法的正常超时时间,超过则执行fallbackMethod
    */
   @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler",commandProperties ={
           @HystrixProperty(name = "execution. isolation. thread. timeoutInMilliseconds",value = "2000")
   } )
    public String paymentInfo_Timeout(Integer id) throws InterruptedException {
        Thread.sleep(3000);
        return "线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"\t"+"耗时3S";
    }

    public String paymentInfoTimeoutHandler(Integer id){
        return "线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"我裂开了┭┮﹏┭┮";
    }
}

同时在主启动类添加 @EnableCircuitBreaker

package live.yremp.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableCircuitBreaker
public class PaymentHystrix8001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentHystrix8001.class,args);
    }
}

此时访问 http://localhost:8001/payment/hystrix/timeout/1 就会执行我们的 Fallback 方法paymentInfoTimeoutHandler()

7.2 消费者服务降级优化

除了服务提供者 cloud-provider-hystrix-payment8001 的降级优化还可以对消费者服务 cloud-consumer-feign-hystrix-order80 进行Hystrix服务降级优化。

7.2.1 修改配置

feign:
  hystrix:
    enabled: true

7.2.2 添加注解

在启动类添加 @EnableHystrix 注解

package live.yremp.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;

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

7.2.3 Controller

package live.yremp.springcloud.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
import live.yremp.springcloud.services.PaymentFeignHystrixService;
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;

@RestController
@Slf4j
public class OrderHystrixController {
    @Resource
    private PaymentFeignHystrixService paymentFeignHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_OK(id);
    }
    @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties ={
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")
    } )
    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_Timeout(id);
    }
    public String paymentInfoTimeoutHandler(Integer id){
        return "当前服务-cloud-consumer-feign-hystrix-order80:,cloud-provider-hystrix-payment8001服务异常!"+"\t"+"线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"我裂开了┭┮﹏┭┮";
    }
}

7.2.4 测试

尝试访问消费者服务,结果如下:

7.3 全局服务降级

如果每个方法上面都加

@HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties ={
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")
    } )

会非常的臃肿,所以尽量让大部分异常等使用一个fallbackMethod ,可以按照下面的步骤添加默认全局的fallbackMethod:

package live.yremp.springcloud.controller;

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 live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
import live.yremp.springcloud.services.PaymentFeignHystrixService;
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;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "defaultGlobalFallbackMethod")
public class OrderHystrixController {
    @Resource
    private PaymentFeignHystrixService paymentFeignHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_OK(id);
    }

    @HystrixCommand
    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_Timeout(id);
    }
    public String paymentInfoTimeoutHandler(Integer id){
        return "当前服务-cloud-consumer-feign-hystrix-order80:,cloud-provider-hystrix-payment8001服务异常!"+"\t"+"线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"我裂开了┭┮﹏┭┮";
    }

    public String defaultGlobalFallbackMethod(Integer id){
        return "defaultGlobalFullbackMethod执行"+"\t";
    }
}

在整个Controller上面添加

@DefaultProperties(defaultFallback = "defaultGlobalFallbackMethod")

然后 @GetMapping("/payment/hystrix/timeout/{id}") 上只添加@HystrixCommand ,没有具体的参数,此时如果异常就会执行 defaultGlobalFallbackMethod()

测试一下

7.4 全局服务降级解耦

上面的 defaultGlobalFullbackMethod() 和Controller层代码混合在一起,代码耦合度较高,下面的方法将进行解耦。

本次案例服务降级处理是在客户端80实现完成的,与服务端8001没有关系只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦

我们新建一个类 PaymentFallbackService 实现 PaymentFeignHystrixService 接口,实现它的所有方法并为每个方法处理异常。

7.4.1 PaymentFallbackService

package live.yremp.springcloud.services;

import org.springframework.stereotype.Component;

@Component
public class PaymentFallbackService implements PaymentFeignHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "-------defualtFallback-------paymentInfo_OK---------异常┭┮﹏┭┮";
    }

    @Override
    public String paymentInfo_Timeout(Integer id) {
        return "-------defualtFallback-------paymentInfo_Timeout---------异常┭┮﹏┭┮";
    }
}

7.4.2 PaymentFeignHystrixService

PaymentFeignHystrixService作为远程服务调用接口,我们需要制定这个接口对应的@FeignClient 中的fullback属性为上面的PaymentFallbackService.class

package live.yremp.springcloud.services;

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(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE",fallback =PaymentFallbackService.class )
public interface PaymentFeignHystrixService {

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


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

删除Controller里面所有服务降级的相关注解设置

package live.yremp.springcloud.controller;

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 live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
import live.yremp.springcloud.services.PaymentFeignHystrixService;
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;

@RestController
@Slf4j
public class OrderHystrixController {
    @Resource
    private PaymentFeignHystrixService paymentFeignHystrixService;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_OK(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id){
        return paymentFeignHystrixService.paymentInfo_Timeout(id);
    }

    public String paymentInfoTimeoutHandler(Integer id){
        return "当前服务-cloud-consumer-feign-hystrix-order80:,cloud-provider-hystrix-payment8001服务异常!"+"\t"+"线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"我裂开了┭┮﹏┭┮";
    }

    public String defaultGlobalFullbackMethod(){
        return "defaultGlobalFullbackMethod执行"+"\t";
    }
}

测试服务降级

8. 服务熔断优化

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

在Spring Cloud框架里,熔断机制通过Hystrix实现。 Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HytrixCommand。

8.1 服务提供者熔断优化

81.1 PaymentService

修改PaymentService,添加一个测试方法 paymentCircuitBreaker()

package live.yremp.springcloud.services;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.UUID;

@Service
@Slf4j
public class PaymentService {
    /*
    正常方法访问的情况
     */
    public String paymentInfo_OK(Integer id){
        return "线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"\t";
    }

   /*
   模拟超时异常
    fallbackMethod:异常 处理方法
     commandProperties:设置请求方法的正常超时时间,超过则执行fallbackMethod
    */
   @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties ={
           @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")
   } )
    public String paymentInfo_Timeout(Integer id) throws InterruptedException {
        Thread.sleep(3000);
        return "线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"\t"+"耗时3S";
    }

    public String paymentInfoTimeoutHandler(Integer id){
        return "线程池:"+Thread.currentThread().getName()+",paymentInfo_OK, ID="+id+"我裂开了┭┮﹏┭┮";
    }

    //=====服务熔断
    @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 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " +id;
    }
}

8.1.2 PaymentController

在PaymentController调用Services中的 paymentCircuitBreaker() 方法

package live.yremp.springcloud.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import live.yremp.springcloud.entries.CommonResult;
import live.yremp.springcloud.entries.Payment;
import live.yremp.springcloud.services.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;
import java.util.UUID;

import static java.lang.Thread.sleep;

@RestController
@Slf4j
public class PaymentController {
    @Resource
    PaymentService paymentService;
    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
        log.info("***************paymentInfo_OK");
        return paymentService.paymentInfo_OK(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id) throws InterruptedException {
        log.info("***************paymentInfo_OK");
        return paymentService.paymentInfo_Timeout(id);
    }

    //服务熔断
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id){
            String result = paymentService.paymentCircuitBreaker(id);
            log.info("------------"+result);
            return result;
    }
}

8.1.3 熔断测试

①先使用一个大于1的id测试正常情况

②连续用多个复数id测试

③多次错误提示后,尝试用正常id访问

④尝试几次正常id之后又恢复正常

8.1.4 小结

涉及到断路器的三个重要参数:快照时间窗请求总数阀值错误百分比阀值

  1. 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据, 而统计的时间范围就是快照时间窗,默认为最近的10秒。
  2. 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
  3. 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。

熔断恢复:

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

8.1.5 @HystrixCommand配置

groupKey = "strGroupCommand",
commandKey ="strCommand",
threadPoolKey = "strThreadPool",
commandProperties = {
//没置隔离策略,THREAD 表示线程池SEMAPHORE: 信号池隔离
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
//当隔离策略选撣信号池隔离的时候,用来没置信号池的大小(最大并发数)
@HystrixProperty(name = "execution.isolation.semaphore . maxConcurrentRequests", value = "10"),
//配置命令执行的超时时间
@HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
//是否启用超时时间
@HystrixProperty(name = "execution.timeout.enabled", value = "true"),
//执行超时的时候是否中断
@HystrixProperty(name = " execution.isolation.thread.interruptOnTimeout", value = "true"),
//执行被取消的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interrupt0nCancel", value = "true"),
//允许回调方法执行的最大并发数
@HystrixProperty(name = "fallback.isolation.semaphore. maxConcurrentRequests", value = "10"),
//服务降級是否启用,是否执行回调函数
@HystrixProperty(name = "fallback.enabled", value = "true"),
//是否启用断路器
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), 
@HystrixProperty(name = "circuitBreaker.enabled", value = "true" ),
//该属性用来没置在熔动时间窗中,断路器熔断的晨小请求数。例如,默认该值为20的时候,
//如果颂动时间窗(默以10秒)内仅收到了19个请求,即使这19 个请求都失败了,断路器也不会打开。
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
//该属性用来没置在熾动时间窗中,表示在熔动时间窗中,在请求数量超过
// circuitBreaker.requestVolumeThreshold的情况下,如果错误请求数的百分比超过50,
//就把断路器设置为”打开”状态,否则就没置为"关闭”状态。
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
//该属性用来没置当断路器打开之后的体眠时间窗。休眠时间窗结束之后,
//会将断路器置为“半开”状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为"打开”状态,
//如果成功就没置为"关闭”状态。
@HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
//断路器强制打开
@HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
//断路器强制关闭
@HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
//演动时间窗没置,该时间用于断路 器判断健康度时需要收集信息的持续时间
@HystrixProperty(name = "metrics.lrollingStats.timeinMilliseconds", value = "10000"),
//该属性用来没置燎动时间窗统计指标信息时划分”桶”的数量,断路器在收集指标信息的时候会根据
//设置的时间窗长度拆分成多个”桶”来累计各度量值,每个”桶”记录了- -段时间内的采集指标。
//比如10秒内拆分成10个”桶"收集这样,所以timeinMilliseconds 必须能被numBuckets 整除。否则会抛异常
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value =“10"),
//该属性用来没置对命令执行的延迟是否使用百分位数来跟踪和计算。如果没置为false,那么所有的概要统计都将返回-1。
@HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
//该属性用来没置百分位统计的滚动窗口的持续时间,单位为毫秒。
@HystrixProperty(name = "metrics.rollingPercentile . timeInMilliseconds", value = "60000"),
//该属性用来没置百分位统计滚动窗口中使用“桶”的数量。
@HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
//该属性用来没置在执行过程中每个“桶” 中保留的最大执行次数。 如果在燎动时间窗内发生超过该没定值的执行次数,
//就从最初的位置开始重写。例如,将该值没置为100,旅动窗口为10秒, 若在10秒内一个“桶 ”中发生了500次执行,
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
//该属性朋来没置对命令执行的延迟是否使朋百分位数来跟踪和计算。如果没置为false,那么所有的概要统计都将返回-1。
@HystrixProperty(name = "metrics.rollingPercentile.enabled",value = "false"),
// 该属性用来没置百分位统计的燎动窗口的持续时间,单位为亳秒。
@HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
//该属性用来没置百分位统计滚动窗口中使用“桶”的数量。
@HystrixProperty(name = "metrics.rollingPercentle.numBuckets", value = "60000"),
//该属性用来没置在执行过程中每个 “桶” 中保留的最大执行次数。如果在燎动时间窗内发生超过该没定值的执行次数,
//就从最初的位置开始重写。例如,将该值没置为100, 欲动窗口为10秒,若在10秒内一个“桶”中发生了500次执行,
//那么该“桶”中只保留最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗, 并增加排序百分位数所需的计篇时间。
@HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
//该属性用来设置采集影响断路器状态的健康快照(请求的成功,错误百分比)的间隔等待时间。
@HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
//是否开启请求缓存
@HystrixProperty(name = "requestCache.enabled", value = "true" ),
// HystrixCommand的执行和事件是否打印日志到HystrixRequestLog中
@HystrixProperty(name = "requestLog. enabled", value = "true"),
},
threadPoolProperties = {
//该参数用来设置执行命令线程她的核心线程数,该值 也就是命令执行的最大并发量
@HystrixProperty(name = "coreSize", value = "10"),
//该参数用来没置线程池的最大队列大小。当设置为-1时,线程池将使用SynchronousQueue实现的
//否则将使用LinkedBlockingQueue实现的队列。
@HystrixProperty(name = "maxQueueSize", value = "-1"),
//该参数用来为队列设置拒绝阙值。通过该参数,即使队列没 有达到最大值也能拒绝请求。
//该参数主要是对LinkedBlockingQueue队列的补充,因为LinkedBlockingQueue
//队列不能动态修改它的对象大小,而通过该属性就可以澜整拒绝请求的队列大小了。
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
pub1ic String strConsumer() {
return "he1lo 2020";
}

9. HystrixDashBoard搭建

新建一个 cloud-consumer-hystrix-dashboard9001 监控服务监控 cloud-provider-hystrix-payment8001 服务。

9.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>live.yremp.springcloud</artifactId>
        <groupId>live.yremp</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-consumer-hystrix-dashboard9001</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- actuator监控信息完善 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

9.2 application.yml

server:
  port: 9001

9.3 主启动类

package live.yremp.springcloud;

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

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

9.4 修改被监控服务

修改 cloud-provider-hystrix-payment8001 服务 主启动类如下:

package live.yremp.springcloud;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
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.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

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

    /**
     *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
     *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
     *只要在自己的项目里配置上下面的servlet就可以了
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

9.5 访问监控Web界面

访问 http://localhost:9001/hystrix 即可看到监控界面

输入 http://localhost:8001/hystrix.stream 地址 Delay设置 2000 开始监控

我们去测试访问 cloud-provider-hystrix-payment8001 项目 ,先测试几次异常请求,再测试几次正常请求查看监控情况:

先进行几次异常请求 异常请求导致circuit开启

再测试几次正常请求,链路恢复,circuit关闭

标签云

ajax AOP Bootstrap cdn Chevereto CSS Docker Editormd GC Github Hexo IDEA JavaScript jsDeliver JS樱花特效 JVM Linux Live2D markdown Maven MyBatis MyBatis-plus MySQL Navicat Oracle Pictures QQ Sakura SEO Spring Boot Spring Cloud Spring Cloud Alibaba SpringMVC Thymeleaf Vue Web WebSocket Wechat Social WordPress Yoast SEO 代理 分页 图床 小幸运 通信原理

Spring Cloud Hystrix服务降级熔断限流
Spring Cloud Hystrix服务降级熔断限流