
文章目录
- 前言
- 雪崩问题
-
- 如何解决雪崩问题
-
- [1. 超时处理](#1. 超时处理)
- [2. 舱壁模式](#2. 舱壁模式)
- [3. 断路器](#3. 断路器)
- [4. 限流](#4. 限流)
- Sentinel
- 流量控制
- 流控效果
- 隔离和降级
- 授权规则
前言
这篇文章,我们将学习 SpringCloud Alibaba 中的 Sentinel 组件。

雪崩问题
在学习 Sentinel 之前,我们先来了解一下什么是雪崩问题。在微服务中,一个服务往往需要调用另一个服务,如果这个被调用的服务出现了问题的话,那么这个调用故障服务的服务也会被阻塞,这样看来的话,其他的微服务似乎并没有收到影响,但是我们想一下 tomcat 服务所持有的线程数是固定的,如果有许多的请求打到这个故障服务上面的话,那么就会有很多的线程被阻塞,时间久了就会导致 tomcat 中的所有线程都被阻塞,这时候就出现了雪崩问题。
那么既然知道了什么是雪崩问题,接下来我们要解决的问题就是如何解决雪崩问题。
如何解决雪崩问题
1. 超时处理
我们为每个服务的处理请求设置一个超时时间,如果某个服务处理一个请求超过了超时时间的话,那么就返回错误信息,释放掉占用的线程,避免占用资源。
2. 舱壁模式
既然你雪崩问题是因为某一个业务占用了所有的 tomcat 线程导致的,那么我是否可以限制你这个业务可以使用的线程,从而避免你一个业务吃掉所有的 tomcat 线程呢?这就是舱壁模式的思想:船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
我们可以限定每个业务可以使用的线程数,如果你这个业务出现了问题,最终只会导致你这一个业务无法使用,等技术人员排查之后就可以重新使用了,这样就避免了整个所有的业务都无法使用。
3. 断路器
断路器模式:由断路器 统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
当某个服务出现异常之后,断路器就会统计你这个服务出现异常的比例。
如果出现异常的比例超过阈值的话,那么我就会认为你这个服务可能会导致雪崩的出现,所以就会拦截所有访问这个异常服务的请求,形成熔断。

4. 限流
使用限流可以减少服务器的压力,通过限制业务访问的 QPS,避免服务器压力过大而出现故障,限流是预防雪崩的出现。
QPS全称Query Per Second,也可以理解为服务器每秒接收的请求。

可以实现服务保护的组件有很多,Netfix HystrixSentinel 和 Resilience4,但是我们这里使用的是 SpringCloud Alibaba 的 Sentinel。
Sentinel
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html
Sentinel 的优势我这里就不多说了,大家可以去官网看看,这里直接来使用 Sentinel。
安装Sentinel
我们可以直接在 maven 项目中的 pom.xml 文件中添加依赖:
xml
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
也可以直接下载 jar 包,因为 SpringCloud Alibaba 提供了 UI 控制台,更方便我们的操作,所以我这里下载了 sentinel-dashboard 的 jar 包。https://github.com/alibaba/Sentinel/releases/tag/1.8.6

下载完成之后,我们呢使用 java -jar 就可以启动服务了:

Sentinel-dashboard 默认启动端口号是 8080,如果我们想要修改端口号,只需要在启动命令上添加 -Dserver.port=
就可以了, 我们通过 localhost:8080 就可以访问控制台了。

默认用户名和密码都是sentinel,登录成功之后是这样的:

进入之后是空白的,这时因为我们没有整合服务,然后我们整合服务看一下是什么样的。
我们在要整合的服务的 pom.xml 文件中添加 sentinel 依赖:
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
然后配置 sentinel 相关信息:
yaml
server:
port: 8088
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
然后启动我们的服务,这里启动服务之后,我们刷新 sentinel-dashboard 发现还是空白,这是因为只有我们访问服务的服务才能触发 sentinel 的监控,所以我们访问一下服务:

然后刷新 sentinel 控制台发现有数据了:

当下载完成 sentinel 之后我们来学习一下 sentinel 预防/解决雪崩的方法。
流量控制
流量控制是预防发生雪崩。

观察左边的菜单栏,我们可以发现簇点链路,那么什么是簇点链路呢?
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路 。簇点链路中被监控的每一个接口就是一个资源。
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
点击流量控制,会弹出来一个窗口让我们设施流量控制的规则:
QPS 表示只允许该服务每秒处理 5 个请求,然后我们使用 jmeter 进行测试,看看如果每秒发送的请求超过 5 次会发生什么。

我们配置的 jmeter 的每秒 QPS 为10,那么是超过 sentinel 配置的流量规则的。

可以看到每秒只有 5 个请求被成功处理了,而剩下的请求则会失败。这就是 sentinel 的流量控制。
在添加流量控制规则的时候,我们还可以看到有高级选项,点开之后会发现还可以设置流控模式和流控效果。

那么接下来我们需要了解一下这些流控模式和流控效果。
流控模式
sentinel 中有三种流控模式:
- 直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流
直接流控模式的效果我们上面看到了,接下来我们来看看关联模式和链路模式。
关联模式
统计与当前资源相关的另一个资源,触发阈值的时候,会对当前资源进行限流。

图片中配置的规则的意思是:当 /order/update 资源的访问量触发阈值 5 的时候,就会对 /order/query 资源进行限流,避免影响 /order/update 资源。

使用 jmeter 进行测试,每秒向 /order/update 资源访问10次,然后再访问 /order/query 资源看看是否会被限流:
可以看到,当点击 /order/query 资源然后设置流控模式为关联模式并且管理的资源为 /order/update 的时候,如果每秒访问 /order/update 资源的次数超过了阈值的话,就会对 /order/query 资源进行限流操作。
这种模式适用于两个有竞争关系的资源并且两个资源存在优先级高低的情况,例如订单保存的优先级就比查看订单的优先级高。
链路模式
只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
假设有两条请求链路:/test1/common 和 /test2/common,当请求资源的需要很大的时候,我只想让从 /test1 或者 /test2 访问到 common 资源,那么这时我就可以使用链路模式的限流模式。
默认情况下,Service层中的方法是不被Sentinel监控的,需要我们自己通过 @SentinelResource("")
注解来标记要监控的方法。
链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,会导致链路模式失效。
我们需要关闭这种对SpringMVC的资源聚合,修服务的application.yml文件:
yaml
spring:
cloud:
sentinel:
web-context-unify: false # 关闭context整合
当我们使用 @SentinelResource 注解标记 service 层的方法,并且管理了 SpringMVC 的资源整合之后,重启服务,并且访问服务之后:

然后我们添加链路流控规则:

只统计从/order/query进入/goods的资源,QPS阈值为2,超出则被限流。
然后我们使用 jmeter 进行测试:

链路规则设置的是对访问 goods 资源的 /order/query 链路进行监控,所以通过 /order/save 链路访问资源就算超过了 QPS 也不会被限流:

但是 /order/query 链路却被限流了:

流控效果
在添加流控规则的时候再细心点我们可以发现还有流控效果可以设置。
流控效果是指请求达到流控阈值时应采取的措施,流控效果一共有三种:
- 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
- warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
- 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
warm up
阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.
例如,我设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.

新增流控规则,设置流控效果为 warm up,预热时长为 5 秒:

启动 jmeter 测试:

可以看到最开始大部分的请求都失败了,成功的请求之后三个 也就是 10/3,然后随着时间的推移,成功的请求变成了 5,6,7,8,9最终每秒成功的请求变成了 10 个。
排队等待
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。
那什么叫做预期等待时长呢?
比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:
- 第6个请求的预期等待时长 = 200 * (6 - 1) = 1000ms
- 第12个请求的预期等待时长 = 200 * (12-1) = 2200ms
那么这第12个请求就会被拒绝。
QPS 为 15,已经超过了我们设定的10。如果是之前的 快速失败、warmup模式,超出的请求应该会直接报错。
然后看看排队等待模式:
可以发现,QPS非常平滑,一致保持在10,但是超出的请求没有被拒绝,而是放入队列。因此响应时间(等待时间)会越来越长。当队列满了以后,才会有部分请求失败。
热点参数限流
前面的限流都是针对所有请求进行的限流,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
全局参数限流
当你访问某个资源的时候,如果传递的有参数,那么全局参数限流会统计你同一个参数出现的次数,如果同一个参数出现的次数超过了阈值的话,就会被限流。
这个热点参数的配置规则表示,对 hot 资源的 0 号参数也就是第一个参数做统计,每 1 秒相同参数的请求数不能超过 5 次。
热点参数限流
全局参数限流会对某一个参数的所有可能的值进行次数的限制,而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了:
点击高级选项:

参数类型只适合下面几种:
这样就可以对个别热点参数设置更高的 QPS,对第一个参数为值为 100 的设置 QPS 为10,为 101 的 QPS 设置为 15。
然后我们来演示一下,但是在演示之前我们需要知道:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源。
java
@SentinelResource("hot")
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
重启服务并且访问接口之后,可以看到我们标记的 hot 资源出现了:
然后我们点击左边菜单的热点规则进行配置:

QPS = 5
隔离和降级
上面的内容都是流量控制,通过流量控制可以预防我们的服务因为高并发而出现故障,但是有些服务并不是因为高并发而导致的异常,那么光依靠流量控制是无法避免出现雪崩的,所以不管你服务是因为什么原因导致的故障,我都需要将这个故障控制在一定范围之内,防止出现雪崩,那么要想做到这些就需要依靠线程隔离(舱壁模式) 和 熔断降级机制了。
线程隔离:

熔断降级:

可以发现不管是线程隔离还是熔断降级,都是针对客户端(调用方)的保护,需要在调用方发起远程调用时做线程隔离、或者服务熔断。
而我们微服务中实现服务之间的调用依赖于 Feign,所以我们需要解决 Feign 和 Sentinel 的整合问题。
在需要 Feign 和 Sentinel 服务的 application.yml 文件中添加配置:
yaml
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
编写熔断降级逻辑:
创建一个类实现 FallbackFactory<T>
接口:
java
import feign.hystrix.FallbackFactory;
@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();
}
};
}
}
然后将这个类交给 Spring 容器管理:
java
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
再将这个类添加到 @FeignClient 注解中:
java
@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
通过上面的操作就实现了熔断降级。
线程隔离(舱壁模式)
实现线程隔离的方式有两种:
- 线程池隔离
- 信号量隔离(Sentinel默认采用)

线程池隔离 :给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
这两种方式各有优缺点:
信号量隔离:轻量级、无额外的开销,但是不支持主动超时和异步调用
线程池隔离:支持主动超时和异步调用但是线程的额外开销较大
然后我们试试 Sentinel 的线程隔离:

在添加流控规则时候阈值类型选择并发线程数。
然后打开 jmeter 进行测试:

0 秒发送 10 个请求有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
这是请求成功的结果:

而这就走了失败降级的操作:

熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器 统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
而断路器熔断策略有三种:慢调用、异常比例、异常数
慢调用
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
我们在业务中延长一下业务执行的时间:
java
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) throws InterruptedException {
if (id == 1) {
// 休眠,触发熔断
Thread.sleep(60);
}
return userService.queryById(id);
}
然后看一下慢调用的业务时间和正常业务的适应时间:
然后设置熔断规则:

这个熔断规则的意思是:RT超过50ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过5次,并且慢调用比例不低于0.4,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
然后我们测试一下,看一下设置了熔断规则的慢调用的结果是什么样的:
慢调用接口快速点击五次,达到触发熔断的条件,然后再次请求:

可以发现这个请求所用的时间非常短,也就是说该请求被快速熔断了,然后再访问其他资源,也就是 /order/102 会发现该资源也被熔断了。

这也就是说明 /order/{orderId} 下的 /userservice/user/{id} 资源被熔断了。
异常比例、异常数
上面是对慢调用达到熔断条件之后进行了熔断操作,那么下面我们来看一下异常比例、异常数达到熔断条件之后的效果。

手动在业务中创建异常:
java
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) throws InterruptedException {
if (id == 1) {
// 休眠,触发熔断
Thread.sleep(60);
} else if (id == 2) {
throw new RuntimeException("故意出错,触发熔断");
}
return userService.queryById(id);
}

异常比例和异常数基本上是相同的,我这里就只展示一个。
熔断之前请求所需要的时间:

触发熔断之后请求所需要的时间:

访问 /order/103 发现也被熔断了:

授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问

资源名就是设置的需要被保护的资源,例如 /order/{orderId},流控应用:是来源者的名单,如果是勾选白名单,则名单中的来源被许可访问。如果是勾选黑名单,则名单中的来源被禁止访问。
这里有人可能会问了,之前我们不是学习过 gateway 吗,gateway 不是yekeyi实现鉴权吗,是的确实可以,但是使用 gateway 的话需要让请求经过 gateway,如果我们某个服务的地址被暴露了,那么就可以直接通过这个地址绕过 gateway 实现访问,这样就不安全了,而使用 sentinel 的授权规则,就可以对打到每个微服务的请求进行拦截。
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)。
那么我们如何获取 origin 呢?Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
java
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
这个方法的作用就是从request对象中,获取请求者的origin值并返回。但是默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。
所以我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。
java
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 1.获取请求头
String origin = request.getHeader("origin");
// 2.非空判断
if (StringUtils.isEmpty(origin)) {
origin = "blank";
}
return origin;
}
}
然后我们在 gateway 中进行设置,只要经过 gateway 的请求,我们都为他设置一个 origin header:
java
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=origin,gateway
那么没有经过 gateway 打到微服务的请求可能就没有 origin 这个 header,或者 origin 的值不是 gateway,这样就会被我们设置的 sentinel 的授权规则给拒绝掉。

然后我们跳过网关请求 /order/{orderId} 资源:

然后经过网关:

这就是 sentinel 的授权规则。
自定义异常结果
通过前面的演示,我们可以发现默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
为了对用户和我们更加友好,我们可以自定义异常类:
java
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
