2024 年我们做了一场限时秒杀,活动开始三分钟,下单服务就挂了。复盘发现:除了正常用户,还有一批黑产用脚本疯狂刷下单接口,峰值 QPS 冲到 21 万,远超系统设计容量,服务线程被瞬间打满,连正常用户都下不了单。活动被迫中止。事后投了一周接入并打磨 Sentinel 流量治理,从限流、熔断、热点防护到系统自适应保护全部配齐,后面几场秒杀再没崩过。本文复盘 Sentinel 流量防护的完整实战。
问题背景
业务:电商下单服务(Spring Cloud),秒杀活动
正常容量:单实例约 3000 QPS,8 实例
事故现象:
- 活动开始 3 分钟,下单接口 QPS 冲到 21 万
- 远超 8 实例 2.4 万的设计容量
- 线程池打满,Tomcat 拒绝新请求
- 下游库存服务被连带拖慢,链路雪崩
- 正常用户大量下单失败
抓包分析流量来源:
- 正常用户:约 1.8 万 QPS
- 黑产脚本:约 19 万 QPS(同一批 IP/设备,疯狂重试)
根因:
1. 接口完全没有限流,来多少收多少
2. 下游(库存/风控)变慢时,上游无熔断,线程全卡在等待
3. 没有针对"单用户/单商品"的细粒度防刷
4. 系统过载时没有自我保护,直到彻底崩溃
5. 没有降级预案,挂了就是白屏
修复 1:QPS 流控
// 接入 Sentinel,给下单接口定义资源并配流控规则
// === 用注解定义资源 ===
@Service
public class OrderService {
@SentinelResource(value = "createOrder",
blockHandler = "createOrderBlocked")
public OrderResult createOrder(OrderRequest req) {
return doCreateOrder(req);
}
// 被限流/降级时走这里(方法签名要和原方法一致 + BlockException)
public OrderResult createOrderBlocked(OrderRequest req, BlockException ex) {
// 不是报错,而是友好提示:让用户重试,而不是看到 500
return OrderResult.fail("当前下单人数过多,请稍后再试");
}
}
// === 流控规则:限制 createOrder 的 QPS ===
public void initFlowRules() {
FlowRule rule = new FlowRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 按 QPS 限流
rule.setCount(2500); // 单实例阈值 2500 QPS
// 控制效果:超过阈值后怎么办
rule.setControlBehavior(
RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 直接拒绝(快速失败)
FlowRuleManager.loadRules(Collections.singletonList(rule));
}
// === 三种控制效果,按场景选 ===
// 1. 直接拒绝(CONTROL_BEHAVIOR_DEFAULT)
// 超阈值立即拒绝,适合明确知道容量上限的接口
// 2. Warm Up(CONTROL_BEHAVIOR_WARM_UP)
// 阈值从低缓慢爬升到设定值,给系统(缓存/连接池)预热时间
// 适合"冷启动"场景,防止瞬间满负荷把冷系统打挂
// rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
// rule.setWarmUpPeriodSec(10);
// 3. 排队等待(CONTROL_BEHAVIOR_RATE_LIMITER)
// 匀速放行,多余请求排队(超时则拒绝),削峰填谷
// 适合突发流量、又不想丢请求的写场景(如消息处理)
// rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
// rule.setMaxQueueingTimeMs(500);
修复 2:关联限流与链路限流
// QPS 直接限流只看自己,Sentinel 还能基于"关系"限流
// === 关联限流:当关联资源压力大时,限流本资源 ===
// 场景:读接口和写接口共用一个 DB
// 写库存(扣减)很重要,读库存(查询)可以让步
// 配置:当"写库存"QPS 高时,限流"读库存",把 DB 资源让给写
public void initRelateRule() {
FlowRule rule = new FlowRule();
rule.setResource("queryStock"); // 限流对象:读库存
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1000);
rule.setStrategy(RuleConstant.STRATEGY_RELATE);
rule.setRefResource("deductStock"); // 关联资源:写库存
// 当 deductStock QPS 达到压力时,queryStock 被限到 1000
FlowRuleManager.loadRules(Collections.singletonList(rule));
}
// === 链路限流:只对来自特定调用链的流量限流 ===
// 场景:同一个"扣库存"方法,被"秒杀下单"和"普通下单"都调用
// 只想限制秒杀链路打过来的,普通下单不限
public void initChainRule() {
FlowRule rule = new FlowRule();
rule.setResource("deductStock");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(500);
rule.setStrategy(RuleConstant.STRATEGY_CHAIN);
rule.setRefResource("seckillEntry"); // 只限来自秒杀入口的链路
FlowRuleManager.loadRules(Collections.singletonList(rule));
}
// 链路限流要配合 web-context-unify=false,让不同入口各成一条链路
修复 3:熔断降级
// 下游(风控/库存)变慢或大量报错时,熔断它,避免线程全卡在等待
// === 熔断规则:三种触发策略 ===
public void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
// 策略 1:慢调用比例 —— 最常用
DegradeRule slow = new DegradeRule("riskCheck");
slow.setGrade(RuleConstant.DEGRADE_GRADE_RT);
slow.setCount(200); // RT 阈值 200ms,超过算"慢调用"
slow.setSlowRatioThreshold(0.5);// 慢调用比例超 50% → 熔断
slow.setStatIntervalMs(10000); // 统计窗口 10s
slow.setMinRequestAmount(20); // 窗口内至少 20 个请求才触发(防误判)
slow.setTimeWindow(10); // 熔断 10s,之后进入半开探测
rules.add(slow);
// 策略 2:异常比例
DegradeRule errRatio = new DegradeRule("riskCheck");
errRatio.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
errRatio.setCount(0.5); // 异常比例超 50% → 熔断
errRatio.setStatIntervalMs(10000);
errRatio.setMinRequestAmount(20);
errRatio.setTimeWindow(10);
rules.add(errRatio);
// 策略 3:异常数
DegradeRule errCount = new DegradeRule("riskCheck");
errCount.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
errCount.setCount(50); // 1 分钟异常数超 50 → 熔断
errCount.setTimeWindow(30);
rules.add(errCount);
DegradeRuleManager.loadRules(rules);
}
// === 熔断后的降级处理 ===
@SentinelResource(value = "riskCheck", fallback = "riskCheckFallback")
public RiskResult riskCheck(OrderRequest req) {
return riskClient.check(req); // 调第三方风控
}
// fallback 处理"业务异常 + 熔断";blockHandler 只处理"限流/熔断 Block"
public RiskResult riskCheckFallback(OrderRequest req, Throwable t) {
// 风控熔断时的降级策略:小额订单放行,大额订单转人工审核
if (req.getAmount() < 100) {
return RiskResult.pass("风控降级:小额自动放行");
}
return RiskResult.review("风控降级:转人工审核");
}
// 熔断状态机:Closed(正常)→ 触发阈值 → Open(熔断,直接走 fallback)
// → timeWindow 后 → Half-Open(放一个请求探测)
// → 成功则 Closed,失败则继续 Open
修复 4:热点参数限流
// 普通限流是"接口级",但黑产刷的是"同一个用户/同一个商品"
// 热点参数限流:对参数的某些具体值单独限流
// === 给资源标注热点参数 ===
@SentinelResource("seckillOrder")
public OrderResult seckillOrder(
@SentinelHotParam Long userId, // 第 0 个参数:用户 id
@SentinelHotParam Long itemId) { // 第 1 个参数:商品 id
return doSeckill(userId, itemId);
}
// === 热点规则:对单个用户限流 ===
public void initHotParamRule() {
ParamFlowRule rule = new ParamFlowRule("seckillOrder");
rule.setParamIdx(0); // 对第 0 个参数(userId)限流
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(5); // 默认:每个 userId 最多 5 QPS
rule.setDurationInSec(1);
// 例外:某些 VIP 用户或内部账号放宽
ParamFlowItem vip = new ParamFlowItem();
vip.setObject(String.valueOf(10001L)); // userId=10001
vip.setClassType(long.class.getName());
vip.setCount(100); // 这个用户单独放到 100 QPS
rule.setParamFlowItemList(Collections.singletonList(vip));
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
}
// 效果:黑产单个账号狂刷 → 被限到 5 QPS,正常用户(点几下)完全无感
// 这是防刷最有效的一招:接口级限流挡不住"分散到大量请求"的刷子,
// 但热点限流精准锁定"单个值刷得太猛"
// === 也可对商品维度限流 ===
// rule.setParamIdx(1) → 对 itemId 限流,防单个爆品把系统打偏
修复 5:系统自适应保护与网关流控
// === 系统自适应保护:最后一道兜底,基于机器整体负载 ===
// 前面的规则都要"预先设阈值",但容量会变(GC、其他服务抢资源)
// 系统规则:不管具体接口,当机器整体扛不住时,自动拒绝入口流量
public void initSystemRule() {
List<SystemRule> rules = new ArrayList<>();
SystemRule load = new SystemRule();
load.setHighestSystemLoad(8.0); // 系统 load1 阈值(参考核数)
rules.add(load);
SystemRule cpu = new SystemRule();
cpu.setHighestCpuUsage(0.8); // CPU 使用率超 80% → 开始保护
rules.add(cpu);
SystemRule qps = new SystemRule();
qps.setQps(20000); // 入口总 QPS 上限
rules.add(qps);
SystemRule rt = new SystemRule();
rt.setAvgRt(500); // 平均 RT 超 500ms → 保护
rules.add(rt);
SystemRuleManager.loadRules(rules);
}
// 系统保护是"自适应"的:它不需要你算准容量,而是看机器实际状态
// 真过载了就拦,是规则没配全时的最后保险
# === 网关层流控:在 Spring Cloud Gateway 用 Sentinel,把限流前移 ===
# 流量在网关就被挡掉,根本不进到下游业务服务,保护更彻底
spring:
cloud:
sentinel:
transport:
dashboard: sentinel-dashboard:8080 # 控制台地址
# 网关流控规则(也可在 Dashboard 动态配)
scg:
fallback:
mode: response
response-status: 429
response-body: '{"code":429,"msg":"请求过于频繁,请稍后再试"}'
---
# 网关 API 分组 + 限流:对 /api/seckill/** 这组路由限流
# 在 Dashboard 配:resource=seckill_route, grade=QPS, count=15000
# 配合 routeId 或自定义 API 分组,粒度灵活
修复 6:监控告警
# Sentinel 指标接 Prometheus(sentinel + micrometer 暴露)
groups:
- name: sentinel-flow
rules:
# 1. 限流触发量(被 block 的请求数)
- alert: FlowBlockedHigh
expr: rate(sentinel_block_qps{resource="createOrder"}[1m]) > 1000
for: 2m
annotations:
summary: "{{ $labels.resource }} 限流 block > 1000/s,可能遭刷或容量不足"
# 2. 熔断触发(资源进入熔断状态)
- alert: CircuitBreakerOpen
expr: sentinel_circuit_breaker_state{state="open"} == 1
for: 1m
annotations:
summary: "{{ $labels.resource }} 已熔断,排查下游慢/异常"
# 3. 资源 RT 飙高(熔断前兆)
- alert: ResourceRtHigh
expr: sentinel_resource_rt{resource="riskCheck"} > 300
for: 3m
annotations:
summary: "{{ $labels.resource }} RT > 300ms,接近慢调用熔断阈值"
# 4. 系统自适应保护触发(整体过载)
- alert: SystemProtectionTriggered
expr: rate(sentinel_system_block_qps[1m]) > 0
for: 1m
annotations:
summary: "{{ $labels.instance }} 系统保护已触发,机器整体过载"
# 5. 热点参数限流命中(防刷生效,也可能误伤)
- alert: HotParamBlockedHigh
expr: rate(sentinel_param_block_qps[1m]) > 500
for: 2m
annotations:
summary: "{{ $labels.resource }} 热点限流 > 500/s,确认是防刷还是误伤"
优化效果
指标 治理前 治理后
=============================================================
峰值承接 QPS 21 万(崩溃) 网关挡掉,业务侧 2 万
黑产脚本流量 畅通无阻 热点限流拦掉 99%
下游风控变慢时 线程全卡死 熔断 + 降级,链路不雪崩
正常用户下单成功率 37% 99.2%
服务过载保护 无 系统规则自适应兜底
活动期间宕机 整体崩溃 零宕机
降级预案 无 风控降级 + 友好提示
压测(模拟 25 万 QPS,含 80% 刷子流量):
- 治理前:3 分钟服务崩溃
- 治理后:刷子被网关 + 热点限流逐层削掉,业务侧平稳运行
正常用户 P99 80ms,无失败
接入与改造:
- 接入 Sentinel + Dashboard:1 天
- 流控 + 熔断规则梳理与配置:2 天
- 热点参数限流 + 防刷:1 天
- 网关流控 + 系统保护:1 天
- 全链路压测(故障 + 刷子注入):2 天
避坑清单
- 核心接口必须配 QPS 流控,阈值由压测得出,绝不"来多少收多少"
- 三种控制效果分清:稳定容量用直接拒绝,冷启动用 Warm Up,削峰用排队
- blockHandler 处理限流/熔断 Block,fallback 处理业务异常,别混用
- 熔断优先用"慢调用比例",配 minRequestAmount 防低流量误判
- 防刷靠热点参数限流,对单 userId/itemId 限流,接口级限流挡不住分散刷
- 热点规则给 VIP/内部账号配例外,避免误伤正常大客户
- 系统自适应保护是兜底,不需算准容量,过载自动拦,务必开启
- 限流尽量前移到网关,流量不进业务服务,保护更彻底
- 每个降级 fallback 都要有真实的兜底逻辑,不能只是抛异常
- 限流量、熔断状态、系统保护触发都要上监控,区分防刷生效与误伤
总结
这次秒杀被刷崩的事故,让我对"限流"这件事的认识彻底变了。过去总觉得限流是个可有可无的"锦上添花",真正想明白后才发现:它是高并发系统的底线防御,没有它,系统就是不设防的城池,一波超出容量的流量——无论来自真实用户还是黑产——都能轻易把它推倒。最大的认知改变是理解了限流的层次性:它不是配一条 QPS 规则就完事,而是一套从粗到细、从外到内的纵深防御。网关层做总量管控,把洪峰挡在业务系统之外;接口层做 QPS 流控,守住单个资源的容量红线;热点参数限流做精准防刷,锁定那些"单个用户、单个商品刷得太猛"的异常值——这一招最关键,因为接口级限流对"分散成海量请求的刷子"无能为力,而热点限流能精准把单个账号摁到 5 QPS,正常用户却毫无感知;最后系统自适应保护兜底,不用你算准容量,机器真扛不住时自动拦。第二个深刻的体会是熔断和降级是一体两面:熔断负责"快速失败",当下游变慢时果断切断,避免线程全部堆积在等待上引发雪崩;降级负责"优雅失败",熔断之后不能让用户看到白屏或 500,而要返回有意义的兜底——小额订单自动放行、大额转人工审核,这才是对业务负责。归根结底,流量治理的哲学是承认"系统容量有限"这个事实,与其在过载时整体崩溃、所有人都用不了,不如主动拒绝一部分、保住核心链路让大多数人可用。能限流的系统才是能在大促活下来的系统。
—— 别看了 · 2026