1. 从一个线上事故说起:为什么需要熔断、降级、限流
做了几年微服务,最怕的不是新需求,而是线上接口突然堵死的那几分钟。之前我们有个订单查询接口,平时毫秒级响应,结果一次大促预热,上游把一批用户数据推送过来,直接打满了数据库连接池。数据库连接拿不到,SQL越积越多,Tomcat线程被一个个卡在等待上。等我把日志打开的时候,线程栈里全都是waiting for connection,下游全部调用开始跟着超时,最后连不相关的用户登录接口也一起变慢,容器健康检查失败,服务被编排平台反复重启,整个链路雪崩。
那次事故之后我们才认真把高可用的三件套补齐:限流、熔断、降级。这三样东西不是同一个层面的解决方案,限流是把冲进来的洪水挡在外面,熔断是发现下游已经不行了赶紧停手,降级是就算真的失败了也给用户一个还能接受的结果。它们单独拎出来任何一环都有短板,只有组合在一起,微服务的入口、链路和兜底才算是完整闭环。
这篇文章就围绕这三个核心点,从设计思路、参数配置、代码实现,到真实的排查经验,完整走一遍。适合正在做微服务改造的团队,尤其是那些已经上了Spring Cloud但还没有把保护机制真正落地的项目。如果你被线上雪崩吓过一次,看完这篇文章应该会有比较强的共鸣。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计思路:三种手段的定位与配合
2.1 限流:守住入口,别让流量冲进来
限流做的是“自我防御”。不管下游多健壮,单机或集群的处理能力总有一个天花板。限流就是在这个天花板前面画一条红线,流量到了红线就拦截,让系统始终运行在可控水位上。
常见的算法有令牌桶、漏桶和滑动窗口。令牌桶允许一定的突发流量,因为桶里攒了令牌,冲进来一波也能被放行一部分;漏桶则把请求排成队,处理速度恒定,很像高速收费站一根车道慢慢放车。还有一个容易混淆的概念是滑动窗口,它把时间切成小格子,统计每个格子内的请求数,用来精确控制“每分钟最多6000次”这类窗口级QPS。
在实际系统里,我一般会在两个位置做限流:第一层是网关,按IP、用户ID、接口路径去限,挡住明显异常的流量;第二层是微服务内部的业务方法,按某个核心资源去限,比如“根据订单号查询详情”这个操作。网关层挡不了所有情况,比如两个服务之间互相调用,流量不经过网关,就必须在服务内部有保护。
2.2 熔断:发现下游不行了,及时止损
熔断是面向依赖的“快速失败机制”。它的原型是电路断路器:正常的时候开关闭合,电流畅流;一旦发生故障,开关弹开,后续请求直接走失败分支,不再去触碰故障点。
微服务里的熔断器有三种状态:关闭、打开、半开。关闭时请求正常发往下游;当错误率或慢调用比例超过阈值,熔断器打开,后续请求直接短路,不发起真实调用;过一段时间进入半开状态,放几个探针流量过去试试水温,如果成功了就慢慢恢复关闭,如果还是失败就继续打开。这个机制保证了故障点不会被持续压垮,也给了系统恢复的时间。
很多人觉得超时就能解决一切,但超时只是“等了一会儿再报错”。如果每秒进来1000个请求,每个要等3秒才超时,这3秒内线程、内存、连接全部被占着,资源池很快耗尽。熔断的意义在于失败得足够快,把资源留给还能处理的请求。
2.3 降级:兜底给用户一个可接受的结果
降级是“损失一部分功能,保住核心体验”。它可以是服务层面的,比如推荐接口挂了,就返回一个默认的热门商品列表;可以是数据层面的,比如最新评论查不到,就返回Redis里的缓存数据;也可以是页面层面的,比如下单按钮置灰,提示“活动火爆,请稍后再试”。
降级和熔断经常混在一起说。我的经验是:熔断是触发条件,降级是处理动作。熔断器打开之后,请求走降级逻辑,返回兜底结果。降级逻辑不能随意写,得根据业务场景决定。比如查询类操作,返回缓存或者空数据问题不大;写操作如果降级,就得考虑会不会造成重复下单、数据不一致,这些都要用幂等、状态机、异步补偿去做配合。
2.4 高可用框架选型:我为什么最终选了Sentinel
最早我们用Hystrix,那时候它确实是事实标准,但后来停止维护了,而且它的监控页做得比较简陋,动态修改规则也不方便。Resilience4j更轻量,功能也够,不过需要自己搭控制台和规则持久化,落地成本偏高。后来换成了Sentinel,它在国内社区活跃度高,跟Spring Cloud Alibaba集成很顺,Dashboard开箱即用,各种流量控制、熔断降级策略比Hystrix丰富很多。
不过选型不能只看名气,还要看团队熟不熟。如果你们本来就用了Resilience4j且跑得好好的,没必要强行换。但如果是从零开始搭微服务保护体系,我推荐直接上Sentinel,后面所有的实操步骤也会以Sentinel为例来讲。
3. 核心参数与规则配置实战
3.1 限流规则怎么定:QPS、并发数、令牌桶/漏桶
先分清两种限流维度:QPS(每秒请求数)和并发线程数。QPS适合做入口流量控制,直接限制请求速率;并发线程数适合保护资源,比如数据库连接池只有50个连接,那就限制最多50个线程同时访问这个资源。
在Sentinel里定义一条限流规则,核心属性有:resource(资源名)、grade(限流维度)、count(阈值)、controlBehavior(流量效果)。常见阈值不能拍脑袋拍出来,我是靠压测得出来的:先不限额,用压测工具把接口打到CPU使用率70%~80%左右,记下对应的QPS,再乘以一个0.7~0.8的安全系数,就是比较合理的阈值。
流量效果有三种常用模式。快速失败就是超了的请求直接拒绝;Warm Up是让阈值从低水平慢慢爬升到设定值,适合系统冷启动,防止瞬间高流量压垮JVM;匀速排队是让请求以一个固定间隔排队通过,适合削峰填谷,比如一个秒杀接口,流量再猛我也只让每秒进100个,其他请求排队等待。
3.2 熔断规则怎么配:慢调用比例、异常比例、异常数
Sentinel的熔断策略有三种:慢调用比例、异常比例、异常数。
慢调用比例:统计时长内,如果请求的RT大于设定的慢调用阈值,并且这些慢请求占比超过比例阈值,就触发熔断。比如RT阈值800ms,比例阈值0.5,统计时长1秒,最小请求数10,那么在1秒内至少有10个请求、其中超过一半响应时间大于800ms,熔断器就打开。
异常比例:统计时长内,如果异常请求数占总请求数的比例超过阈值,就熔断。适合接口本身没有明显变慢,但业务异常大量抛出的情况。
异常数:统计时长内异常数量直接达到某值就熔断,适合异常量突然暴增的场景。
熔断时长也很关键,别配太短,否则下游没恢复又被打进去;也别配太长,影响用户体验。一般先配5秒或10秒,观察恢复速度再调。
3.3 降级逻辑怎么写:Fallback 与业务兜底
在Sentinel里,@SentinelResource注解可以同时配置blockHandler和fallback,这两个很多人分不清。
blockHandler处理的是Sentinel触发的BlockException,比如请求被限流了、被熔断了,会走到这里。fallback处理的是业务方法本身抛出的异常,比如空指针、下游HTTP调用返回错误。规范写法的示例:
java复制@SentinelResource(
value = "orderQuery",
blockHandler = "queryBlockHandler",
fallback = "queryFallback"
)
public OrderVO queryOrder(String orderId) {
// 调用下游订单服务
return orderServiceClient.query(orderId);
}
public OrderVO queryBlockHandler(String orderId, BlockException ex) {
// 限流/熔断兜底
return buildDefaultOrder(orderId);
}
public OrderVO queryFallback(String orderId, Throwable t) {
// 业务异常兜底
return buildCacheOrder(orderId);
}
这里有一个常见的坑:fallback方法的返回值、参数列表要和原方法匹配,参数后面加一个Throwable参数;blockHandler则需要加一个BlockException参数。方法写错会导致启动时报错或者兜底不生效。
3.4 结合Spring Cloud Gateway做入口限流
网关限流一般是第一道防线。我用过Gateway自带的RequestRateLimiter,基于Redis配合令牌桶算法。配置里核心是redis-rate-limiter.replenishRate(每秒令牌补充速度)和burstCapacity(桶容量)。举个配置片段:
yaml复制spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
key-resolver: "#{@ipKeyResolver}"
还需要定义一个KeyResolver,按IP区分用户:
java复制@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
网关层的限流能做到统一的入口保护,但集群中每个节点是独立的,Redis保存的是全局计数。如果你用Sentinel网关适配,也可以达到类似效果,并且有更好的控制台支持。入口限流不能替代业务方法限流,毕竟内部服务之间的调用也有突发。
4. 实际操作中的完整流程(基于Sentinel + Spring Cloud)
4.1 基础设施准备:Nacos、Gateway、业务服务
我自己常用的组合是Spring Cloud Alibaba + Nacos + Sentinel。Nacos既当注册中心又当配置中心,Gateway作为流量入口,订单服务、用户服务作为业务服务。版本选择上有一个很关键的点:Spring Boot 2.7以前和Spring Boot 3.x对应的Spring Cloud Alibaba版本完全不一样,别看错依赖。
先启动Nacos Server,默认端口8848,然后搭建Gateway服务,把路由配置指向下游服务。Gateway本身也要注册到Nacos,这样才能通过lb://调用实例。这个过程没什么黑科技,但版本匹配容易踩坑,我建议用Spring Cloud Alibaba官方维护的版本依赖,别自己把不同版本的starter混着引。
4.2 引入依赖并配置控制台
在业务服务的pom.xml里引入:
xml复制<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
然后在application.yml里配置Sentinel Dashboard的地址:
yaml复制spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719
eager: true
eager设为true是为了让服务启动时就主动连接控制台,否则只有第一次请求过来才会注册。Sentinel Dashboard是单独的web应用,从GitHub下载release包,用java -jar启动就行。打开控制台后,你会看到每一个注册上来的服务,以及它们的方法调用流量。
4.3 用@SentinelResource保护核心接口
在业务代码里,对最核心的查询下单接口加保护。前面已经演示过注解的写法,这里再补充一个细节:如果不想侵入业务代码,也可以用Sentinel的 AspectJ 方式或者统一在Controller入口封装,但团队在快速迭代期,注解可能是最可控的方式。
接入后先用一个简单的JMeter脚本,或者Postman的Runner功能,快速打几秒流量。打开Sentinel Dashboard的“实时监控”,会看到每个资源的QPS曲线。曲线如果能正常显示,说明数据链路是通的;如果监控里看不到,多半是服务没配好transport端口,或者本地防火墙拦了8719。
4.4 控制台动态调整规则
在Dashboard左侧菜单选“流控规则”,新增一条规则,填写资源名、阈值、模式。保存后规则会实时下发到应用JVM里,不需要重启。熔断规则类似,在“熔断规则”菜单里新建。
这里必须说一个坑:Dashboard把规则保存在内存里,服务重启规则就没了。生产环境一定要把规则持久化到Nacos,或者通过Sentinel的DataSource接口从配置中心拉取。我一般用Nacos作为规则持久化,每次改规则就改Nacos配置,Sentinel客户端监听到变更后自动刷新。这样做的另一个好处是可以结合发布流程做规则评审,改规则也走Git。
5. 常见问题与排查技巧实录
5.1 熔断恢复后仍有大量请求异常,怎么回事
有次我们配置了异常比例熔断,下游依赖恢复后,熔断器进入了半开状态,但系统依然有大量请求报错。打开日志发现,半开状态只放了少量试探请求,这些请求全打到了刚刚苏醒的下游实例上,但下游实例的数据库连接池还没预热,结果试探请求继续超时,熔断器又弹回打开状态,形成了“反复震荡”。
解决方法是两个方向:一个是把熔断时长的窗口调大,给下游足够的恢复时间;另一个是检查下游健康检查逻辑,确保实例注册到Nacos前,业务依赖的资源已经准备好。另外,半开状态下放行的请求数量阈值可以调大一点,比如从10调到20,否则恢复速度太慢。
5.2 限流阈值设置不合理导致误杀
限流阈值设置太死,容易出现正常的用户被误杀。最常见的是按IP限流时,公司出口IP是同一个NAT地址,所有人看起来都是同一个IP,结果某个同事刷了一个页面,全公司都被限流了。这种情况建议不要只按IP KeyResolver,而是按“用户ID + 接口路径”组合限流。
还有一次我们用了快速失败模式,但业务本身有瞬时高TPS的规律。每秒前500个请求是正常的,之后才会有忙闲波动,结果因为阈值设成300,高峰期大量请求被拒。后来改成匀速排队模式,把排队超时控制在200毫秒以内,用户感知几乎为零,误杀问题就没了。
5.3 降级后数据不一致风险
降级最怕的是“假成功”。比如库存扣减接口超时,我们降级返回“扣减成功”,实际库存服务可能并没有扣成功,最终导致超卖;反过来,如果返回“扣减失败”,实际上扣了,用户会重复下单。
我建议对写入类接口尽量少用静默降级,改成异步重试 + 幂等号方案。每个请求带上全局唯一的幂等ID,下游接收到重复请求直接返回之前的处理结果。查询类接口降级则优先取缓存或默认值,宁可给旧数据,也不能直接抛异常。
5.4 热词里的坑:JDK版本问题与降级
技术圈的热词有时也能帮我们避坑。比如搜索“jdk降级到17”,说明不少人在升级JDK时遇到兼容性问题。我们当时把服务从JDK8升到JDK17时,Sentinel 1.8.x在反射和模块访问上就出过问题,导致规则下发不生效。后来换到适配JDK17的新版本才稳定。
我的建议是:除非新版本框架明确兼容,否则不要为了“用新”去升级。高可用的第一原则是稳定,运行环境、依赖版本都是系统的一部分。如果确实需要升级,先在一组边缘服务上灰度运行,观察监控指标再全面推。
6. 把高可用做成一种习惯
如果说有什么切身的建议,那就是不要等到线上挂了再补这些机制。先给核心接口配上限流,再给依赖链路配上熔断,最后把降级逻辑一个接口一个接口地补,这个过程不需要花几天,但能换来很多个安稳的夜晚。
我还有一个习惯:每次大促或发布新功能前,会专门做一次“故障演练”。把下游服务手动关停,看熔断器会不会按预期打开;把流量拉高到上限两倍,看限流是否真的在保护系统;再把缓存清空,看降级逻辑能不能扛住。这些演练不会完全模拟真实故障,但能暴露最明显的配置错误。
高可用不是搭好一个框架就算结束,规则阈值、兜底逻辑、持久化方案、监控告警,每一环都需要持续调优。碰到问题也不要慌,按照限流、熔断、降级这条链路去拆,大部分故障都能找到对应的处理入口。
