流量安全优化:基于 Sentinel 实现网站流量控制和熔断
摘要:基于 Sentinel 实现网站流量控制和熔断,通过限流、熔断和降级机制防止系统过载,保障服务稳定性,提升系统容错能力。
网站流量控制和熔断
流量安全优化的目标可以简单概括为:确保数据在传输过程中的机密性、完整性和可用性,防止未经授权的访问、篡改、泄露和攻击,同时提升网络传输效率与性能。
而从流量控制和熔断的角度来看,流量安全优化的目标又可以概括为:防止系统过载、保障服务可用性、抵御恶意流量,并确保系统能够快速从故障中恢复。这也是本期教程中我们追求的目标。
核心概念
随着网站的发展,用户量逐渐增大,特别是互联网公司,用户量更是呈指数型增长,此时一旦出现促销活动,网站的流量会大大超越平均水平,在高并发请求下系统很可能会崩溃。
对应到我们的面试刷题平台,在金三银四或金九银十面试高峰期,网站流量会变大,还可能会有各种爬虫和恶意攻击。为了避免系统崩溃和保护服务稳定性,我们需要对网站做一定的防护措施。
常见的防护措施就是 流量控制:限制系统进入的请求数量,防止过载。
除此之外,为了进一步隔离和保护系统,防止某些组件异常时影响系统的稳定性,还会采用 熔断机制 + 降级策略 进行兜底处理,提升系统的健壮性和可用性。
下面分别对流量控制、熔断和降级进行解释:
1 流量控制
流量控制是为了 防止系统被过多的请求压垮,确保资源合理分配并保持服务的可用性,比如对请求数量的限制。
流量控制的 3 个主要优势:
- 防止过载:当瞬间涌入的请求量超出系统处理能力时,会导致资源枯竭,如 CPU 和内存耗尽。流量控制通过限制系统能处理的请求数,确保不会发生过载。
- 避免雪崩效应:高负载下某个服务崩溃可能引发其他依赖服务的崩溃,形成连锁反应。流量控制可以有效预防这种连锁故障,避免系统雪崩。
- 优化用户体验:即便部分请求被拒绝或延迟处理,流量控制也能确保大部分用户的请求能够正常响应,避免全局响应时间过长的情况。
常见的实现流量控制方法有 2 种:
- 限流:通过固定窗口、令牌桶或漏桶等算法限制单位时间内的请求数量。
- 排队:当请求量超出处理能力时,部分请求进入等待队列,防止立即超载。
如果大家使用过一些云服务,会更容易理解流量控制,主要有以下常见的流量控制类型:
1)请求频率限制:限制单位时间内单用户、单 IP 的请求数(如每秒最多 100 次请求)
2)带宽限制:控制访问系统时消耗的带宽量或者下载速度。
3)总流量限制:限制用户或系统整体的数据传输量。
4)细粒度控制:根据接口、用户等特定维度进行组合限流。如每人每分只能访问 5 次特定接口。
2 熔断机制
熔断机制的目的是 避免当下游服务发生异常时,整个系统继续耗费资源重复发起失败请求,从而防止连锁故障。
这类似于电路中的断路器,当检测到异常,熔断器会自动切断对故障服务的调用,防止问题扩大。
工作机制:
- 监控服务健康状态:系统会实时监控服务的调用情况,例如请求成功率、响应时间等,判断服务的健康状况。
- 进入熔断状态:当某个服务的错误率达到设定阈值(如响应时间过长或出错率过高)时,系统会 激活熔断器,暂时停止对该服务的调用,避免消耗不必要的资源和让错误进一步扩散。
- 快速失败:在熔断状态下,系统不会再等待超时,而是直接返回失败响应,减少系统资源占用,并避免因长时间等待导致用户体验的恶化。(也可以降级处理)
- 熔断恢复机制:熔断并非永久状态。在一段时间后,熔断器会进入 半开状态,允许少量请求测试服务的健康情况。如果恢复正常,熔断器将关闭,恢复正常服务调用;如果仍有问题,则继续保持熔断。
熔断流程:
举个例子,一个支付服务由于高负载频繁超时,此时熔断器会检测到支付服务的健康状况恶化,暂时切断对它的调用,防止前端系统继续发出请求。如果不采取熔断措施,支付服务的异常可能会拖垮整个系统,甚至影响其他依赖的服务模块或系统资源(比如请求连接)。
3 降级机制
降级的目的是在某个服务的响应能力下降、或该服务不可用时,提供简化版的功能或返回默认值作为 兜底,保持系统的部分功能可用,确保用户体验的连续性,避免系统频繁报错。
降级可以是手动配置,也可以根据系统负载自动触发。系统可能由于多种原因(如高负载、外部依赖不可用等)触发降级,返回简化的响应或默认值。
降级机制的好处:
- 优雅地处理故障:在降级状态下,系统不会直接返回错误信息,而是提供一个替代方案。例如,某个数据查询服务不可用时,系统可以返回缓存数据,确保用户看到的是有效信息,而非错误页面。
- 降低服务压力:降级有助于减轻系统对非核心服务的依赖,确保核心功能的稳定运行。例如,当推荐系统或广告服务出现故障时,降级可以减少对这些服务的调用,保护系统的整体稳定性。
举个例子,在一个电商网站上,如果商品推荐系统由于外部服务故障无法正常运行,可以触发降级机制,显示一组静态的推荐商品列表。这确保用户仍然能够顺利浏览商品页面,而不是直接看到错误信息。
是不是有点 try...catch... 的感觉?但降级这个概念显然比异常处理要更 "高大上" 一些,不一定是出了异常才降级,响应较慢或者受到其他服务影响可能也会触发降级。
4 熔断和降级的区别
初学者很容易把这两个概念搞混,二者是完全不同的概念,只不过经常结合使用罢了。
熔断不一定要降级,只是切断调用;降级也不一定需要熔断,单次调用失败也可以降级(比如数据库查询失败返回内存的数据)。
具体来说:
- 熔断是当服务健康状况恶化时,通过 切断调用 避免系统资源浪费或服务间故障扩散。
- 降级是在系统压力过大或某个服务不可用时,通过 提供简化的替代方案 ,保持系统可用性。
两者经常结合使用,先触发熔断后再进行降级。
需求分析(限流熔断规则)
回归到本项目的具体需求:要对什么资源进行限流熔断?规则是怎么样的?
我们来完成两个有代表性的需求:
- 对单个接口整体限流
- 对单个 IP 访问单个接口限流
1 查看题库列表接口限流熔断
资源:listQuestionBankVOByPage 接口
目的:控制对耗时较长的、经常访问的接口的请求频率,防止过多请求导致系统过载。
限流规则:
策略:整个接口每秒钟不超过 10 次请求
阻塞操作:提示“系统压力过大,请耐心等待”
熔断规则:
熔断条件:若接口异常率超 10%,或者慢调用(响应时长 > 3 秒)的比例大于 20%,触发熔断。
熔断操作:直接返回本地数据(缓存或空数据)
2 单 IP 查看题目列表限流熔断
资源:listQuestionVoByPage 接口
限流规则:
策略:每个 IP 地址每分钟允许查看题目列表的次数不能超过 60 次。
阻塞操作:提示"访问过于频繁,请稍后再试"
熔断规则:
熔断条件:若接口异常率超 10%,或者慢调用(响应时长 > 3 秒)的比例大于 20%,触发熔断。
熔断操作:直接返回本地数据(缓存或空数据)
后端开发(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) {
}
作用:Sentinel 流量控制
value = "listQuestionBankVOByPage"
:在 Sentinel 中这个资源的名称
blockHandler = "handleBlockException"
:被流量控制时的处理函数
比如:超过 QPS 限制、系统负载过高等
fallback = "handleFallback"
:业务异常时的降级函数
比如:数据库连接失败、服务调用超时等
启动项目,注意需加 JVM 参数 -Dcsp.sentinel.dashboard.server=consoleIp:port.
启动项目成功并且访问接口后,可以在控制台看到刚定义的资源:
2)实现限流阻塞和熔断降级方法
为了实现方便,尽快验证效果,我们先在接口相同的 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) {// 限流操作return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统压力过大,请耐心等待");
}
没有自定义处理方法时,用户会看到:
Blocked by Sentinel (flow limiting)
有自定义处理方法时,用户看到:
系统压力过大,请耐心等待
3)通过控制台定义规则
限流规则:根据需求配置即可
熔断规则:新增两条熔断规则,注意设置最小请求数、统计时长
4)测试
连续快速发送多次请求,触发限流,执行了 blockHandler
处理器的逻辑:
注意,只有业务异常(比如请求参数错误、或者数据库操作失败等问题),才会算到熔断条件中,限流熔断本身的异常 BlockException 是不计算的。
测试熔断的时候,可以故意给 sortField 请求参数传一个不存在的字段,触发业务异常。可以尝试下熔断的触发和恢复:
- 先通过传错业务参数触发异常,导致熔断
- 等待熔断结束后,再触发一次异常,还会继续熔断
- 过一段时间,再触发一次正常请求,则熔断解除
测试发现,任何业务异常(不仅仅是被熔断了),都会触发 fallbackHandler
,该方法可作为一个通用的降级逻辑处理器。
测试发现,如果 blockHandler
和 fallbackHandler
同时配置,当熔断器打开后,仍然会进入 blockHandler
进行处理,因此需要在该方法中处理因为熔断触发的降级逻辑:
/*** 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, "系统压力过大,请耐心等待");
}
Sentinel 的 blockHandler
处理的是BlockException
,该异常表示系统受到流量控制限制(如限流或熔断),这些不是业务逻辑中的异常,因此 fallback
不会处理这些异常。如果不配置 blockHandler
,才会在熔断时,进入到 fallbackHandler
中进行兜底。
总结一下:
blockHandler
处理 Sentinel 流量控制异常,如BlockException
。fallback
处理业务逻辑中的异常,比如我们自己的BusinessException
。
可以根据自己的实际情况配置。
2 单 IP 查看题目列表限流熔断
资源:listQuestionVoByPage 接口
限流规则:
- 策略:每个 IP 地址每分钟允许查看题目列表的次数不能超过 60 次。
- 阻塞操作:提示 "访问过于频繁,请稍后再试"
熔断规则:
- 熔断条件:如果接口异常率超过 10%,或者慢调用(响应时长 > 3 秒)的比例大于 20%,触发 60 秒熔断。
- 熔断操作:直接返回本地数据(缓存或空数据)
由于需要针对每个用户进一步精细化限流,而不是整体接口限流,可以采用 热点参数限流机制,允许根据参数控制限流触发条件。
对于我们的需求,可以将 IP 地址作为热点参数。
1)定义资源
对于 @SentinelResource
注解方式定义的资源,若注解作用的方法上有参数,Sentinel 会将它们作为参数传入 SphU.entry(res, args)
。比如以下的方法里面 uid
和 type
会分别作为第一个和第二个参数传入 Sentinel API,从而可以用于热点规则判断:
@SentinelResource("myMethod")
public Result doSomething(String uid, int type) { // some logic here... }
2)限流降级代码(初步版本)
由于 Controller 接口参数较杂乱,使用编程式定义资源的方法。
@SentinelResource("myMethod")
public Result doSomething(String uid, int type) {// 基于 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);}}
}
核心逻辑:编程式 API
entry = SphU.entry("listQuestionVOByPage", EntryType.IN, 1, remoteAddr);
这行代码的作用:
SphU.entry("listQuestionVOByPage")
:申请进入名为"listQuestionVOByPage"
的资源
EntryType.IN
:入口类型
1
:请求数量(用于限流计数)
remoteAddr
:热点参数(IP地址),用于实现"每个IP单独限流"相当于在说:"我要访问 listQuestionVOByPage 这个资源,来自 IP=remoteAddr 的请求"
注意:
- 使用热点参数时:如果
entry
传了参数,exit
也必须传相同的参数- 必须成对调用:每个
SphU.entry()
必须对应一个entry.exit()
- 避免 try-with-resources:try-with-resources 会自动调用
entry.exit()
,但无法传递热点参数,导致统计错误。业务异常统计原则:Sentinel 熔断规则只统计业务异常,不统计 Sentinel 自身的
BlockException
。必须手动记录业务异常,否则熔断规则无法正确触发
限流降级代码(完整版)
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 (Throwable ex) {// 区分业务异常和 Sentinel 异常if (!BlockException.isBlockException(ex)) {// ✅ 业务异常:手动记录,用于熔断统计Tracer.trace(ex);return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误");}// ❌ Sentinel 异常(BlockException):不计入熔断统计// 降级操作if (ex instanceof DegradeException) {return handleFallback(questionQueryRequest, request, ex);}// 限流操作return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "访问过于频繁,请稍后再试");} finally {if (entry != null) {entry.exit(1, remoteAddr);}
}
3)通过编码方式定义规则。可以新建 sentinel
包并定义一个单独的 Manager 作为 Bean,利用 @PostConstruct 注解,在 Bean 加载后创建规则。
@Component
public class SentinelRulesManager {@PostConstructpublic 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));}
}
4)测试
启动项目就能看到规则:
为了测试方便,可以先将规则的阈值调整小一点,然后通过接口文档验证效果。
限流效果:
测试降级效果的时候,可以故意将 sortField 传一个不存在的字段。效果如图,触发了 DegradeException:
大功告成!