springcloud之hystrix隔离、熔断、降级

hystrix 简介

hystrixnetlifx 开源的一款容错框架,防雪崩利器,具备服务降级,服务熔断,依赖隔离,监控 hystrix dashboard 等功能,从而提升系统的可用性和容错性

官方 githubhttps://github.com/Netflix/Hystrix/wiki/How-To-Use

hystrix 容错机制的特点

要防止雪崩效应,必须有一个强大的容错机制。该容错机制需实现以下两点:

  • 包裹请求:使用 HystrixCommand 包裹对依赖的调用逻辑,每个命令在独立线程中执行
  • 断路器(跳闸)机制:当某服务的错误率超过一定阈值时,hystrix 可以自动或者手动跳闸,停止请求该服务一段时间
  • 资源隔离:hystrix 为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定
  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑
  • 自我修复:断路器打开一段时间后,会自动进入半开状态

hystrix 的断路器工作原理

在这里插入图片描述

  • 当请求达到阈值时候:默认 10 秒内 20 次请求,当请求失败率达到 50% 时,此时断路器将会开启,所有的请求都不会执行
  • 断路器开启 5 秒(默认)时,这时断路器是半开状态, 会允许其中一个请求执行
  • 如果执行成功,则断路器会关闭;如果失败,则继续开启。循环重复这两个流程

hystrix 实现服务的隔离、熔断、降级

继续使用 上一篇 文章中的项目,稍加改造即可实现 hystrix 功能

Maven 依赖

eureka-client-consumer 消费方项目添加如下依赖

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

application.properties 配置文件

server.port=8090

#注册进eureka的名称
spring.application.name=eureka-client-consumer

eureka.client.service-url.defaultZone=http://eureka7001:8761/eureka/
eureka.instance.prefer-ip-address=true

#Feign默认整合了Hystrix,要想为Feign打开Hystrix支持,需要此项设置
#在springcloud Dalston之前的版本中,Feign默认开启Hystrix支持,无需设置feign.hystrix.enabled=true
#从springcloud Dalston版本开始,Feign的Hystrix支持默认关闭,需要手动设置开启
feign.hystrix.enabled=true

#配置ribbon的超时时间,默认二者都是1000
#ribbon.ConnectTimeout=2000
#ribbon.ReadTimeout=2000

#第一次启动项目时,请求接口查询数据库有点耗时,会进入降级策略,所以将hystrix的超时时间设置为3s,默认是1s,这是全局设置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

服务消费方主启动类

服务提供方的保持不变,消费方主启动类一定要标注注解 @EnableHystrix

@EnableHystrix
@EnableFeignClients
@EnableEurekaClient
@Slf4j
@SpringBootApplication
public class AppConsumer {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(AppConsumer.class, args);
        log.info("------AppConsumer Running------");
    }
}

服务降级

feign 整合 hystrix 的情况下,服务降级的实现主要有以下 3 种方式。主要变动的代码都在消费方。当服务调用失败(程序异常,线程池 / 信号量已满,服务下线)或超时了,再或者服务熔断触发了服务降级,这些情况就让其触发服务降级,从而保证了系统的可用性可靠性,防止了系统的整体缓慢甚至崩溃

方式一

  • 使用 feign 的注解 @FeignClient 的属性 fallback 指定的降级回退方法
  • 如果业务层抛出异常,这种方式对异常处理不友好,会直接将异常抛出给用户,除非主动处理这个异常
@FeignClient(name = "eureka-client-producer", fallback = UserConsumerFeignFallback.class)
public interface UserConsumerFeign {
    
    

    @GetMapping(path = "/user/selectUserById")
    ResultVo selectUserById(@RequestParam(name = "id") Integer id);
}
@Component
public class UserConsumerFeignFallback implements UserConsumerFeign {
    
    

    @Override
    public ResultVo selectUserById(Integer id) {
    
    
        ResultVo resultVo = new ResultVo();
        resultVo.setId(id);
        resultVo.setUsername("恭喜你已进入UserConsumerFeignFallback类所在的服务降级区域");
        resultVo.setNickname("恭喜你已进入UserConsumerFeignFallback类所在的服务降级区域");
        return resultVo;
    }
}

方式二

  • 使用 hystrix 提供的注解 @HystrixCommand 的属性 fallbackMethod 来指定降级回退方法。但这种方式业务逻辑与降级回退方法耦合在一起,不好进行统一管理
  • 如果业务层抛出异常,这种方式对异常处理比较友好,会直接走降级回退方法,并不会直接显式抛出异常来
@Slf4j
@Service
public class UserConsumerServiceImpl implements UserConsumerService {
    
    

    @Autowired
    private UserConsumerFeign userConsumerFeign;

    /**
     * feign 进行远程调用
     */
    @HystrixCommand(fallbackMethod = "queryOneByIdFallback")
    @Override
    public ResultVo queryOneById(Integer id) {
    
    
        ResultVo resultVo = userConsumerFeign.selectUserById(id);
        log.info("resultVo为:" + resultVo.toString());
        log.info("调用服务提供方的端口为:" + resultVo.getPort());
        return resultVo;
    }

    public ResultVo queryOneByIdFallback(Integer id) {
    
    
        ResultVo resultVo = new ResultVo();
        resultVo.setId(id);
        resultVo.setUsername("恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
        resultVo.setNickname("恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
        return resultVo;
    }
}

方式三

每一个方法独立配置降级的话会造成代码冗余,加大了工作量。此时可以使用注解 @DefaultPropertiesdefaultFallback 属性来指定类的全局降级回退方法

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallackMethod")
public class PaymentController {
    
    

    @Resource
    private PaymentHystrixService paymentService;

    @GetMapping("/consumer/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("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
    
    
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")  //3秒钟以内就是正常的业务逻辑
    })
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
    
    
        String result = paymentService.paymentInfo_TimeOut(id);
        return result;
    }

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

    /**
     * 全局fallback处理方法
     */
    public String payment_Global_FallackMethod(@PathVariable("id") Integer id) {
    
    
        return "我是消费者80,对付支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,(┬_┬)";
    }
}

服务降级测试

这里测试以服务下线(肯定调用失败)和服务调用超时两种情况来测试服务降级。服务降级的实现方式选择第一种或第二种都可以

服务下线

启动 eureka-server-one,eureka-client-consumer 两个项目,eureka-client-consumercontroller 层代码未改动。注意没有启动 eureka-client-producer 服务提供方项目,postman 测试如下

在这里插入图片描述在这里我选择以一种方式,所以结果如上

服务调用超时

服务提供方 controller 代码改动

改动 eureka-client-producer 服务提供方的 controller 层代码,如下

@Slf4j
@Controller
@RequestMapping(path = "/user")
public class UserController {
    
    

    @Autowired
    private UserService userService;

    @GetMapping(path = "/selectUserById")
    @ResponseBody
    public ResultVo selectUserById(Integer id) {
    
    
        try {
    
    
            // 产生3~10的随机数
            Random random = new Random();
            int i = random.nextInt(10) + 3;
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        return userService.selectOne(id);
    }
}

然后再分别启动 eureka-server-one,eureka-client-producer,eureka-client-consumer 这三个项目,postman 测试结果如下

在这里插入图片描述
可以看到服务调用超时的时候,进入服务降级了

如何判断调用超时默认超时时间

针对这个问题,就要查看 hystrix 源码了

在这里插入图片描述

  • 它默认是 1000 ms1 s,而在服务提供方却延时了 3~10 s,肯定超时
  • 另外,还可以查看 feign 的超时连接时间默认是 2 s,如下

在这里插入图片描述

hystrix 的超时时间配置

全局配置

如果只是想全局的设置,可以在配置文件中配置如下,默认是 1000 ms

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

使用 @HystrixCommand

如果使用的是 @HystrixCommand 注解,那么可以在注解中直接指定超时时间,如下

@HystrixCommand(fallbackMethod="fallback",
	commandProperties = {
    
    
	     @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000" )
	}
)
@Override
public String firstLoginTimeOut(Integer id) {
    
    
    return userFeign.getCouponTimeOut(id);
}

服务熔断

熔断与降级

  • 熔断的核心是断路器(跳闸),没有断路器的熔断那就不是熔断了
  • 熔断也会触发降级回退方法的

服务熔断测试

服务提供方 controller 代码改动

改动 eureka-client-producer 服务提供方的 controller 层代码,即恢复原样,如下

@Slf4j
@Controller
@RequestMapping(path = "/user")
public class UserController {
    
    

    @Autowired
    private UserService userService;

    @GetMapping(path = "/selectUserById")
    @ResponseBody
    public ResultVo selectUserById(Integer id) {
    
    
        return userService.selectOne(id);
    }
}

服务消费方 service 层代码改动

为了演示服务熔断效果,改动如下

@Slf4j
@Service
public class UserConsumerServiceImpl implements UserConsumerService {
    
    

    @Autowired
    private UserConsumerFeign userConsumerFeign;

    /**
     * 在 10s 统计窗口时间之内,有 10 次请求,请求失败率达到 50% 的时候,此时断路器将会开启,
     * 并在 5s 后断路器开始尝试恢复正常,如果没有错误,就完全恢复
     */
    @HystrixCommand(fallbackMethod = "queryOneByIdFallback", commandProperties = {
    
    
            @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),// 统计窗口时间,默认10s
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 断路器熔多断的最小请求数,默认20
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),// 断路器打开后,多久以后开始尝试恢复,默认5s
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")// 请求失败率,默认50%
    })
    @Override
    public ResultVo queryOneById(Integer id) {
    
    
        if (id < 0) {
    
    
            throw new RuntimeException("id 不能为负数");
        }
        ResultVo resultVo = userConsumerFeign.selectUserById(id);
        log.info("resultVo为:" + resultVo.toString());
        log.info("调用服务提供方的端口为:" + resultVo.getPort());
        return resultVo;
    }

    public ResultVo queryOneByIdFallback(Integer id) {
    
    
        ResultVo resultVo = new ResultVo();
        resultVo.setId(id);
        resultVo.setUsername("id 不能为负数,恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
        resultVo.setNickname("id 不能为负数,恭喜你已进入UserConsumerServiceImpl类所在的服务降级区域");
        return resultVo;
    }
}

测试一

测试在 10s 窗口时间之内,我们发送 10 次请求,请求失败率达到 50% 时,让断路器打开,postman 测试如下

在这里插入图片描述
传入参数 -1,此时断路器已经打开。此时在 5s 时间之内,再传入参数 1,发送请求(5s 后断路器开始尝试恢复正常)。看其效果,如下

在这里插入图片描述
可以看到,即使发送请求传入的参数是 1,结果依旧是熔断状态,因为断路器打开后,在设置的时间内(我们设置 5s),所有的请求都不会执行,直到过了这个设置的时间内之后,这时断路器是半开状态的。我们再次发送请求传入的参数是 1,结果如下

在这里插入图片描述
此时发送请求传入的参数是 1,结果正常,此时的断路器就会关闭了

服务隔离

hystrix 进行隔离,有线程池隔离和信号量两种方式

信号量隔离

信号量隔离,就是一个计数器,显示服务的请求数量,起到了限流的作用

线程池隔离

  • hystrix 的线程池隔离:接口的请求在来到 hystrix 之前还要经过 controller、service 等,真正被 hystrix 接收到后,hystrix 才会创建线程池,把请求放到新的线程中,请求下游的服务
  • 这个过程中,hystrix 就可以控制等待超时、失败请求统计等操作。所以这个线程池的大小就决定了对下游服务的并发请求量,实际上也是在这里起到了对下游服务的一个保护。重点就是对下游服务的保护
  • 对自己所在的服务是否有保护呢,如果我们的 hystrix 超时时间(execution.isolation.thread.timeoutInMilliseconds)设置的非常长,那么当下游服务响应慢或无响应时,hystrix 所在的服务也会长时间挂起,这样 tomcat 的线程池也就很快会耗尽,失去保护作用。所以我们的 hystrix 超时时间,失败统计次数,失败比例等参数设置的合理才能起到对自己的保护作用,也就是对下游响应慢或无响应的服务,能够快速熔断,进行降级,返回降级结果,释放宝贵的 tomcat 线程资源

线程池隔离配置示例

@Service
public class UserServiceImpl implements UserService {
    
    

    private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);

    @Autowired
    private UserFeign userFeign;

    /**
	 * threadPoolKey:1:如果设置的groupKey值已经存在,他会使用这个groupKey值。
	 *       			这种情况下:同一个groupKey下的依赖调用共用同一个线程池
     * 				  2:如果groupKey值不存在,则会对于这个groupKey新建一个线程池
     *
     * 1:threadPoolKey的默认值是groupKey,而groupKey默认值是@HystrixCommand标注的方法所在类名
     * 2:可以通过在类上加@DefaultProperties(threadPoolKey="xxx")设置默认的threadPoolKey
     * 3;可以通过@HystrixCommand(threadPoolKey="xxx")指定当前HystrixCommand实例的threadPoolKey
     * 4:threadPoolKey用于从线程池缓存中获取线程池和初始化创建线程池,由于默认以groupKey即类名为threadPoolKey,那么默认所有在一个类中的HystrixCommand共用一个线程池
     * 5:动态配置线程池 --
     * 	可以通过hystrix.command.HystrixCommandKey.threadPoolKeyOverride=线程池key动态设置
     * 	 threadPoolKey,对应的HystrixCommand所使用的线程池也会重新创建,还可以继续通过
     *   hystrix.threadpool.HystrixThreadPoolKey.coreSize=n和hystrix.threadpool.HystrixThreadPoolKey.maximumSize=n动态设置线程池大小
     * 6:commandKey的默认值是@HystrixCommand标注的方法名,即每个方法会被当做一个HystrixCommand
     */
    @HystrixCommand(threadPoolKey = "time", threadPoolProperties = {
    
    
            @HystrixProperty(name = "coreSize", value = "2"),
            @HystrixProperty(name = "maxQueueSize", value = "20")}, commandProperties = {
    
    
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "9000")})
    @Override
    public String timeOut(Integer mills) {
    
    
        log.info("-----------mills:的值为:" + mills + "--------------");
        return userFeign.timeOut(mills);
    }

    /**
     * 一个类中的HystrixCommand使用两个线程池,进行线程隔离【和上面的方法隔离】
     */
    @HystrixCommand(threadPoolKey = "time_1", threadPoolProperties = {
    
    
    		@HystrixProperty(name = "coreSize", value = "2"),
            @HystrixProperty(name = "maxQueueSize", value = "20")}, commandProperties = {
    
    
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "9000")})
    @Override
    public String timeOut_1(Integer mills) {
    
    
        log.info("-----------mills:的值为:" + mills + "--------------");
        return userFeign.timeOut(mills);
    }
}

学习文档

猜你喜欢

转载自blog.csdn.net/weixin_38192427/article/details/121184221