前言
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
同一个资源可以对应多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。
一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
resource:资源名,即限流规则的作用对象
count: 限流阈值
grade: 限流阈值类型,QPS 或线程数
strategy: 根据调用关系选择策略
初始项目搭建
和以前一样准备一个新的项目模块,导入相关依赖,测试Sentinel流控效果项目主要需要引入的依赖主要是spring-cloud-starter-alibaba-sentinel和Nacos的服务注册与发现依赖spring-cloud-starter-alibaba-nacos-discovery,另外为了对服务进行监控,因此还需要spring-boot-starter-actuator依赖。大致如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringCloudAlibaba</artifactId>
<groupId>com.yy</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>Sentinel流控学习,端口号8024</description>
<artifactId>cloud-alibaba-sentinel-8024</artifactId>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- sentinel-datasource持久化-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.yy</groupId>
<artifactId>cloud-common</artifactId>
</dependency>
</dependencies>
</project>
同样的步骤配置yml文件。
server:
port: 8024
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos地址
sentinel:
transport:
dashboard: localhost:8080 #Sentinel Dashboard地址
#默认8180端口,如果被占用则依次从8180开始+1扫描,直至找到未被占用的端口
port: 8180
management:
endpoints:
web:
exposure:
include: "*"
然后构建基本的一些服务接口即可,这里不作详细赘述。主要是感受一下Sentinel不同的流控效果。另外需要注意的是,在启动项目前需要先启动Nacos服务以及运行Sentinel监控面板的jar包。但是初始运行后,由于Sentinel是懒加载的原因,并不能第一时间在面板上显示我们的服务名称,而是需要手动调试一个服务接口后,方能显示服务名称以及对应的服务空值信息。
然后我们就可以在右上角添加一些流控规则。
资源名:唯一名称,默认请求路径
针对来源: Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
阈值类型/单机阈值:
1,QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
2,线程数:当调用该api的线程数达到[阈值的时候,进行限流
是否集群:不需要集群
流控模式:
1,直接: api达到限流条件时,直接限
2,关联:当关联的资源达到阈值时,就限流自己
3,链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
流控效果:
1,快速失败:直接失败,抛异常
2,Warm up:根据codeFactor (冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。下面逐一进行测试。
QPS流量控制效果
当采用QPS(每秒请求数)流量控制时,当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括下面 3 种,对应 FlowRule 中的 controlBehavior 字段:
1,直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式。
该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。下面选择选择一个api接口使用直接拒绝方式,设置阈值为1。
然后我们能就可以测试ap接口效果了,如果一秒请求一次该接口,那么响应信息正常返回。
但是如果我们快速刷新请求一秒内多次访问,则会抛出flowException异常,这里笔者已经处理过异常返回结果了
package com.yy.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yy.enums.ResultEnum;
import com.yy.utils.Result;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Author young
* Date 2023/1/10 20:14
* Description: Sentinel自定义异常返回
*/
@Component
public class SentinelResponseConfig implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
// BlockException 异常接口,其子类为Sentinel五种规则异常的实现类
// AuthorityException 授权异常
// DegradeException 降级异常
// FlowException 限流异常
// ParamFlowException 参数限流异常
// SystemBlockException 系统负载异常
String msg = null;
if (e instanceof FlowException) {
msg = "服务被限流了";
} else if (e instanceof DegradeException) {
msg = "服务被降级了";
} else if (e instanceof ParamFlowException) {
msg = "热点参数限流";
} else if (e instanceof SystemBlockException) {
msg = "系统规则(负载/...不满足要求)";
} else if (e instanceof AuthorityException) {
msg = "授权规则不通过";
}
// http状态码
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setHeader("Content-Type", "application/json;charset=utf-8");
httpServletResponse.setContentType("application/json;charset=utf-8");
// spring mvc自带的json操作工具,叫jackson
new ObjectMapper()
.writeValue(
httpServletResponse.getWriter(),
Result.fail().message(msg)
);
}
}
因此是如下这样的:
2,冷启动(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式
该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。可以类比于redis中的缓存击穿一样,在突然面临高并发访问的情况下,为了防止服务器受到冲击而设置一个提前预热,这样形成的流控效果。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
同样的,我们利用另一个api接口来测试冷启动方式的流控效果。设置一个新的流控规则。
设置完成后api的请求数会根据codeFactor (冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。也就是说如果我们设置单机阈值最高为10,则10/3=3,那么5秒内虽然我们设置了单机最高阈值为10,但是初始仍然以单机阈值3开始,也就是一秒前3个请求是正常的,其他请求会被限制。但是5秒后,会逐步恢复最高阈值的请求数。
然后我们利用ApiPost对api接口进行自动化测试,执行20次,没有时间间隔。
然后我们就会发现前三次,请求返回正常,但是第四次就被限流了。
由于执行次数太少时间太快,后续并没有观察到不过如果我们自己刷新的话,仍然可以发现5秒后,一秒10次请求可以正常返回信息了。
3,匀速器(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式
这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过。否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。
匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效。并且需要注意的是注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
这里我们设置QPS为2,也就是一秒最高两个请求,然后设置排队等待效果,设置超时时间。然后通过ApiPost模拟10个请求,这样如果在快速失败的情况下,从第三个就会出现限流,但是使用排队等待后,服务的请求就会排队执行,并不会抛出异常情况了。
并发线程数流量控制
线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对高线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离),或者使用信号量来控制同时请求的个数(信号量隔离)。这种隔离方案虽然能够控制线程数量,但无法控制请求排队时间。当请求过多时排队也是无益的,直接拒绝能够迅速降低系统压力。Sentinel线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程个数,如果超出阈值,新的请求会被立即拒绝。并发数控制通常在调用端进行配置。
可以通过线程池模拟客户端调用, 也可以通过 Jmeter 模拟,触发流控的结果如下:
curl http://127.0.0.1:8088/getStockDetail{"code":101,"message":"接口限流了","data":null}%
流控模式
调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。
直接
当资源触发流控规则过后直接,抛出异常信息同上。
关联
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写的速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。
如果配置流控规则为关联模式,那么当 /hello 接口超过阈值过后,就会对 /getStockDetail 接口触发流控规则。
链路
NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategy 为 RuleConstant.CHAIN,同时设置 FlowRule.ref_identity 为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而对来自 Entrance2 的调用漠不关心。
调用链的入口是通过 API 方法 ContextUtil.enter(name) 定义的。
这个模式主要仅对同一个service的某个入口进行流控,其他入口不做控制
sentinel1.7.0版本后需通过如下方式使链路流控模式生效(摘自官方)
import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class FilterConfiguration {
/**
* @NOTE 在spring-cloud-alibaba v2.1.1.RELEASE及前,sentinel1.7.0及后,关闭URL PATH聚合需要通过该方式,
* spring-cloud-alibaba v2.1.1.RELEASE后,可以通过配置关闭:spring.cloud.sentinel.web-context-unify=false
* 手动注入Sentinel的过滤器,关闭Sentinel注入CommonFilter实例,修改配置文件中的 spring.cloud.sentinel.filter.enabled=false
* 入口资源聚合问题:https://github.com/alibaba/Sentinel/issues/1024 或 https://github.com/alibaba/Sentinel/issues/1213
* 入口资源聚合问题解决:https://github.com/alibaba/Sentinel/pull/1111
*/
@Bean
public FilterRegistrationBean<Filter> registrationBean() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CommonFilter());
registrationBean.addUrlPatterns("/*");
// 入口资源关闭聚合
registrationBean.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registrationBean.setName("sentinelFilter");
return registrationBean;
}
}