一次秒杀把服务打挂了,我用Sentinel规则配置化救了回来

上周五晚上九点,我正在家给孩子讲睡前故事,手机开始疯狂震动。

监控群里刷屏:库存服务接口超时率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本身是支持动态规则的,通过SentinelPropertyWritableDataSource实现。核心思路是:规则不写在代码里,而是存在配置中心(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:08warehouse: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规则全部配置化,改限流、调熔断、加降级,都是改配置的事,秒级生效。团队的应急响应能力上了一个台阶。

另外,配置化之后我还建议把规则变更记录到日志里,出了事能追溯"是谁在什么时候改了什么规则"。这个我们正在做,后面有进展再写。

如果这篇对你有用,点个关注。下一篇我想写分库分表------去年我们订单表拆库的经历,当时没想清楚的三件事,后面全变成坑了。

相关推荐
Zane199415 分钟前
明明没删字段,反序列化却报错:都是隐式 serialVersionUID 惹的祸
java·后端
0xBADCODE17 分钟前
动态DEX加载+反射+DES硬编码密钥:安卓三层逆向实战
android·java·python·安全·网络安全·逆向·ctf
Gopher_HBo17 分钟前
beego启动流程
后端
fliter19 分钟前
Go Map 详解:键值对实际上是如何存储的
后端
万物智能19 分钟前
设备树DTS-【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
后端·算法
被摘下的星星21 分钟前
Java 中 .length、.length() 以及 .size() 的区分
java·开发语言
CadeCode22 分钟前
Oracle 逗号拼接字段处理
数据库·后端·性能优化
代码不停26 分钟前
子数组问题
java·算法
geovindu34 分钟前
CSharp: State Pattern
开发语言·后端·c#·.net·状态模式·行为模式