Spring Cloud笔记-Hystrix断路器(十)

1.概述

1.分布式系统面临的问题

在复杂的分布式架构系统里,服务之间的相互调用无法避免出现失败的情况。如果一个服务的调用链路比较长的时候,这种调用失败的概率会更高一些,比如A调用B,B调用C,C调用D这种长链路调用,又或者一个服务的调用需要依赖多个服务协作完成,这种调用失败的概率也会高一些,比如A需要调用B,C,D服务,只有B,C,D服务都走完,A服务才能完成。

扇出:当前模块直接调用下级模块的个数。

如果扇出的链路上某个服务调用响应时间过长或不可用,对当前模块的调用就会占用越来越多的资源,甚至造成系统崩溃。这就是所谓的系统雪崩效应。

一个高流量的应用,单一的后端依赖可能导致所有服务器上的所有资源在很快时间内饱和,更糟糕的是,应用程序还可能导致服务之间的延迟增加,队列,线程等资源紧张,进而导致整个系统发生更多的故障。

为了避免出现这样的情况,就需要对故障和延迟进行隔离,避免因为某一个服务的问题,拖累其他服务的正常运行。

当一个模块出现问题后,如果这个模块还在接受请求,而且还调用了其他模块,这样就会导致级联故障,导致系统雪崩效应。

2.Hystrix是什么

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

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

3.Hystrix能做什么

服务降级、服务熔断、接近实时的监控。

4.Hystrix官网

https://github.com/Netflix/Hystrix/wiki/How-To-Use

https://github.com/Netflix/Hystrix的Hystrix Status的信息可知,Hystrix目前不再进行开发了,从github的更新来看,已经好久没有更新了,Hystrix推荐使用Resilience4j来替代,不过,国内使用Resilience4j的大厂比较少,更多的大厂使用的是Spring Cloud Alibaba Sentinel来实现熔断与限流,又或者在现有的开源框架上进行包装,自己实现功能。

虽然Hystrix不再更新,但是Hystrix的设计理念值得学习,也为后面学习Spring Cloud Alibaba Sentinel做好铺垫。

2.Hystrix重要概念

1.服务降级(fallback)

当程序出现异常、超时、服务熔断、线程池、信号量已满的情况,会发生服务降级,此时服务会返回一个友好的提示,请稍后再试,不让客户端一直等待并立刻返回一个有好的提示。

2.服务熔断(break)

当服务的请求量达到一定限度的时候,可以发生服务熔断,类似于家里的保险丝在大电流的时候,会触发熔断,此时服务直接不可访问,后续可以走服务降级的方式立刻给客户端返回响应。

3.服务限流(flowlimit)

常用在某一时刻的高并发请求,比如秒杀阶段,为了不把服务打死,会将某些请求进行限流,比如放进队列,让其暂时等待。

3.Hystrix案例

1.构建项目

先把cloud-eureka-server7001和cloud-eureka-server7002改成单机版,后序启动会快一些,修改defaultZone的值为指向自己服务的地址,而不是互相注册的方式。

新建cloud-provider-hystrix-payment8001模块,修改pom.xml,这里加入spring-cloud-starter-netflix-hystrix依赖。

<?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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>cloud-provider-hystrix-payment8001</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <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.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</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>
</project>

添加application.yml配置文件。

server:
  port: 8001
spring:
  application:
    name: cloud-provider-hystrix-payment
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

创建主启动类。

package com.atguigu.springcloud;

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

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

创建业务类(Service和Controller)。

package com.atguigu.springcloud.service;

import org.springframework.stereotype.Service;

@Service
public class PaymentService {
    public String paymentInfo_OK(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + "\tpaymentInfo_OK, id=" + id;
    }

    public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
        int time = 3000;
        Thread.sleep(time);
        return "线程池:" + Thread.currentThread().getName() + "\tpaymentInfo_TimeOut, id=" + id + "\t耗时:" + time;
    }
}
package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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 PaymentController {
    @Resource
    private PaymentService paymentService;
    @Value("${server.port}")
    private String serverPort;

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

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
        String result = paymentService.paymentInfo_TimeOut(id);
        log.info("result={}", result);
        return result;
    }
}

先启动cloud-eureka-server7001模块,再启动cloud-provider-hystrix-payment8001模块,通过浏览器访问http://localhost:8001/payment/hystrix/ok/1http://localhost:8001/payment/hystrix/timeout/1查看效果,此时两个请求都可以正常访问,只是timeout那个请求慢一点。下面,我们就基于这两个请求进行扩展,演示Hystrix里的功能。

2.高并发测试

使用高并发测试工具Apache Jmeter进行测试。

在测试计划上右键→添加→线程(用户)→线程组,添加一个线程组,命名为Spring Cloud,线程数为200,Ramp-Up时间为1,循环次数为100,其余参数保持默认即可,保存线程组。在Spring Cloud线程组上右键→添加→取样器→HTTP请求,命名cloud-provider-hystrix-payment8001,服务器名称或IP为localhost,端口号为8001,HTTP请求为GET请求,路径为http://localhost:8001/payment/hystrix/timeout/1,点击保存,先试试这个超时的请求。点击菜单栏里的绿色箭头发起请求。此时,再通过浏览器访问http://localhost:8001/payment/hystrix/ok/1会发现,也被拖慢了。

新建cloud-consumer-feign-hystrix-order80模块,修改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>cloud2020</artifactId>
        <groupId>com.atguigu.springcloud</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.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <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-eureka-client</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>
</project>

添加application.yml配置文件。

server:
  port: 80
# 这里只把feign做客户端用,不注册进eureka
eureka:
  client:
    # 表示是否将自己注册进EurekaServer默认为true
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
spring:
  application:
    name: cloud-consumer-feign-hystrix-order

添加主启动类。

package com.atguigu.springcloud;

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

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

添加业务类(Service和Controller)。

package com.atguigu.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
    @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) throws InterruptedException;
}
package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.service.PaymentHystrixService;
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
public class OrderHystrixController {
    @Resource
    PaymentHystrixService paymentHystrixService;

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

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
        return paymentHystrixService.paymentInfo_TimeOut(id);
    }
}

先后启动cloud-eureka-server7001,cloud-provider-hystrix-payment8001,cloud-consumer-feign-hystrix-order80模块。

浏览器访问http://localhost:8001/payment/hystrix/ok/1http://localhost:8001/payment/hystrix/timeout/1http://localhost/consumer/payment/hystrix/ok/1都可以正常访问,当访问http://localhost/consumer/payment/hystrix/timeout/1提示超时了,这是因为Feign的原因,Feign默认连接超时为1秒,默认读取资源超时是1秒,这个暂且先不管,当然,也可以根据前一篇OpenFeign的笔记,修改超时时间。

这时候启动Apache JMeter,向http://localhost:8001/payment/hystrix/timeout/1请求发送高并发访问,通过浏览器访问http://localhost/consumer/payment/hystrix/ok/1,会发现直接超时了,此时模拟的情况就是:大量的请求打到生产者上,此时,通过Feign再来一个外部请求,就会出现超时,在不跑压测的时候,这个请求http://localhost/consumer/payment/hystrix/ok/1的响应时间还是很快速的。

原因:8001服务接口被大量请求占用,处理速度跟不上,Tomcat的工作线程已经占满了,后来到的线程只能等待,这就导致浏览器访问本该立刻返回的请求http://localhost/consumer/payment/hystrix/ok/1出现了超时的现象。

真是因为有这种故障或者不佳的情况出现,我们才需要降级、容错、限流等技术来处理这个问题。

  • 生产者8001超时了,消费者80不能一直等待,必须有服务降级
  • 生产者8001宕机了,消费者80不能一直等待,必须有服务降级
  • 生产者8001正常,消费者80自身故障或要求等待时间小于生产者处理时间,消费者80自己处理服务降级

3.服务降级

先从服务生产者端改造,找到paymentInfo_TimeOut()方法,因为这个方法的执行之间有点长,我们给它加一个标准,当方法执行不满足标准要求时,就让目标方法快速失败,继而去执行服务降级的方法,不再继续等待目标方法的返回。

package com.atguigu.springcloud.service;

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() + "\tpaymentInfo_OK, id=" + id;
    }

    // @HystrixCommand注解表示,当目标方法不满足commandProperties指定的参数时,终止当前方法,继而执行fallbackMethod指定的方法
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
        int time = 5000;
        // int a = 1 / 0;// 当程序报错,也会触发服务降级方法
        Thread.sleep(time);
        return "线程池:" + Thread.currentThread().getName() + "\tpaymentInfo_TimeOut, id=" + id + "\t耗时:" + time;
    }

    // 服务降级的方法
    // 需要注意的是,服务降级方法要和目标方法的方法签名保持一致,即参数和返回值要一致,否则会提示找不到服务降级方法
    public String paymentInfo_TimeOutHandler(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + "\tpaymentInfo_TimeOutHandler, id=" + id + "\t运行了服务降级方法";
    }
}

在主启动类上添加@EnableCircuitBreaker注解开启断路器功能,启动EurekaMain7001和PaymentHystrixMain8001模块,浏览器访问http://localhost:8001/payment/hystrix/timeout/1进行测试,方法体业务耗时5秒钟,调用方最多就能等3秒钟,当时间超过3秒,还没有返回,直接执行了paymentInfo_TimeOutHandler()方法,在浏览器端,我们可以看到具体输出。

上面,我们把服务生产者的服务降级做了处理,下面我们对服务消费者做处理,需要注意的是:服务降级既可以放在生产者,也可以放在消费者,更多的情况是放在消费者端。

在主启动类上添加@EnableHystrix注解。业务类,仿照服务生产者端的写法,做同样的修改。

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.service.PaymentHystrixService;
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.RestController;

import javax.annotation.Resource;

@RestController
public class OrderHystrixController {
    @Resource
    PaymentHystrixService paymentHystrixService;

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

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    // @HystrixCommand注解表示,当目标方法不满足commandProperties指定的参数时,终止当前方法,继而执行fallbackMethod指定的方法
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMehtod", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")
    })
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
        // int a = 1 / 0; // 当程序报错,也会触发服务降级方法
        return paymentHystrixService.paymentInfo_TimeOut(id);
    }

    // 服务降级的方法
    // 需要注意的是,服务降级方法要和目标方法的方法签名保持一致,即参数和返回值要一致,否则会提示找不到服务降级方法
    public String paymentTimeOutFallbackMehtod(@PathVariable("id") Integer id) {
        return "消费者调用生产者繁忙,请稍等后再试";
    }
}

此时,服务生产者端,timeout()方法服务运行时间是5s,加了服务降级策略后,timeout()方法如果在3s内不能执行完,就执行服务降级方法,服务消费者端的要求更苛刻,消费者最多等2.5s,在2.5s不能返回,就执行降级方法。通过浏览器访问http://localhost/consumer/payment/hystrix/timeout/1,在Network里观察,发现2秒多点就触发了服务降级方法,这是因为,我们没有设置Ribbon的默认超时时间(建立连接超时1s,读取资源超时1s)是2s,小于HystrixProperty设置的2.5秒,先触发了Ribbon的超时,代码报错,然后触发了降级方法。我们修改application.yml配置文件,修改Ribbon超时时间。

# 设置feign客户端超时时间,OpenFeign默认支持Ribbon
ribbon:
  ReadTimeout: 5000 # 建立连接后,读取资源所用时间
  ConnectTimeout: 5000 # 建立连接所用时间

此时,再发送http://localhost/consumer/payment/hystrix/timeout/1请求,通过Network查看时间,发现返回时间是2.5秒多点,此时是HystrixProperty设置的2.5秒起作用了。并且,浏览器输出的信息是客户端中降级方法的输出。

再来演示一个情况,将客户端HystrixProperty的时间改成3.5s,一般也不会这么设置,因为客户端的3.5s都大于服务端的3s了,这里仅仅做一个测试使用,通过了浏览器访问http://localhost/consumer/payment/hystrix/timeout/1,观察Network的时间,3s就返回了,而且浏览器展现的内容,是服务端降级方法的输出。

这个地方的超时时间有点乱,我感觉可以这么理解,看这几个超时时间,以超时时间最小的为标准,只要达到最小的超时时间,就会引起服务降级,服务降级的引起是由最小的超时时间决定的。

服务降级加在生产者的时候,直接调用服务生产者,是生产者Controller中HystrixProperty的3s触发降级。

服务降级加在消费者的时候,直接调用服务消费者:

  • 如果没有修改过Ribbon的默认超时,并且消费者调用超过默认的2s,此时,触发服务降级的是min(Ribbon的2s,消费者HystrixProperty指定的时间),谁的值小,就是由谁引发的服务降级。
  • 如果修改过Ribbon的默认超时,这里假设修改的值非常大,排除Ribbon调用超时引起的服务降级问题,那么,服务降级的触发条件是消费者HystrixProperty指定的时间,如果此时服务生产者也在HystrixProperty中指定了时间,那么以min(消费者HystrixProperty指定的时间,生产者HystrixProperty指定的时间)作为降级时间,哪个值小,就是由哪个触发的降级,从而走哪个降级方法。

回头看一下配置过的服务降级方法,如果有10个方法需要服务降级,那么就需要额外配置10个服务降级方法,有没有办法用一个通用的方法处理服务降级,其余的再做自定义配置呢?服务降级方法和业务方法放在一起,导致业务类里混乱不堪,是否可以将降级方法拿出来呢?

添加一个全局的服务降级方法,在class上添加@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod"),表明这个类中的所有带@HystrixCommand的方法,如果方法上没有特殊指明,那么,方法发生降级的时候,就走这个paymentGlobalFallbackMethod()方法。

package com.atguigu.springcloud.controller;

import com.atguigu.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 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
// 指定全局降级方法,对带了@HystrixCommand注解的方法起作用
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {
    @Resource
    PaymentHystrixService paymentHystrixService;

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

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    // @HystrixCommand注解表示,当目标方法不满足commandProperties指定的参数时,终止当前方法,继而执行fallbackMethod指定的方法
    // @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMehtod", commandProperties = {
    //         @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3500")
    // })
    // 带了@HystrixCommand注解表示,走Hystrix的服务降级策略,而且此时没有指明自定义的callback,就走默认的callback
    @HystrixCommand(commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")})
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
        // int a = 1 / 0; // 当程序报错,也会触发服务降级方法
        return paymentHystrixService.paymentInfo_TimeOut(id);
    }

    // 服务降级的方法
    // 需要注意的是,服务降级方法要和目标方法的方法签名保持一致,即参数和返回值要一致,否则会提示找不到服务降级方法
    public String paymentTimeOutFallbackMehtod(@PathVariable("id") Integer id) {
        return "消费者调用生产者繁忙,请稍等后再试";
    }

    // 全局服务降级方法,@HystrixCommand中,如果没有指定fallbackMethod的,就走全局服务降级方法
    public String paymentGlobalFallbackMethod() {
        return "全局服务降级方法,请稍后再试";
    }
}

浏览器访问http://localhost/consumer/payment/hystrix/timeout/1,依旧可以按照我们指定的超时规则进行服务降级,服务降级时,执行的方法是paymentGlobalFallbackMethod()。

为了实现解耦,可以借助@FeignClient注解中的fallback参数。新建一个PaymentFallbackService.java的类,实现PaymentHystrixService接口,重写接口里的方法,这里重写的两个方法,就是用于服务降级执行的方法,最后,将PaymentFallbackService类名,写到@FeignClient注解中的fallback参数上即可完成业务方法与降级方法解耦的效果。

package com.atguigu.springcloud.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
    @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) throws InterruptedException;
}
package com.atguigu.springcloud.service;

import org.springframework.stereotype.Component;

/**
 * 当PaymentHystrixService里的调用出错了,会执行这里对应的方法,从而把业务方法和降级方法解耦
 */
@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "paymentInfo_OK()方法对应的降级方法";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
        return "paymentInfo_TimeOut()方法对应的降级方法";
    }
}

修改application.yml,开启Hystrix,默认是false的,这个地方和前面的超时配置好像有点冲突,如果开启了Hystrix,超时哪块不起作用,不知道什么原因。

feign:
  hystrix:
    enabled: true

先通过浏览器访问http://localhost/consumer/payment/hystrix/ok/1,可以通过服务生产者,拿到生产者返回的值,此时,将生产者关掉,模拟生产者宕机的情况,再次访问http://localhost/consumer/payment/hystrix/ok/1,会看到它执行了PaymentFallbackService里的paymentInfo_OK()方法,现在服务降级方法和业务方法就分离开了。

4.服务熔断

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

在Spring Cloud框架里,熔断机制通过Hystrix实现,Hystrix会监控微服务之间的调用情况,当失败的调用达到一定阈值,会触发熔断机制(默认是5秒内20次失败),熔断机制的注解是@HystrixCommand。

服务熔断相关内容扩展:https://martinfowler.com/bliki/CircuitBreaker.html

在cloud-provider-hystrix-payment8001的PaymentService里添加方法来演示服务熔断。

// 服务熔断,这里配置的HystrixProperty可以在https://github.com/Netflix/Hystrix/wiki/Configuration查看,也可以查看HystrixCommandProperties类
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
        @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
        // 一个rolling window内请求的次数,当请求次数达到10,才计算失败率,从而判断是否满足断路的条件
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
        // 触发断路后的10s都会直接失败,超过10s后,尝试一次恢复
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
        // 失败率达到60%的时候,进行断路
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    if (id < 0) {
        throw new RuntimeException("id不能为负数");
    }
    String uuid = UUID.randomUUID().toString();
    return Thread.currentThread().getName() + "\t调用成功,UUID=" + uuid;
}
// 服务降级方法
public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
    return "id不能为负数,请稍后再试,id=" + id;
}

有了Service,也要有Controller,才能实现调用,在cloud-provider-hystrix-payment8001的PaymentController中添加方法。

@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    String result = paymentService.paymentCircuitBreaker(id);
    log.info("result={}", result);
    return result;
}

启动EurekaMain7001和PaymentHystrixMain8001模块。浏览器发送http://localhost:8001/payment/circuit/1请求,可以拿到正确的返回值,尝试发送大量的http://localhost:8001/payment/circuit/-1请求,再次发送一个http://localhost:8001/payment/circuit/1请求,对于这个正常的请求,依旧走了服务降级的方法,说明此时断路器是断开的,还没有恢复,再次发送几个正确的请求后,熔断器恢复,可以处理正常请求。

当发生熔断的时候,会记录下熔断的时间,根据这里的配置,在后续的10s内,请求都会走服务降级的方法,10s过后,尝试进行服务调用,如果能调通,恢复调用链路,如果不能调通,继续保持熔断,并重新记录时间。

熔断分3个类型:熔断打开、熔断半开、熔断关闭。

  • 熔断打开:Hystrix发生熔断,新的请求不在进行服务调用,而是直接运行服务降级方法,记录下发生熔断的时间,当熔断持续时间到达设置的sleepWindowInMilliseconds时,进入熔断半开
  • 熔断半开:部分请求进行服务调用,如果调用成功,熔断关闭,如果调用失败,继续保持熔断打开状态,重新记录时间
  • 熔断关闭:正常进行服务调用

5.服务限流

在后续的Alibaba的Sentinel中说明。

4.Hystrix工作流程

来到Hystrix的页面,点击How it Works

这里插播一个小问题,如果发现GitHub上的图片不显示了,那么可以尝试这个方法能不能解决。

访问https://www.ipaddress.com/,将显示图片的域名帖进去,这里我的是raw.Githubusercontent.com,会得到一个IP地址。把这个IP地址和域名写到本地hosts里,再次刷新页面,就可以显示图片了,原因好像是DNS污染或DNS存在缓存。

如果还是不能访问的小伙伴,就先看这个图吧,我贴上来。整个流程一共是9个步骤:

  1. 创建HystrixCommand或HystrixObservableCommand对象
  2. 执行Command命令
  3. 检查是否有可用缓存,如果有,直接返回cache的内容,如果没有,走第4步
  4. 检查熔断器的开闭,如果开启,运行服务降级方法,走第8步,如果没有开启,走第5步
  5. 检查线程池、队列、信号量是否充足,如果不充足,运行服务降级方法,走第8步,如果充足,走第6步
  6. 执行HystrixObservableCommand的construct()方法或者HystrixCommand的run()方法
  7. 根据执行的结果,判断是否执行成功,是否出现超时,并反馈给回路中的健康判断功能,从而影响断路器的开闭
  8. 熔断器断开、资源不足、执行失败、执行超时等情况出现,走服务降级方法获取返回值
  9. 一切正常,断路器为关闭状态,正常调用服务拿到返回值

5.服务监控Hystrix Dashboard

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒钟执行请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控 。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转换成可视化界面。

新建cloud-consumer-hystrix-dashboard9001模块,修改pom.xml,引入了spring-cloud-starter-netflix-hystrix-dashboard坐标。

<?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>cloud2020</artifactId>
        <groupId>com.atguigu.springcloud</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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</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>
</project>

添加application.yml配置文件,指明端口号。

server:
  port: 9001

添加主启动类,添加@EnableHystrixDashboard和@EnableCircuitBreaker注解,添加一个bean,用于指定监控路径,否则监控不到内容。

package com.atguigu.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);
    }

}

要想实现监控的功能,需要在pom.xml中有spring-boot-starter-actuator的坐标。启动HystrixDashboardMain9001模块,浏览器访问http://localhost:9001/hystrix可以看到界面。

为了演示监控的效果,即9001模块监控8001模块的运行情况,修改cloud-provider-hystrix-payment8001模块,检查8001模块的pom.xml里,是否有spring-boot-starter-actuator坐标,如果没有添加上,这个必须要有。

修改PaymentHystrixMain8001的主启动类,注入一个Bean,用于指定监控路径,否则会报Unable to connect Command Metric Stream的错误,注意修改的是8001的主启动类。

package com.atguigu.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.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

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

    /**
     * 此配置是为了服务监控而配置,与服务容错本身无关,Spring Cloud升级后的坑
     * 因为SpringBoot里ServletRegistrationBean默认路径不是 “/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;
    }
}

修改完成后,启动Eureka7001注册中心,Provider8001服务生产者,Dashboard9001服务监控,在http://localhost:9001/hystrix页面的输入框中,输入http://localhost:8001/hystrix.stream,即要监控的服务是哪一个,Delay默认2000即可,Title随便起一个名字。点击“Monitor Stream”开始监控,新开一个窗口,多次访问http://localhost:8001/payment/circuit/1,在Circuit标签下可以看到有一个折线和一个圆点,折线是记录最近2分钟内访问流量的相对变化,访问流量大的时候,折线上升,圆点变大,同时,这个圆点还会根据健康状态改变颜色,健康度从绿色→黄色→橙色→红色依次递减,还有一个Circuit,表示是否熔断,如果是Closed表示未熔断,如果是Open表示熔断了。

在监控页面的右上角,有7种颜色,从左到右分别对应:请求成功数量、请求熔断数量、错误请求数量,超时请求数量,线程池拒绝请求数量,请求失败数量,最近10秒的错误率。

 更多Dashboard的内容参考:https://github.com/Netflix-Skunkworks/hystrix-dashboard/wiki

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值