雪崩效应
复杂的分布式体系结构中的应用程序很多都有依赖调用关系, 每个依赖关系有些时候会出现不可避免的失败(异常,超时,网络故障等).
这种多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应
。服务雪崩效应
是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。
如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。
服务熔断
针对雪崩效应
, Netflix公司提供了解决方案: 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)不一致, 导致访问报错. 下图所示:
服务熔断测试
场景一: 关闭服务提供方
将服务提供方
关闭后,访问服务调用api, 应用并没有一直等待服务提供者
的响应, 而是执行了我们声明的服务熔断方法
, 给客户合适的反馈, 实际应用场景中应该返回提示界面
给客户.
现在将服务提供方
应用启动, 再次访问上面的api, 调用成功.
场景二: 服务提供方
正常, 访问不存在的dept信息
如果访问系统库表中不存在的dept信息时, 也就是服务提供方
没有查询到信息, 返回null
, 然后在服务调用方
通过非空判断抛出异常, 是否也会走到服务熔断方法
中去呢? 下面我们看测试将结果. 也一样的进行了熔断处理.存在的问题
@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信息.
发现没有发生服务降级, 而是返回空处理.
场景二: 服务提供方
正常, 在服务调用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;
}
结果也没有发生服务降级, 而是返回系统默认的异常信息界面.
场景三: 服务提供方
正常, 访问存在的dept信息.
正常返回.
场景四: 关闭 服务提供方
, 随意访问.
发生了服务降级. 也就是整体资源不够的时候, 将某些服务先关闭, 待资源ok的时候,再开启回来.
注意, 我们需要弄清楚服务熔断
和服务降级
的区别.
服务熔断
一般是某个服务故障或者异常引起, 类似现实生活中的保险丝
, 当某个异常条件被触发, 直接熔断整个服务, 而不是一直等到此服
务响应超时.服务降级
所谓服务降级, 一般是从整体负荷
考虑, 就是当某个服务熔断之后, 服务器将不再被调用. 此时客户端可以自己准备一个本地的
fallback回调, 返回一个缺省值.
这样做, 虽然服务水平下降了, 但是不会造成雪崩效应
, 拉跨整个调用链, 比直接瘫痪好很多.
个人博客
欢迎访问个人博客: https://www.crystalblog.xyz/
备用地址: https://wang-qz.gitee.io/crystal-blog/