上周五晚上九点,我正在家给孩子讲睡前故事,手机开始疯狂震动。
监控群里刷屏:库存服务接口超时率90%,数据库连接池打满,下游仓储服务开始报错。领导电话直接打过来:"秒杀活动提前上线了,快看下!"
我打开电脑的功夫,库存服务已经挂了,其他依赖它的服务也开始连锁反应。那一刻我意识到,光有熔断限流的代码还不够,你得能快速、安全地把规则改上去,还得改完立刻生效。
这篇把我这次事故的处理过程、以及后来落地的Sentinel规则配置化方案完整写出来。血泪教训,希望你别遇到。
事故复盘:为什么秒杀一开始就扛不住
这次秒杀是临时提前上线的,流量预估严重低估。平时几百QPS的接口,瞬间涌进来几万QPS。
我们的库存服务之前做了一部分限流,用的Sentinel,但规则是写在代码里的硬编码:
java
// 硬编码限流:QPS超过1000直接拒绝
FlowRule rule = new FlowRule();
rule.setResource("stock:deduct");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1000);
FlowRuleManager.loadRules(Collections.singletonList(rule));
问题就出在这:规则写在代码里,意味着改规则=改代码=发版。事故发生时,我想把限流从1000调到500,最快的路径居然是重新打包、构建、发版------整个过程至少10分钟。而事故现场,10分钟足够让一个服务从"告警"变成"挂掉"。
更麻烦的是,代码里硬编码规则还有个隐患:规则和业务代码耦合,不同环境(测试/生产)用的同一套规则,没法差异化配置。
Sentinel规则配置化的核心思路
Sentinel本身是支持动态规则的,通过SentinelProperty和WritableDataSource实现。核心思路是:规则不写在代码里,而是存在配置中心(Nacos),运行期监听变化,实时加载。
这样改规则只需要去Nacos改配置,秒级生效,不用发版。
第一步:接Nacos动态数据源
java
@Configuration
public class SentinelNacosConfig {
@Value("${nacos.server-addr}")
private String serverAddr;
@Bean
public DataSource<String> flowRuleDataSource() {
// 监听Nacos上"sentinel-flow-rules"这个配置
return new NacosDataSource<>(serverAddr, "SENTINEL_GROUP",
"sentinel-flow-rules",
source -> JSON.parseArray(source, FlowRule.class));
}
}
关键在NacosDataSource:它会在启动时拉一次配置,然后监听Nacos配置变化,一旦修改就推送新的规则给Sentinel,整个过程不用重启、不用发版。
第二步:在Nacos里配置规则
规则内容就是一个JSON数组,放在Nacos配置中心:
json
[
{
"resource": "stock:deduct",
"grade": 1,
"count": 1000,
"limitApp": "default",
"strategy": 0,
"controlBehavior": 0
}
]
resource:要保护的资源名grade:限流维度,1=QPS,0=并发线程数count:阈值controlBehavior:流量整形方式,0=直接拒绝,1=Warm Up,2=匀速排队
事故当晚,我就是直接在Nacos里把count从1000改成500,配置保存的瞬间规则就生效了,数据库压力肉眼可见地降下来。整个过程不到1分钟。
不只是限流:熔断和降级也要配置化
限流只是第一道防线。这次事故里,库存服务挂了之后,下游仓储服务还在拼命重试,把仓储也拖垮了。这就要靠熔断。
Sentinel的熔断规则同样可以走Nacos:
json
[
{
"resource": "warehouse:ship",
"grade": 2,
"count": 50,
"timeWindow": 30,
"minRequestAmount": 10,
"statIntervalMs": 1000
}
]
grade: 2:按慢调用比例熔断count: 50:平均响应时间超过50ms就算慢调用timeWindow: 30:熔断后30秒内直接拒绝,不再调用下游minRequestAmount:最少请求数,避免请求太少时误判
熔断生效后,库存服务虽然还在报错,但错误不再向下游蔓延。仓储服务缓过来了,整个链路没有全挂。
降级规则也类似。比如秒杀场景,可以先返回一个"系统繁忙"的降级响应,而不是让用户一直转圈:
java
@SentinelResource(value = "stock:deduct",
blockHandler = "blockHandler",
fallback = "fallback")
public boolean deductStock(Long skuId, Integer count) {
// 真正的扣库存逻辑
return stockService.deduct(skuId, count);
}
// 限流/熔断时的兜底
public boolean blockHandler(Long skuId, Integer count, BlockException e) {
log.warn("stock deduct blocked, skuId={}", skuId);
return false; // 返回"库存扣减失败",前端提示稍后重试
}
// 业务异常时的兜底
public boolean fallback(Long skuId, Integer count, Throwable t) {
log.error("stock deduct fallback, skuId={}", skuId, t);
return false;
}
事故当天的完整时间线
复盘一下事故当天的处理,你就知道配置化有多救命:
- 21:00 告警刷屏,库存服务超时率90%
- 21:02 我打开电脑,确认是秒杀流量远超预估
- 21:04 Nacos里把
stock:deduct的限流从1000改到500,30秒生效 - 21:06 观察监控,超时率开始下降,但下游仓储还在告警
- 21:08 给
warehouse:ship加熔断规则,隔离故障 - 21:15 系统恢复稳定,超时率降到5%以内,秒杀正常进行
如果规则是硬编码的,21:02到21:15这13分钟里,我大部分时间都在等发版。而配置化之后,我全程没有打一行代码、没有一次发版。
落地配置化时踩的坑
方案落地过程中,有几个坑值得说。
坑1:Nacos挂了怎么办。 有同事担心"规则全在Nacos,Nacos挂了规则不就丢了?"其实Sentinel有本地兜底:规则加载后会缓存在本地内存,Nacos挂了,已加载的规则依然生效。 只是改不了规则了,但不会导致系统失去保护。
坑2:规则格式错误,静默失败。 NacosDataSource解析规则时,如果JSON格式错误,不会报错,规则直接不生效。这是最危险的------你以为有保护,实际裸奔。排查方法:看应用日志里有没有"fail to load"之类的字样,或者用Sentinel控制台确认规则是否加载成功。
坑3:规则太多太散,管理混乱。 规则都放Nacos之后,如果没有规范,很快会变成一锅粥。我们后来做了个简单的治理:每个服务的规则放在单独的Nacos配置里,命名规范服务名-sentinel-rules,并在文档里登记每套规则的作用和阈值依据。
结尾
这次事故给我的最大教训不是"限流要配多少",而是:治理能力比治理规则本身更重要。 规则写得再好,改起来要发版,关键时刻就是废的。
现在我们的Sentinel规则全部配置化,改限流、调熔断、加降级,都是改配置的事,秒级生效。团队的应急响应能力上了一个台阶。
另外,配置化之后我还建议把规则变更记录到日志里,出了事能追溯"是谁在什么时候改了什么规则"。这个我们正在做,后面有进展再写。
如果这篇对你有用,点个关注。下一篇我想写分库分表------去年我们订单表拆库的经历,当时没想清楚的三件事,后面全变成坑了。