微服务-熔断器Hystrix

雪崩效应

复杂的分布式体系结构中的应用程序很多都有依赖调用关系, 每个依赖关系有些时候会出现不可避免的失败(异常,超时,网络故障等).
这种多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应
服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。
微服务-熔断器Hystrix

服务熔断

针对雪崩效应, Netflix公司提供了解决方案: Hystrix. 一个延迟和容错的开源库,用于隔离访问远程服务、第三方库,防止出现级联失败.

熔断原理

比如家里的电路熔断器, 如果电路发生短路能立刻熔断电路, 避免发生灾难. 在分布式系统中应用这种模式后, 服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时, 能够主动熔断, 防止整个系统被拖垮.

不同于电路熔断的是,Hystrix可以实现弹性容错, 当情况发生好转之后, 可以自动重连. 通过断路的方式, 可以将后续请求直接拒绝掉, 一段时间之后允许部分请求通过(半开), 如果调用成功则回到电路闭合状态, 否则继续断开(全开).
微服务-熔断器Hystrix

熔断器的使用

导入依赖

因为服务熔断发生在服务调用方, 所以需要在服务消费者应用添加Hystrix依赖.

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

启动类开启Hystrix配置

需要在服务消费者应用的启动类上添加@EnableHystrix注解开启熔断器配置.

@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端配置
@EnableDiscoveryClient // 开启服务发现
// @RibbonClient 在启动微服务时加载自定义的Ribbon配置类
// 这个自定义配置类不能放在@ComponentScan所扫描的当前包下及子包下面,否则自定义配置类会被所有的Ribbon客户端共享, 达不到定制化目的.
// 下面配置解释: 对microservicecloud-provider微服务使用自定义Ribbon策略MyRule
@RibbonClient(name = "microservicecloud-provider", configuration = {MyRuleConfig.class})
@EnableFeignClients // 开启Feign声明式调用的配置
@EnableHystrix // 开启Hystrix熔断器配置
public class ConsumerApplication {

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

}

低版本SpringCloud使用的是@EnableCircuitBreaker注解, 高版本SpringCloud使用的@EnableHystrix注解, @EnableHystrix注解包含了@EnableCircuitBreaker.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableCircuitBreaker // 兼容低版本的注解
public @interface EnableHystrix {
}

声明服务熔断方法

在controller层的服务调用api方法上添加@HystrixCommand注解, 使用其fallbackMethod属性声明服务熔断方法.
com.atguigu.springcloud.controller.ConsumerController#findById3

/**
 * 使用Eureka的服务发现api调用远程服务提供方(负载均衡Ribbon-简化版)
 * RestTemplate配置bean需要添加@LoadBalanced注解, RestTemplate才能自动负载均衡
 * Ribbon底层使用了{@link DiscoveryClient}服务发现组件
 * @param id
 * @return
 */
@GetMapping(value = "/findById3/{id}")
// 熔断器方法: 一旦调用服务提供方失败并抛出异常后,就会自动跳转执行fallbackMethod指定的方法
@HystrixCommand(fallbackMethod = "fallbackHandle")
public Dept findById3(@PathVariable(value = "id") Integer id) {
	Dept dept = restTemplate.getForObject("http://microservicecloud-provider" + "/dept/" + id, Dept.class);
	if (null == dept) throw new RuntimeException("查询失败");
	return dept;
}

编写服务熔断方法

在当前服务调用api的controller类中添加声明的服务熔断方法.
com.atguigu.springcloud.controller.ConsumerController#fallbackHandle

/**
 * Hystrix熔断器的熔断方法
 * 返回值类型要和上面保持一致, 否则访问会报错.
 * @return
 */
private Dept fallbackHandle(@PathVariable(value = "id") Integer id) {
	log.info(">>>没有查到id为{}的dept信息,发生了熔断..");
	Dept dept = Dept.builder()
			.deptno(id)
			.dname("没有查到id为" + id + "的dept信息,发生了熔断")
			.build();
	return dept;
}

注意: 服务熔断方法的返回值类型要和服务调用api方法的返回值类型保持一致, 否则访问会抛出异常.

比如下面这样:

private String fallbackHandle(@PathVariable(value = "id") Integer id) {
	log.info(">>>没有查到id为{}的dept信息,发生了熔断..");
	return "发生熔断了";
}

因为服务熔断方法的返回值类型(String)和服务调用api方法的返回值类型(Dept)不一致, 导致访问报错. 下图所示:
微服务-熔断器Hystrix
微服务-熔断器Hystrix

服务熔断测试

场景一: 关闭服务提供方
服务提供方关闭后,访问服务调用api, 应用并没有一直等待服务提供者的响应, 而是执行了我们声明的服务熔断方法, 给客户合适的反馈, 实际应用场景中应该返回提示界面给客户.
微服务-熔断器Hystrix
现在将服务提供方应用启动, 再次访问上面的api, 调用成功.
微服务-熔断器Hystrix
场景二: 服务提供方正常, 访问不存在的dept信息
如果访问系统库表中不存在的dept信息时, 也就是服务提供方没有查询到信息, 返回null, 然后在服务调用方通过非空判断抛出异常, 是否也会走到服务熔断方法中去呢? 下面我们看测试将结果. 也一样的进行了熔断处理.
微服务-熔断器Hystrix
存在的问题

@HystrixCommand(fallbackMethod = “fallbackHandle”)服务熔断处理方式存在调用逻辑和熔断逻辑高度耦合的缺陷,
而且当调用的api方法很多时, 每个方法都要配置一个服务熔断的fallback方法, 会造成方法臃肿.
如何解决呢? 下面的Feign提供了面向接口编程的解决方案: 服务降级

服务降级

导入依赖

Feign默认就有对Hystix的集成.

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

开启Feign对Hystrix的支持

默认情况下Hystix是关闭的。我们需要通过下面的配置参数来开启:

#OpenFeign内置有Hystrix, 需要开启Hystrix(熔断器)
feign:
  hystrix:
    enabled: true

启动类开启Feign配置

启动类上不要忘记了@EnableFeignClients注解, 开启Feign功能的配置.

@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端配置
@EnableDiscoveryClient // 开启服务发现
// @RibbonClient 在启动微服务时加载自定义的Ribbon配置类
// 这个自定义配置类不能放在@ComponentScan所扫描的当前包下及子包下面,否则自定义配置类会被所有的Ribbon客户端共享, 达不到定制化目的.
// 下面配置解释: 对microservicecloud-provider微服务使用自定义Ribbon策略MyRule
@RibbonClient(name = "microservicecloud-provider", configuration = {MyRuleConfig.class})
@EnableFeignClients // 开启Feign声明式调用的配置
@EnableHystrix // 开启Hystrix熔断器配置
public class ConsumerApplication {

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

}

编写FeignClient接口

面向接口编程, 类似mybatis中的mapper接口, 使用的时候底层会自动生成动态代理对象. 接口类上添加@FeignClient注解, 使用value属性指定要调用的服务提供方的服务ID, 使用fallbackFactory属性指定服务降级的处理类.

/**
 * 类描述:Feign声明式调用的接口
 *  fallback 或 fallbackFactory属性指定服务降级处理逻辑
 * @Author wang_qz
 * @Date 2021/10/27 20:44
 * @Version 1.0
 */
@FeignClient(value = "microservicecloud-provider", fallbackFactory = DeptFeignClientFallbackFactory.class)
public interface DeptFeignClientService {

    @PostMapping(value = "/dept/add")
    boolean add(Dept dept);

    @GetMapping(value = "/dept/{id}")
    Dept get(@PathVariable(value = "id") Integer id);

    @GetMapping(value = "/dept")
    List<Dept> findAll();
}

编写服务降级类

编写服务降级逻辑处理的类, 需要继承FallbackFactory<T> , 然后重写feign.hystrix.FallbackFactory.Default#create方法.

package com.atguigu.springcloud.service.impl;

import com.atguigu.springcloud.entities.Dept;
import com.atguigu.springcloud.service.DeptFeignClientService;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 类描述:fallbackFactory指定的服务降级类
 * @Author wang_qz
 * @Date 2021/10/30 21:18
 * @Version 1.0
 */
@Component
@Slf4j
public class DeptFeignClientFallbackFactory implements FallbackFactory<DeptFeignClientService> {

    @Override
    public DeptFeignClientService create(Throwable throwable) {

        return new DeptFeignClientService() {
            @Override
            public boolean add(Dept dept) {
                log.info("添加失败, 发生了服务降级...");
                return false;
            }

            @Override
            public Dept get(Integer id) {
                Dept dept = Dept.builder()
                        .deptno(id)
                        .dname("没有查询到id为" + id + "的dept信息, 发生了服务降级...")
                        .build();
                return dept;
            }

            @Override
            public List<Dept> findAll() {
                log.info("查询失败, 发生了服务降级...");
                return null;
            }
        };
    }
}

补充

@FeignClient还提供了属性fallback配置服务降级的逻辑处理类. 但服务降级类的实现方式稍微有所不同.

/**
 * 类描述:Feign声明式调用的接口
 *  fallback 或 fallbackFactory属性指定服务降级处理逻辑
 * @Author wang_qz
 * @Date 2021/10/27 20:44
 * @Version 1.0
 */
@FeignClient(value = "microservicecloud-provider", fallback = DeptFeignClientServiceImpl.class)
public interface DeptFeignClientService {

    @PostMapping(value = "/dept/add")
    boolean add(Dept dept);

    @GetMapping(value = "/dept/{id}")
    Dept get(@PathVariable(value = "id") Integer id);

    @GetMapping(value = "/dept")
    List<Dept> findAll();
}

fallback声明的服务降级类, 需要继承FeignClient接口类.

package com.atguigu.springcloud.service.impl;

import com.atguigu.springcloud.entities.Dept;
import com.atguigu.springcloud.service.DeptFeignClientService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * 类描述:fallback指定的服务降级类
 * @Author wang_qz
 * @Date 2021/10/31 11:34
 * @Version 1.0
 */
@Component
@Slf4j
public class DeptFeignClientServiceImpl implements DeptFeignClientService {
    @Override
    public boolean add(Dept dept) {
        log.info("添加失败, 发生了服务降级...");
        return false;
    }

    @Override
    public Dept get(Integer id) {
        Dept dept = Dept.builder()
                .deptno(id)
                .dname("没有查询到id为" + id + "的dept信息, 发生了服务降级...")
                .build();
        return dept;
    }

    @Override
    public List<Dept> findAll() {
        log.info("查询失败, 发生了服务降级...");
        return null;
    }
}

Feign的使用

在服务调用api中使用feign接口.

/**
 * Feign声明式调用
 */
@Autowired
private DeptFeignClientService feignClientService;
/**
 * 使用Eureka的服务发现api调用远程服务提供方(Feign声明式调用)
 * Feign内置了Ribbon负载均衡, 默认轮询策略
 * @param id
 * @return
 */
@GetMapping(value = "/findById4/{id}")
public Dept findById4(@PathVariable(value = "id") Integer id) {
	Dept dept = feignClientService.get(id);
	return dept;
}

服务降级测试

场景一: 服务提供方正常. 访问不存在dept信息.
发现没有发生服务降级, 而是返回空处理.
微服务-熔断器Hystrix
场景二: 服务提供方正常, 在服务调用api中进行非空判断, 抛出异常, 访问不存在dept信息.

/**
 * 使用Eureka的服务发现api调用远程服务提供方(Feign声明式调用)
 * Feign内置了Ribbon负载均衡, 默认轮询策略
 * @param id
 * @return
 */
@GetMapping(value = "/findById4/{id}")
public Dept findById4(@PathVariable(value = "id") Integer id) {
	Dept dept = feignClientService.get(id);
	// 非空判断, 抛出异常
	if (null == dept) throw new RuntimeException("查询失败");
	return dept;
}

结果也没有发生服务降级, 而是返回系统默认的异常信息界面.
微服务-熔断器Hystrix
微服务-熔断器Hystrix
场景三: 服务提供方正常, 访问存在的dept信息.
正常返回.
微服务-熔断器Hystrix
场景四: 关闭 服务提供方, 随意访问.
发生了服务降级. 也就是整体资源不够的时候, 将某些服务先关闭, 待资源ok的时候,再开启回来.
微服务-熔断器Hystrix
注意, 我们需要弄清楚服务熔断服务降级的区别.

服务熔断
一般是某个服务故障或者异常引起, 类似现实生活中的保险丝, 当某个异常条件被触发, 直接熔断整个服务, 而不是一直等到此服
务响应超时.
服务降级
所谓服务降级, 一般是从整体负荷考虑, 就是当某个服务熔断之后, 服务器将不再被调用. 此时客户端可以自己准备一个本地的
fallback回调, 返回一个缺省值.
这样做, 虽然服务水平下降了, 但是不会造成雪崩效应, 拉跨整个调用链, 比直接瘫痪好很多.

个人博客

欢迎访问个人博客: https://www.crystalblog.xyz/

备用地址: https://wang-qz.gitee.io/crystal-blog/

上一篇:微服务的远程调用


下一篇:五阶段--总结