限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。
而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
线程隔离之前讲到过:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。
熔断降级:是在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高,则熔断该业务,不允许访问该服务的提供者了。
可以看到,不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。
而我们的微服务远程调用都是基于 Feign 来完成的,因此我们需要将 Feign 与 Sentinel 整合,在 Feign 里面实现线程隔离和服务熔断。
Feign Client 整合 Sentinel
Spring Cloud 中,微服务调用都是通过 Feign 来实现的,因此做客户端保护必须整合 Feign 和 Sentinel。
修改 Feign 配置开启 Sentinel 功能
修改 OrderService 的 application.yml 文件,开启 Feign 的 Sentinel 功能:
feign:sentinel: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 默认采用)

线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
- 优点:支持主动超时,支持异步调用
- 缺点:线程的额外开销比较大
- 场景:低扇出(Fanout)
信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
- 优点:轻量级,无额外开销
- 缺点:不支持主动超时,不支持异步调用
-
Sentinel 的线程隔离
在添加限流规则时,可以选择两种阈值类型:

QPS:就是每秒的请求数,在快速入门中已经演示过
- 线程数:是该资源能使用用的 tomcat 线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。
案例需求:给 order-service 服务中的 UserClient 的查询用户接口设置流控规则,线程数不能超过 2。然后利用 JMeter 测试。
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到 open 状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。open 状态 5 秒后会进入 half-open 状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到 closed 状态
- 请求失败:则切换到 open 状态
慢调用比例
慢调用比例:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。例如:
解读: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 接口设置降级规则:
超过 50ms 的请求都会被认为是慢请求
异常比例、异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
例如,一个异常比例设置:
解读:统计最近 1000ms 内的请求,如果请求量超过 10 次,并且异常比例不低于 0.4,则触发熔断。
一个异常数设置:
解读:统计最近 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 接口设置降级规则:
在 5 次请求中,只要异常比例超过 0.4,也就是有 2 次以上的异常,就会触发熔断。
