☰
Spring Cloud Gateway+Sentinel网关限流熔断实战
2026/10/26 7:02:58 网站建设 项目流程

在线程池里给网关做限流和熔断,很多人第一反应是上组件、拉配置中心、写一堆规则,最后发现真正跑起来的时候,日志里全是超时。这个项目是我在做一套分布式电商练手工程时顺手落下来的限流熔断小模块,标题叫网关限流熔断组件的实战落地。它解决的问题很具体:订单、商品、库存这些服务被前端和第三方同时打,任何一个慢接口都能把整条链路拖垮,而我又不想在每个业务代码里到处塞 try-catch 和计数器。适合已经写过 Spring Cloud 基础整合、手上有过一两个微服务项目、但还没真正把 Sentinel 或 Resilience4j 用进生产链路的人看。下面这些内容,全部基于我自己搭的那套环境,代码可以直接抄,坑我也替你踩过一遍。

1. 项目整体设计与思路拆解

1.1 为什么限流不能塞在业务代码里

我最早的做法非常朴素,在订单服务的下单接口里写了个 AtomicInteger 计数器,超过阈值直接返回"系统繁忙"。跑了两天就发现三个问题:第一,计数器是进程内的,部署三个实例就是三份独立计数,实际放行量是阈值的三倍;第二,慢接口拖住的是 Tomcat 线程,计数再准也没用,线程池照样被打满;第三,每加一个接口就要复制一遍这段逻辑,业务代码被污染得没法看。

限流的本质是在请求进入真正耗资源的业务逻辑之前,用极低的成本做一次准入判断。这个判断必须发生在离入口最近的地方,也就是网关层。网关是所有流量的必经之路,它天然知道请求从哪来、打到哪个服务、当前 QPS 是多少。把限流放在这里,业务服务只需要关心自己的业务,不用为流量治理操心。

熔断则解决另一个问题:下游已经挂了,上游还在傻等。库存服务雪崩的时候,订单服务每来一个请求就卡三秒超时,三秒钟内线程全被占住,订单服务自己也被拖死。熔断器的作用就是在检测到下游失败率过高时,快速失败,直接把请求挡回去,给下游喘息的时间。这套逻辑放在网关,能覆盖所有经过网关的调用,比在每个 FeignClient 上单独配置省心得多。

1.2 组件选型的三个真实考量

市面上主流方案就两个:Sentinel 和 Resilience4j,前者是阿里系,后者是社区方案。我最终选了 Sentinel,理由不是"功能强",而是三个很具体的点。

第一个是控制台。Sentinel 自带一个 Dashboard,规则可以在界面上改、实时生效,做压测的时候我不用反复重启服务改配置文件。Resilience4j 的规则主要是写在 yml 或者代码里,动态调整要自己接配置中心。

第二个是规则维度的丰富度。Sentinel 除了 QPS 限流,还支持并发线程数限流、热点参数限流、熔断降级、系统自适应保护。热点参数限流这个对我很有用——同一个商品详情接口,热门商品和冷门商品的 QPS 相差几百倍,我可以只针对特定参数值单独限流,而不是一刀切。

第三个恰恰是它的缺点,我得说清楚:Sentinel 的规则默认存在内存里,服务重启就没了,必须配合 Nacos 之类的持久化。而且它的网关适配模块(sentinel-spring-cloud-gateway-adapter)在版本升级时 API 变过几次,容易踩坑。如果你的团队对阿里系组件有顾虑,Resilience4j 也完全够用,只是动态规则要自己动手。

对比项SentinelResilience4j
规则动态调整内置 Dashboard,改完即生效需自行对接配置中心
规则持久化需对接 Nacos/Redis 等天然在配置文件里
热点参数限流原生支持不支持,需自行实现
熔断模型慢调用比例、异常比例、异常数失败率、慢调用
网关适配官方 adapter,版本耦合较强需手写 GlobalFilter
学习成本中等,概念略多较低,API 简洁

1.3 整体架构分层

整个模块我按三层来拆:规则层、执行层、观测层。

规则层负责规则的存储和推送。规则最终存在 Nacos 的一个配置项里,网关启动时拉取,同时在 Sentinel Dashboard 里做的修改会通过 Nacos 数据源回写到配置项,这样服务重启后规则还在。这一层的关键是选对InitFunc,Sentinel 提供了好几种数据源实现,我用的NacosDataSource。

执行层就是网关里的过滤器。Spring Cloud Gateway 基于 WebFlux,所有过滤器都是异步非阻塞的,Sentinel 提供的SentinelGatewayFilter正好符合这个模型。我只需要在配置类里把资源和规则注册进去,过滤器链会自动拦截。

观测层是我加的一点私货。Sentinel 的BlockException默认返回一个很丑的 JSON,我给每种限流原因配了不同的响应体和 HTTP 状态码,同时在拦截的时候把 traceId、资源名、被限制的 QPS 都打到日志里。这块不做的话,线上出问题你只知道"被限流了",不知道为什么被限。

2. 核心细节解析与实操要点

2.1 资源粒度到底怎么定

这是最容易拍脑袋的地方。Sentinel 里一切限流都围绕"资源"展开,资源定得太粗,一个慢接口把整个服务限死;定得太细,规则多到没法维护。

我的做法是按"接口路径前缀 + HTTP 方法"定资源。比如/api/order/**是一类资源,/api/product/{id}是另一类。这样做的理由是网关层拿不到具体的业务方法名,只能拿 URL 和方法,而 URL 恰好反映了业务语义。

更细一层的做法是用UrlCleaner把路径参数清洗掉。Spring Cloud Gateway 的路径/api/product/10086和/api/product/10087如果不做清洗,会变成两个不同的资源,规则根本没法配。SentinelGatewayConfig里可以设setUrlCleaner,用正则把数字段替换掉。这个细节不做,限流规则永远命中不了你想要的接口。

注意:资源名清洗要在 Sentinel 初始化之前完成,顺序错了清洗不生效,而且不会报错,只是规则静默失效。

2.2 限流阈值怎么算出来

阈值绝对不能拍脑袋写个 1000。我的计算路径是这样的:先用压测拿到单实例在 P99 响应时间可接受时的最大吞吐,比如订单查询接口单实例 400 QPS;再乘以实例数,假设 4 个实例就是 1600 QPS;然后打个 0.7 的折扣,因为网关本身还有开销,也会受网络抖动影响,最终定 1100 左右。

还有一个维度是线程数限流。QPS 限流管的是速率,但如果下游接口是慢接口,每秒 200 个请求每个耗 1 秒,瞬时就会占满 200 个线程。所以对已知的慢接口,我会额外配一条并发线程数规则,阈值设在连接池或业务线程池的六成左右。

这两条规则是并存的,哪条先触发就算哪条。实测下来,突发流量往往是线程数先顶不住,平稳的高压才是 QPS 先触发。

2.3 熔断规则的三个关键参数

熔断降级我配的是慢调用比例模型,因为它比异常比例更贴近真实场景——很多下游慢而不报错,异常率是 0,但线程全被拖住,这比报错还致命。

三个参数需要一起调:

  • 慢调用 RT 阈值:超过这个时间的请求算慢调用。我一般设成 P99 的 1.5 倍。订单查询 P99 是 300ms,阈值就设 450ms。
  • 熔断触发比例:一个统计窗口内慢调用占比超过多少就熔断。我设 0.5,也就是一半以上请求都慢,那这个服务基本不可用了,没必要再放流量进去。
  • 熔断时长:熔断后多久进入半开状态试探。设太短,下游还没恢复又被压垮;设太长,下游恢复了流量也进不来。我一般从 10 秒起步,逐步调大。

半开状态这个设计很值得说一句。熔断时间到了之后,Sentinel 会放一个请求过去试探,如果这次成功了就关闭熔断,失败的继续熔断。所以它不是一个"死循环",能自己恢复。

2.4 网关适配模块的接入方式

这里有个坑必须提前讲。Sentinel 的网关适配有两种用法:一种是引入spring-cloud-alibaba-sentinel-gateway,它会自动配置;另一种是手动引入sentinel-spring-cloud-gateway-adapter,自己写配置类。

我推荐后者,理由是可控。自动配置的版本和你项目里 Spring Cloud Gateway 的版本不一致的时候,会出现过滤器注册顺序错乱、BlockExceptionHandler不生效等一堆玄学问题。手动配置虽然多写三十行代码,但每一行你都清楚在干什么。

手动配置的核心是三个 Bean:SentinelGatewayFilter、SentinelGatewayBlockExceptionHandler、GatewayRuleManager的规则加载。前两个负责拦截和兜底响应,第三个负责规则。少一个都不行。

提示:SentinelGatewayBlockExceptionHandler的排序要设成Ordered.HIGHEST_PRECEDENCE,否则会被其他异常处理器抢先,限流响应变成默认的 500 页面。

3. 实操过程与核心环节实现

3.1 依赖引入与版本对齐

我的环境是 Spring Boot 2.7.x、Spring Cloud 2021.0.x、Spring Cloud Alibaba 2021.0.5.0。这三个版本必须成套,混搭必出问题。

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>

第二个依赖的版本不用自己写,Spring Cloud Alibaba 的 BOM 已经管好了。第三个是规则持久化用的,不加的话规则只在内存里,重启就没了。

对应的配置项:

spring: cloud: sentinel: transport: dashboard: 127.0.0.1:8858 port: 8719 eager: true datasource: gw-flow: nacos: server-addr: 127.0.0.1:8848 >[ { "apiName": "order-query", "predicateItems": [ { "pattern": "/api/order/detail", "matchStrategy": 0 }, { "pattern": "/api/order/list", "matchStrategy": 0 } ] }, { "apiName": "product-query", "predicateItems": [ { "pattern": "/api/product/**", "matchStrategy": 2 } ] } ]

matchStrategy三个取值:0 是精确匹配,1 是前缀匹配,2 是正则匹配。用正则的时候路径里的**要转成.*,这个转换 Sentinel 不会帮你做,写错了规则就是全部不命中。

流量规则对应的结构:

[ { "resource": "order-query", "resourceMode": 0, "grade": 1, "count": 1100, "intervalSec": 1, "controlBehavior": 0, "burst": 0, "maxQueueingTimeMs": 500 } ]

resourceMode为 0 表示按 routeId 限流,为 1 表示按 API 分组限流。我这里用的是 1,也就是按分组名匹配。grade为 1 是 QPS 模式,为 0 是线程数模式。controlBehavior为 0 是快速失败,为 1 是预热,为 2 是排队等待,为 3 是预热加排队。

3.3 引流量的预热模式

这里单独说一下预热模式,因为它解决了一个很现实的问题:服务刚发布的时候缓存是空的,直接放满流量会把数据库打穿。

预热模式的原理是冷启动时阈值从count / coldFactor开始,在设定的预热时长内逐步升到 count。coldFactor默认是 3,也就是说初始阈值是目标值的三分之一。我在商品详情接口上开了这个模式,目标 800 QPS,预热 60 秒,效果是发布后第一分钟内实际只放 260 到 800 之间递增的量,数据库的慢查询日志一次没爆过。

配置上就是controlBehavior: 1,配合warmUpPeriodSec字段。值得一提的部分是coldFactor不能随便调小,调到 2 以下预热就失去了意义,因为它和目标的差距不够大。

3.4 熔断降级与兜底返回值

熔断规则的 JSON 结构稍微不一样:

[ { "resource": "order-query", "grade": 1, "count": 450, "timeWindow": 10, "minRequestAmount": 20, "slowRatioThreshold": 0.5, "statIntervalMs": 10000 } ]

grade为 1 是慢调用比例模型,count在慢调用模型里代表最大 RT(毫秒),在异常比例模型里代表比例。minRequestAmount是最小请求数,低于这个数不做熔断判断,防止刚启动时被一两个慢请求误判。statIntervalMs是统计窗口,10 秒。

兜底返回值我改了两处。第一处是把默认的 500 换成 429 和对应业务码,前端拿到响应能直接提示"请求过于频繁,请稍后再试"。第二处是在响应体里带上blockedBy字段,标注是被流量规则还是降级规则挡下来的,排查问题的时候能省很多时间。

@Component public class GatewayBlockHandler implements BlockRequestHandler { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) { Map<String, Object> body = new HashMap<>(4); body.put("code", 429); body.put("msg", "当前访问人数较多,请稍后重试"); body.put("path", exchange.getRequest().getPath().value()); body.put("blockedBy", t.getClass().getSimpleName()); return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .bodyValue(body); } }

blockedBy这里有一点要注意,它拿到的是异常类名,ParamFlowException、FlowException、DegradeException分别对应热点限流、普通限流、熔断降级。做日志聚合的时候按这个字段分组,能很快看出来到底是哪类规则在起作用。

3.5 用 JMeter 验证规则真的生效

代码写完不算完,必须有验证手段。我用的方案是 JMeter 起 300 个线程持续压一分钟,观察三个指标:Sentinel Dashboard 的簇点链路 QPS、网关的 429 响应数量、业务服务的实际请求量。

第一次压测的结果就不对:网关返回了 429,但业务服务的请求量完全不降。查了半天发现是规则配错了层级——我配的是routeId维度的规则,但网关的routeId用的是注册中心的服务名,中间有一层大小写转换。改成 API 分组之后正常。

第二次是熔断不生效。慢调用模型要求熔断规则里count的量和实际 RT 匹配,我当时设了 450ms,但压测的接口本身 P99 就只有 200ms,压根没有慢调用样本,自然不会熔断。这种"规则没错但不生效"的情况最难查,因为日志里什么异常都没有。

所以提一个建议:规则上线前,用一个故意 sleep 的 mock 接口把熔断逻辑跑通一遍。我在网关里留了一个/api/mock/slow接口,sleep 800ms,专门用来验证熔断,规则配好先压这个接口,看到 429 了就说明整条链路是通的。

4. 常见问题与排查技巧实录

4.1 规则不生效的排查顺序

规则不生效是最典型的问题,我总结了一套排查顺序,基本能在五分钟内定位。

现象可能原因排查手段
Dashboard 里看不到资源资源未被访问过或未初始化检查eager是否为 true,先手动调一次接口
资源有了但规则不命中资源名与实际 URL 不一致打印exchange.getRequest().getPath(),比对清洗后的路径
规则命中但流量不降规则维度配错(routeId 与 API 分组)确认resourceMode取值,看 Dashboard 的规则列表
重启后规则丢失未配置持久化数据源检查 Nacos 配置项的rule-type是否与规则类型匹配
接口返回 500 而非 429异常处理器排序被抢占给BlockExceptionHandler设最高优先级

这里第三条我踩过两次。resourceMode是 0 的时候,resource字段要填路由的 ID,而这个 ID 在 Spring Cloud Gateway 里默认是服务名,如果你用了StripPrefix之类的过滤器,实际路由 ID 可能和你以为的不一样。最稳的办法是在 Gateway 的配置里显式写id,不要依赖默认值。

4.2 压测时的线程打满问题

有一次压测,网关本身的 CPU 直接跑到 100%,但业务服务的 QPS 只有预估的三成。查下来是接口响应体太大,网关在聚合响应的时候内存吃紧。

这个问题严格说不算限流的锅,但限流模块的观测数据帮我发现了它——Dashboard 上看到的 QPS 曲线和业务侧的 QPS 曲线严重不匹配,中间少了七成的量,说明请求在网关内部就损耗了。

后来做了两件事:一是把大接口的响应字段裁剪,列表接口不返回详情字段;二是把网关的 JVM 堆从 512M 调到 1G。调整之后两条曲线基本重合了。

排查这类问题时,最有用的是"同一时刻三个位置的 QPS":压测工具的发送量、网关入口的接收量、业务服务的处理量。三个数字对不齐,问题就出在对不齐的那一段。

4.3 规则之间的相互覆盖

我遇到过一个很隐蔽的问题:同一个接口既有 QPS 规则又有熔断规则,压测的时候明明是熔断应该先触发,结果一直是限流先挡。

原因是 Sentinel 对同一个资源的规则是按控制器顺序判断的,而且限流规则配的阈值太低,压力还没累积到触发熔断就被限流挡在门外了。熔断判断需要样本,样本被限流挡掉了,永远凑不齐minRequestAmount。

解决办法很直接:熔断规则的minRequestAmount要设得比限流窗口内的正常通过量小。比如限流阈值 1100 QPS、统计窗口 10 秒,那么窗口内最多通过 11000 个请求,minRequestAmount设 20 是合理的。但如果限流阈值只有 5 QPS,窗口内最多 50 个请求,minRequestAmount设 20 就要小心,可能压力刚起来就被限流拦住了。

4.4 规则持久化的坑

Nacos 数据源这块有个坑我要单独说。规则的 JSON 必须是一个完整的数组,不能是增量。你在 Dashboard 上改了一条规则,Sentinel 会推全量规则到 Nacos,覆盖掉原来的配置内容。如果两个人在 Dashboard 上同时改不同规则,后改的会覆盖先改的,这个冲突 Sentinel 不会给你做合并。

我的做法是:Dashboard 只用来做临时调试和观察数据,规则的实际维护走 Git 里的 JSON 文件,通过 Nacos 的控制台或者配置发布接口推上去。这样规则变更有记录、可回溯,也不会互相覆盖。

另外一点,>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询