网站流量控制和熔断
流量控制
通过对请求数量的限制,防止系统被过多的请求压垮。
流量控制的3个主要优势:
1、防止过载
2、避免雪崩效应
3、优化用户体验
常见的实现流量控制的2种方式:
1、限流:通过固定窗口、令牌桶或漏桶等算法限制单位时间内的请求数量
2、排队:当请求量超出处理能力时,部分请求进入等待队列,防止立即超载
熔断机制
目的是当我们的某个下游服务发生异常时,赶紧把这个服务隔离或降级,不让它一直发送失败请求,保证别的服务功能能正常使用,整个系统不会挂掉。
熔断的工作机制:
1、监控服务健康状态:系统会实时监控服务的调用情况,例如请求成功率、响应时间等,判断服务的健康状况。
2、进入熔断状态:当某个服务的错误率达到设定阈值(如响应时间过长或出错率过高)时,系统会 激活熔断器,暂时停止对该服务的调用,避免消耗不必要的资源和让错误进一步扩散。
3、快速失败:在熔断状态下,系统不会再等待超时,而是直接返回失败响应,减少系统资源占用,并避免因长时间等待导致用户体验的恶化(也可以降级处理)。
4、熔断恢复机制:熔断并非永久状态。在一段时间后,熔断器会进入 半开状态,允许少量请求测试服务的健康情况。如果恢复正常,熔断器将关闭,恢复正常服务调用;如果仍有问题,则继续保持熔断。
降级机制
某个服务响应能力下降或该服务不可用,提供一个简化版的功能或返回默认值作为兜底,保持系统的部分功能可用,确保用户的体验连续性,避免系统频繁报错。
熔断和降级一般是结合使用,先触发熔断再进行降级。
有损服务
就是"丢车保帅",在系统资源有限或负载较高的情况下,系统有意识的舍弃部分非核心功能,来确保系统整体的稳定和核心功能的可用性。
下面我以题库题目场景为例
1、查看题库列表接口限流熔断:
资源:listQuestionBankVOByPage 接囗
目的:控制对耗时较长的、经常访问的接口的请求频率,防止过多请求导致系统过载。
限流规则:
策略:整个接口每秒钟不超过 10 次请求
阻塞操作:提示"系统压力过大,请耐心等待
熔断规则:
熔断条件:如果接口异常率超过 10%,或者慢调用(响应时长>3秒)的比例大于 20%,触发 60 秒熔断。
熔断操作:直接返回本地数据(缓存或空数据)
2、单IP查看题目列表限流熔断:
资源:listQuestionVoByPage 接口
限流规则:
策略:每个IP 地址每分钟允许查看题目列表的次数不能超过 60 次。
阻塞操作:提示"访问过于频繁,请稍后再试
熔断规则:
熔断条件:如果接口异常率超过 10%,或者慢调用(响应时长>3秒)的比例大于 20%,触发 60 秒熔断。
熔断操作:直接返回本地数据(缓存或空数据)
Sentinel
Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应保护等多个维度来帮助用户保障微服务的稳定性。
sentinel的功能:
限流:支持基于 QPS、并发数量等条件的限流,支持滑动窗口、预热、漏桶等算法。
熔断降级:支持失败率、慢调用比例等指标触发熔断,并提供自动恢复机制。
热点参数限流:可以基于特定的参数进行限流,如限制特定用户ID的请求频率。
系统负载保护:可以根据系统的实际负载(如 CPU、内存)动态调整流量。
丰富的规则配置:通过配置中心或控制台动态调整限流和熔断规则。
优势:功能丰富、提供控制台、更新较频繁、社区活跃、文档清晰,能够快速入门上手。
sentinel主要是对资源进行保护,主要分为3步:定义资源、定义规则、校验规则是否生效
Sentinel配置
在项目中引入sentinel的依赖:
<!-- sentinel-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
然后我们在zhimianguan目录下新建一个sentinel包,编写一个SentinelTest类试试sentinel使用:
public class SentinelTest {
public static void main(String[] args) {
// 配置规则.
initFlowRules();
while (true) {
// 1.5.0 版本开始可以直接利用 try-with-resources 特性
try (Entry entry = SphU.entry("HelloWorld")) {
// 被保护的逻辑
System.out.println("hello world");
} catch (BlockException ex) {
// 处理被流控的逻辑
System.out.println("blocked!");
}
}
}
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// Set limit QPS to 20.
rule.setCount(20);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
然后我们需要根据官方文档下载sentinel的控制台,就是下载它的jar包然后根据官方文档提供步骤在命令行进行启动,然后访问控制台。
启动的命令:java -Dserver.port=8131 -jar sentinel-dashboard-1.8.6.jar
我这边设置启动的端口是8131,本地输入http://localhost:8131/,即可访问控制台,账号密码默认都是sentinel
接着在我们的项目中引入控制台的依赖,使项目与sentinel控制台通讯:
<!-- sentinel控制台-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
然后我们需要配置一下sentinel的JVM虚拟机参数才能在控制台展示使用,步骤如下:

然后输入参数:-Dcsp.sentinel.dashboard.server=localhost:8131
然后启动这个SentinelTest,刷新控制台就可以在控制台看到数据了。
规则管理和推送:sentinel的规则存储在哪里呢?又是如何通过控制台修改规则后,将规则同步给客户端进行限流熔断呢?
官方文档中说,规则推送主要分3种模式:原始模式、pull模式、push模式。
目前控制台的规则推送也是通过 规则查询更改 HTTP API 来更改规则。这也意味着这些规则默认仅在内存态 生效,应用重启之后,该规则会丢失。
以上是原始模式。当了解了原始模式之后,官方建议通过 动态规则 并结合各种外部存储来定制自己的规则源。我们推荐通过动态配置源的控制台来进行规则写入和推送,而不是通过 Sentinel 客户端直接写入到动态配置源中。
在生产环境中,官方推荐 push 模式,支持自定义存储规则的配置中心,控制台改变规则后,会 push 到配置中心。
那我们怎么在项目中使用呢?下面来整合到springboot实践它(基于springboot starter+注解模式开发+原始规则推送模式开发):
在引入整合依赖时,一定要注意版本号,参考官方的版本说明选择版本号。那我们的项目采用的springboot版本是2.7.2版本的,因此使用的sentinel starter版本为2021.0.5.0,在项目中引入依赖(可以把刚才引入的两个sentinel依赖都删掉了,因为sentinel starter它默认给我们整合了这两个依赖):
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.0.5.0</version>
</dependency>
然后我们刚才是不是给SentinelTest添加了JVM参数配置,同样的我们现在要运用到整个项目,我们去给启动类MainApplication添加上刚才的那个参数配置。
然后启动项目,并且去接口文档随便发送个请求一下,我们的sentinel控制台就会接收到资源,自动注册对应请求的接口。
通过控制台自动识别是不是很方便,当然了,sentinel官方文档还有多种不同的方式定义资源和定义规则。
开发模式:
建议按照优先使用适配包来自动识别资源,然后能运用注解尽量运用注解,最后再选择主动编码定义资源。
回归到项目,将sentinel保护资源,限流、熔断等用到我们项目中的刚才那两条需求分析。
下面我们来在实际项目场景中运用sentinel进行流量控制和熔断。
后端开发(sentinel实战)
1、查看题库列表接口限流熔断:
资源:listQuestionBankVOByPage 接囗
目的:控制对耗时较长的、经常访问的接口的请求频率,防止过多请求导致系统过载。
限流规则:
策略:整个接口每秒钟不超过 10 次请求
阻塞操作:提示"系统压力过大,请耐心等待
熔断规则:
熔断条件:如果接口异常率超过 10%,或者慢调用(响应时长>3秒)的比例大于 20%,触发 60 秒熔断。
熔断操作:直接返回本地数据(缓存或空数据)
开发模式:用注解定义资源 + 基于控制台定义规则
1)定义资源:给需要限流的接口添加@SentinelResource注解
@PostMapping("/list/page/vo")
@SentinelResource(value = "listQuestionBankVOByPage",
blockHandler = "handleBlockException",
fallback = "handleFallback")
public BaseResponse<Page<QuestionBankVO>> listQuestionBankVOByPage(@RequestBody QuestionBankQueryRequest
questionBankQueryRequest,HttpServletRequest request) {
2)实现限流阻塞和熔断降级方法
然后我们需要来定义这些处理的方法blockHandler和fallback(为了方便快速验证实现,直接在controller里面编写):
/**
* listQuestionBankVOByPage 降级操作:直接返回本地数据
*/
public BaseResponse<Page<QuestionBankVO>> handleFallback(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,
HttpServletRequest request, Throwable ex) {
// 可以返回本地数据或空数据
return ResultUtils.success(null);
}
/**
* listQuestionBankVOByPage 流控操作
* 限流:提示"系统压力过大,请耐心等待"
*/
public BaseResponse<Page<QuestionBankVO>> handleBlockException(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,
HttpServletRequest request, BlockException ex) {
//降级操作
if(ex instanceof DegradeException){
return handleFallback(questionBankQueryRequest, request, ex);
}
// 限流操作
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统压力过大,请耐心等待");
}
3)定义规则:接着我们重启项目,打开接口文档随便发一个对应接口的请求,然后打开sentinel的控制台的簇点链路会有一个listQuestionBankVOByPage,然后我们可以点击流控给它配置一个限流规则,设置单机阈值为10。熔断的熔断规则,先设置慢调用比例为:最大RT3000,比例阈值0.2,熔断时长60,最小请求数10,统计时长30000;然后设置异常比例为:比例阈值0.1,熔断时长60,最小请求数10,统计时长30000。
2、单IP查看题目列表限流熔断:
资源:listQuestionVoByPage 接口
限流规则:
策略:每个IP 地址每分钟允许查看题目列表的次数不能超过 60 次。
阻塞操作:提示"访问过于频繁,请稍后再试
熔断规则:
熔断条件:如果接口异常率超过 10%,或者慢调用(响应时长>3秒)的比例大于 20%,触发 60 秒熔断。
熔断操作:直接返回本地数据(缓存或空数据)
开发模式:编程式定义资源 + 编码方式定义规则
需要针对每一个用户进一步细化限流,而不是整体接口限流,可以采用热点参数限流机制,允许根据参数控制限流触发条件,
因此我们可以将IP地址作为热点参数。
1)定义资源:
首先我们要在QuestionController中重新写一个listQuestionVOByPageSentinel接口,不要在listQuestionVOByPage里写,等测试稳定后再进行切换,代码如下:
/**
* 分页获取题目列表(封装类)---限流版
*
* @param questionQueryRequest
* @param request
* @return
*/
@PostMapping("/list/page/vo/sentinel")
public BaseResponse<Page<QuestionVO>> listQuestionVOByPageSentinel(@RequestBody QuestionQueryRequest questionQueryRequest,
HttpServletRequest request) {
ThrowUtils.throwIf(questionQueryRequest == null, ErrorCode.PARAMS_ERROR);
long size = questionQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
// 基于 IP 限流
String remoteAddr = request.getRemoteAddr();
Entry entry = null;
try {
entry = SphU.entry("listQuestionVOByPage", EntryType.IN, 1, remoteAddr);
// 被保护的业务逻辑
// 查询数据库
Page<Question> questionPage = questionService.listQuestionByPage(questionQueryRequest);
// 获取封装类
return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
if (ex instanceof DegradeException) {
return handleFallback(questionQueryRequest, request, ex);
}
// 限流操作
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "访问过于频繁,请稍后再试");
} finally {
if (entry != null) {
entry.exit(1, remoteAddr);
}
}
}
/**
* listQuestionVOByPageSentinel 降级操作:直接返回本地数据
*/
public BaseResponse<Page<QuestionVO>> handleFallback(@RequestBody QuestionQueryRequest questionQueryRequestcccccccccc,
HttpServletRequest request, Throwable c) {
// 可以返回本地数据或空数据
return ResultUtils.success(null);
}
注意 Sentinel 的降级仅针对业务异常,对 Sentinel限流降级本身的异常 BlockException 不生效。为了统计异常比例或异常数,需要手动通过 Tracer.trace(ex)记录业务异常。
为什么上一个需求中,我们不用手动调用 Tracer 上报异常呢?因为使用 Sentinel 的开源整合模块,如 SentineDubbo Adapter, Sentinel Web Servlet filter 或 @sentinelResource 注解会自动统计业务异常,无需手动调用。这里需要给我们的资源定义增加异常统计代码,修改的部分修改后代码如下:
catch (Throwable ex) {
// 业务异常
if (!BlockException.isBlockException(ex)) {
Tracer.trace(ex);
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");
}
// 降级操作
if (ex instanceof DegradeException) {
return handleFallback(questionQueryRequest, request, ex);
}
// 限流操作
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "访问过于频繁,请稍后再试");
2)编写阻塞和降级操作代码:
这个其实就是刚才写的handleFallback方法,刚刚已经写完了。
3)定义规则(上个需求是使用控制台定义规则,每次都要用控制台来有点麻烦,这边使用编码形式):
新建一个sentinel包并创建一个SentinelRulesManager作为Bean,利用@PostConstruct注解,在Bean加载后创建规则,代码如下:
@Component
public class SentinelRulesManager {
@PostConstruct
public void initRules() {
initFlowRules();
initDegradeRules();
}
// 限流规则
public void initFlowRules() {
// 单 IP 查看题目列表限流规则
ParamFlowRule rule = new ParamFlowRule("listQuestionVOByPage")
.setParamIdx(0) // 对第 0 个参数限流,即 IP 地址
.setCount(60) // 每分钟最多 60 次
.setDurationInSec(60); // 规则的统计周期为 60 秒
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
}
// 降级规则
public void initDegradeRules() {
// 单 IP 查看题目列表熔断规则
DegradeRule slowCallRule = new DegradeRule("listQuestionVOByPage")
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
.setCount(0.2) // 慢调用比例大于 20%
.setTimeWindow(60) // 熔断持续时间 60 秒
.setStatIntervalMs(30 * 1000) // 统计时长 30 秒
.setMinRequestAmount(10) // 最小请求数
.setSlowRatioThreshold(3); // 响应时间超过 3 秒
DegradeRule errorRateRule = new DegradeRule("listQuestionVOByPage")
.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
.setCount(0.1) // 异常率大于 10%
.setTimeWindow(60) // 熔断持续时间 60 秒
.setStatIntervalMs(30 * 1000) // 统计时长 30 秒
.setMinRequestAmount(10); // 最小请求数
// 加载规则
DegradeRuleManager.loadRules(Arrays.asList(slowCallRule, errorRateRule));
}
}
然后我们启动项目,打开sentinel控制台就能看到规则了,然后进行测试。
**注意:**如果你关机了,每次启动项目前都要先通过命令启动sentinel(要在所属jar包的文件中输入cmd然后输入启动命令java -Dserver.port=8131 -jar sentinel-dashboard-1.8.6.jar)
扩展
1、将规则配置本地持久化(用文件来本地持久化配置,这样项目重启就不会丢失)
在SentinelRulesManager中编写代码使用:
/**
* 持久化配置为本地文件
*/
public void listenRules() throws Exception {
// 获取项目根目录
String rootPath = System.getProperty("user.dir");
// sentinel 目录路径
File sentinelDir = new File(rootPath, "sentinel");
// 目录不存在则创建
if (!FileUtil.exist(sentinelDir)) {
FileUtil.mkdir(sentinelDir);
}
// 规则文件路径
String flowRulePath = new File(sentinelDir, "FlowRule.json").getAbsolutePath();
String degradeRulePath = new File(sentinelDir, "DegradeRule.json").getAbsolutePath();
// Data source for FlowRule
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new FileRefreshableDataSource<>(flowRulePath, flowRuleListParser);
// Register to flow rule manager.
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
WritableDataSource<List<FlowRule>> flowWds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
// Register to writable data source registry so that rules can be updated to file
WritableDataSourceRegistry.registerFlowDataSource(flowWds);
// Data source for DegradeRule
FileRefreshableDataSource<List<DegradeRule>> degradeRuleDataSource
= new FileRefreshableDataSource<>(
degradeRulePath, degradeRuleListParser);
DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
WritableDataSource<List<DegradeRule>> degradeWds = new FileWritableDataSource<>(degradeRulePath, this::encodeJson);
// Register to writable data source registry so that rules can be updated to file
WritableDataSourceRegistry.registerDegradeDataSource(degradeWds);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(source,
new TypeReference<List<FlowRule>>() {
});
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(source,
new TypeReference<List<DegradeRule>>() {
});
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
当然了,也可以根据官方文档使用push模式,用Nacos来做持久化。
2、封装限流组件为Spring Boot Starter(可做可不做吧)