限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。
而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
关于线程隔离和熔断降级是什么,看这里雪崩问题

不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。
而我们的微服务远程调用都是基于Feign来完成的,因此我们需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。

FeignClient整合Sentinel

SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。

修改配置,开启sentinel功能

修改order-service的application.yml文件,开启Feign的Sentinel功能:

  1. feign:
  2. sentinel:
  3. enabled: true # 开启feign对sentinel的支持

编写失败降级逻辑

业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。
给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种

步骤一:在feing-api项目中定义类,实现FallbackFactory:

feign-api是抽出去的模块,里面包含了所有的feign发送请求的功能。抽取Feign远程调用

  1. package cn.itcast.feign.client.fallback;
  2. import cn.itcast.feign.client.UserClient;
  3. import cn.itcast.feign.pojo.User;
  4. import feign.hystrix.FallbackFactory;
  5. import lombok.extern.slf4j.Slf4j;
  6. @Slf4j
  7. public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
  8. @Override
  9. public UserClient create(Throwable throwable) {
  10. return new UserClient() {
  11. //如果远程调用findById接口发生了异常,就返回一个包含错误信息的user
  12. //这个方法重写是因为实现FallbackFactory接口时用了UserClient的泛型,重写的这里面的方法
  13. @Override
  14. public User findById(Long id) {
  15. log.error("查询用户异常", throwable);
  16. return new User(-1L,"错误","异常");
  17. }
  18. };
  19. }
  20. }

步骤二:在feing-api项目中的配置类中将UserClientFallbackFactory注册为一个Bean:

  1. package cn.itcast.feign.config;
  2. import cn.itcast.feign.client.fallback.UserClientFallbackFactory;
  3. import feign.Logger;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. public class DefaultFeignConfiguration {
  7. @Bean
  8. public Logger.Level feignLogLevel(){
  9. return Logger.Level.FULL; // 日志级别为BASIC
  10. }
  11. @Bean
  12. public UserClientFallbackFactory userClientFallbackFactory(){
  13. return new UserClientFallbackFactory();
  14. }
  15. }

步骤三:在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:

  1. package cn.itcast.feign.client;
  2. import cn.itcast.feign.client.fallback.UserClientFallbackFactory;
  3. import cn.itcast.feign.pojo.User;
  4. import org.springframework.cloud.openfeign.FeignClient;
  5. import org.springframework.web.bind.annotation.GetMapping;
  6. import org.springframework.web.bind.annotation.PathVariable;
  7. @FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class, fallbackFactory = UserClientFallbackFactory.class)
  8. public interface UserClient {
  9. @GetMapping("/user/{id}")
  10. User findById(@PathVariable("id") Long id);
  11. }

第三步的configuration = DefaultFeignConfiguration.class可以设置在启动类的注解@EnableFeignClients(clients = {UserClient.class},configuration = DefaultFeignConfiguration.class)里面实现全局配置 这个配置注解@EnableFeignClients在order-service模块里面的。Feign自定义配置配置类的方式

重启order-service后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路:
image.png

线程隔离(舱壁模式)

线程隔离有两种方式实现:

  • 线程池隔离
  • 信号量隔离(Sentinel默认采用)

如图:
image-20210716123036937.png
线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果

  • 优点:
    • 支持主动超时
    • 支持异步调用
  • 缺点
    • 线程的额外开销比较大
  • 场景
    • 低扇出,就是少调用其他服务,比如A服务调用多个个其他服务,概念上就像一个扇形,一个顶点,圆弧等,这个扇形越大,调用的服务越多,开销越大。

信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。

  • 优点:
    • 轻量级,无额外开销
  • 缺点
    • 不支持主动超时
    • 不支持异步调用
  • 场景

    • 高频调用
    • 高扇出

      sentinel的线程隔离

      在添加限流规则时,可以选择两种阈值类型:
  • QPS:就是每秒的请求数,流量控制>控流模式

  • 线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)

    测试

    给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2
    image.png
    用JMeter测试,我们设置0秒发20个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
    可以看到,虽然请求结果都成功了,但是用户这里却走了失败降级的逻辑
    image.png
    image.png

    熔断降级

    熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
    断路器控制熔断和放行是通过状态机来完成的:
    image-20210716130958518.png
    状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态

  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

断路器熔断策略有三种:慢调用、异常比例、异常数

1.慢调用

慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
image.png
如上图的意思是:
请求超过50ms的调用是慢调用,统计最近1000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.3(慢调用数量/请求总数),则触发熔断,熔断时长为10秒。然后进入half-open状态,放行一次请求做测试。

测试

按照上图的配置,对远程调用user的请求做降级配置。
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:

  1. @GetMapping("/{id}")
  2. public User queryById(@PathVariable("id") Long id) throws InterruptedException {
  3. //id为1时,触发慢调用
  4. if(id==1L){
  5. Thread.sleep(60);
  6. }
  7. return userService.queryById(id);
  8. }

此时,orderId=101的订单,关联的是id为1的用户,调用时间大于60ms。orderId=102的订单,关联的是id为2的用户,调用时间小于60ms。
image.pngimage.png
我们快速的访问orderId=101的订单,1秒内访问超过5次,就会触发熔断,熔断后任何订单调用user都会被阻碍直接失败。
image.png
orderId=101访问被熔断了,102同样访问不了,10秒后就能访问了。
image.png

2.异常比例,异常数

异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
image.png

测试

像上图这样设置降级规则,统计1秒内,超过5次请求,且报异常的请求数量超过2就熔断10秒
首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常数的熔断:

  1. @GetMapping("/{id}")
  2. public User queryById(@PathVariable("id") Long id) throws InterruptedException {
  3. //id为1时,触发慢调用
  4. if(id==1L){
  5. Thread.sleep(60);
  6. }else if(id==2L){
  7. throw new RuntimeException("故意抛出异常,出发异常数熔断");
  8. }
  9. return userService.queryById(id);
  10. }

我们去浏览器快速访问orderId为102的接口,直接异常,触发了熔断规则后,再去访问103也是异常
image.png
image.png