限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。

而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。

线程隔离之前讲到过:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。
隔离和降级 - 图1
熔断降级:是在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高,则熔断该业务,不允许访问该服务的提供者了。
隔离和降级 - 图2
可以看到,不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。

而我们的微服务远程调用都是基于 Feign 来完成的,因此我们需要将 Feign 与 Sentinel 整合,在 Feign 里面实现线程隔离和服务熔断。

Feign Client 整合 Sentinel

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

修改 Feign 配置开启 Sentinel 功能

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

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

编写失败降级逻辑

业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。

给 Feign Client 编写失败后的降级逻辑,有两种实现方式:

  • FallbackClass,无法对远程调用的异常做处理
  • FallbackFactory,可以对远程调用的异常做处理(我们选择这种)

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

package cn.itcast.feign.clients.fallback;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            @Override
            public User findById(Long id) {
                log.error("查询用户异常", throwable);
                return new User();
            }
        };
    }
}

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

@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
    return new UserClientFallbackFactory();
}

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

@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

Feign Client 整合 Sentinel 总结

Sentinel支持的雪崩解决方案:

  • 线程隔离(仓壁模式)
  • 降级熔断

Feign 整合 Sentinel 的步骤:

  • 在application.yml中配置:feign.sentienl.enable=true
  • 给 FeignClient 编写 FallbackFactory 并注册为Bean
  • FallbackFactory 配置到 FeignClient

    线程隔离(舱壁模式)

    线程隔离的实现方式

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

  • 线程池隔离

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

隔离和降级 - 图3
线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果

  • 优点:支持主动超时,支持异步调用
  • 缺点:线程的额外开销比较大
  • 场景:低扇出(Fanout)

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

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

    Sentinel 的线程隔离

    在添加限流规则时,可以选择两种阈值类型:
    隔离和降级 - 图4

  • QPS:就是每秒的请求数,在快速入门中已经演示过

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

案例需求:给 order-service 服务中的 UserClient 的查询用户接口设置流控规则,线程数不能超过 2。然后利用 JMeter 测试。

熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。

断路器控制熔断和放行是通过状态机来完成的:
隔离和降级 - 图5
状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到 open 状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。open 状态 5 秒后会进入 half-open 状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到 closed 状态
    • 请求失败:则切换到 open 状态

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

慢调用比例

慢调用比例:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。例如:
隔离和降级 - 图6
解读:RT 超过 500ms 的调用是慢调用,统计最近 10000ms 内的请求,如果请求量超过 10 次,并且慢调用比例不低于 0.5,则触发熔断,熔断时长为 5 秒。然后进入 half-open 状态,放行一次请求做测试。

案例需求:给 UserClient 的查询用户接口设置降级规则,慢调用的 RT 阈值为 50ms ,统计时间为 1 秒,最小请求数量为 5,失败阈值比例为 0.4,熔断时长为 5

设置慢调用,修改 user-service 中的 /user/{id} 这个接口的业务。通过休眠模拟一个延迟时间:

@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
    if (id == 1) {
        // id 为 1 时触发慢调用
        Thread.sleep(60);
    }
    return userService.queryById(id);
}

设置熔断规则,下面,给 feign 接口设置降级规则:
隔离和降级 - 图7
超过 50ms 的请求都会被认为是慢请求

异常比例、异常数

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

例如,一个异常比例设置:
隔离和降级 - 图8
解读:统计最近 1000ms 内的请求,如果请求量超过 10 次,并且异常比例不低于 0.4,则触发熔断。

一个异常数设置:
隔离和降级 - 图9
解读:统计最近 1000ms 内的请求,如果请求量超过 10 次,并且异常比例不低于 2 次,则触发熔断。

案例需求:给 UserClient 的查询用户接口设置降级规则,统计时间为 1 秒,最小请求数量为 5,失败阈值比例为 0.4,熔断时长为 5s

设置异常请求,首先,修改 user-service 中的 /user/{id} 这个接口的业务。手动抛出异常,以触发异常比例的熔断:

@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
    if (id == 1) {
        // id 为 1 时触发慢调用
        Thread.sleep(100);
    } else if (id == 2) {
        throw new RuntimeException("故意出错,触发熔断");
    }
    return userService.queryById(id);
}

设置熔断规则,下面给 feign 接口设置降级规则:
隔离和降级 - 图10
在 5 次请求中,只要异常比例超过 0.4,也就是有 2 次以上的异常,就会触发熔断。