1. 项目引入与设计思路1.1 网关层限流熔断要解决什么问题我之前维护过一个内部网关下游挂着用户、订单、商品等十多个微服务。平时流量不高大家都过得挺滋润直到一次大促活动来了个瞬时峰值用户服务连接池直接被打满紧接着数据库连接耗尽整个链路开始雪崩一个简单的查询都要卡十几秒。后来把Hystrix换成Resilience4j重新梳理网关层的保护策略才真正稳下来。这次这个项目标题的核心就落在两个词上限流和熔断。限流对应的是入口控速把进入系统的流量控制在下游能承受的范围内熔断对应的是故障阻断当下游服务出现异常时网关不再继续转发请求直接快速失败给下游争取恢复时间。这两个能力放在Spring Cloud Gateway这一层做好处很明显不需要改业务服务的代码策略集中管理无论后面是REST接口、WebSocket还是异步调用都能被同一套保护机制覆盖。在实际落地过程中最优先的选择是把限流做成可配置的把熔断做成可观测的。如果只写死一个RPS100上线后下游扩容了运维还得改代码发版那就麻烦了。所以这篇文章里我会把完整的代码案例、配置文件、压测过程和踩坑记录全部铺开适合正在用Spring Cloud Gateway搭建微服务网关、想在入口层加一层防护的开发者参考。1.2 方案选型为什么是Resilience4j而不是Hystrix或Sentinel很多老项目还在用Hystrix但Hystrix已经停止维护了而且它是基于线程池隔离模型的在Gateway这种Reactor异步链路里适配得比较别扭需要额外的包装。Sentinel功能很强但是引入Sentinel Dashboard之后运维成本和系统复杂度都会上来如果你的团队规模不大没必要为了限流熔断专门再维护一套控制台。Resilience4j走的是轻量级组件路线它是一个个独立的容错组件组合RateLimiter限流器、CircuitBreaker熔断器、Bulkhead隔离、Retry重试、TimeLimiter超时控制。它基于Java 8的CompletableFuture和Reactor的Mono/Flux做了适配能原生嵌入Gateway的过滤器链配置走Spring Boot的application.yml不需要额外搭建服务端这点非常适合中小团队落地。这个项目里我采用的组合方式是限流使用Gateway自带的RequestRateLimiter过滤器工厂底层基于Redis的令牌桶算法熔断使用CircuitBreaker过滤器工厂底层走Resilience4j。这样既保证了限流的高性能又保留了熔断的精细化控制两者的配置都是声明式的后续调整参数不用改代码。2. 工程初始化与依赖引入2.1 版本选型说明先说一下我实测过的版本组合这套组合比较成熟不容易踩坑组件版本Spring Boot2.7.18Spring Cloud2021.0.8Spring Cloud Gateway3.1.x由Spring Cloud BOM管理Resilience4j1.7.1Spring Boot Actuator2.7.18Reactor Netty由Spring Boot管理如果你用的是Spring Boot 3.xGateway升级到4.x整体思路是一样的但部分配置项包名有变化Resilience4j也升级到了2.x接口方法会略有差异。为了保证稳定性我建议新项目可以直接上Spring Boot 3.x老项目迁移则优先看BOM兼容性。版本这里有一个容易忽略的点Spring Cloud Gateway的限流过滤器底层依赖spring-boot-starter-data-redis-reactive如果你忘了加这个依赖项目能正常启动但一旦流量触发限流逻辑就会因为找不到ReactiveRedisTemplate的自动配置而报错而且这个报错往往是在运行期才出现的非常坑。2.2 Maven依赖清单下面是完整的Maven依赖我按用途做了分组说明dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version2021.0.8/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement核心依赖部分dependencies !-- Spring Cloud Gateway -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency !-- Resilience4j 熔断核心 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-circuitbreaker-reactor-resilience4j/artifactId /dependency !-- Resilience4j Reactor适配 -- dependency groupIdio.github.resilience4j/groupId artifactIdresilience4j-reactor/artifactId /dependency !-- Redis Reactive 客户端限流必需的依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependency !-- Actuator用来暴露熔断和限流指标 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency /dependencies这里特别说明一下spring-cloud-starter-circuitbreaker-reactor-resilience4j这个依赖是关键。很多人只加了resilience4j-spring-boot2然后在Gateway里配置CircuitBreaker过滤器时会提示找不到ReactiveResilience4JCircuitBreakerFactory。因为Gateway的过滤器工厂依赖的是Reactor响应式包装类光引入普通版本不够必须引入带reactor标识的这个Starter。3. 限流模块配置与实现细节3.1 限流的底层模型令牌桶与RedisRequestRateLimiter过滤器工厂底层用的是令牌桶算法算法本身不复杂可以想象成一个桶桶里最多放burstCapacity个令牌系统每秒钟往桶里补充replenishRate个令牌每个请求进来要消耗requestedTokens个令牌。有令牌就放行没有令牌就返回429。这里有个常见的认知误区很多人以为replenishRate就是RPS上限其实不是。实际每秒最大通过量是replenishRate / requestedTokens而burstCapacity决定了瞬时突发能到多少但是要注意如果burstCapacity设置得比replenishRate大很多客户端可以在桶满时一次性打进来大量请求瞬间打爆下游。所以这两个参数要配合设计我在3.3小节会详细说计算方式。这个限流流程中Redis负责存储每个Key的令牌桶状态。默认实现的内部流程是网关收到请求后根据KeyResolver计算出限流Key然后去Redis里执行一个Lua脚本脚本会读取当前桶容量判断是否还有足够令牌有则扣减并放行没有则拒绝。整个操作是原子性的所以即使网关部署了多个实例限流效果也是全局限流而不是单机限流。3.2 路由限流配置与key-resolver先实现KeyResolver这是决定限流粒度的地方。我们内部一般首选IP维度限流简单直接package com.example.gateway.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; Configuration public class RateLimiterConfig { Bean public KeyResolver ipKeyResolver() { return new KeyResolver() { Override public MonoString resolve(ServerWebExchange exchange) { String ip exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); return Mono.just(ip); } }; } }如果业务上希望按用户维度限流可以改成从JWT或Header里取用户IDBean public KeyResolver userKeyResolver() { return exchange - { String userId exchange.getRequest().getHeaders().getFirst(X-User-Id); if (userId null) { return Mono.just(anonymous); } return Mono.just(userId); }; }然后在application.yml里配置路由级别的限流参数spring: application: name: gateway-service cloud: gateway: routes: - id: user-service-route uri: lb://user-service predicates: - Path/api/user/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 20 redis-rate-limiter.burstCapacity: 40 redis-rate-limiter.requestedTokens: 1 key-resolver: #{ipKeyResolver}重点提醒key-resolver后面那串#{ipKeyResolver}是SpEL表达式必须确保Bean名称和Bean方法名严格一致。我见过有人把Bean方法命名为ipKeyResolver()结果配置文件里写#{ipKeyResolver}没问题可一旦改了方法名忘了改yaml限流就会退化成所有请求共用一个Key效果完全乱套。3.3 参数计算与业务场景评估参数设计一定要结合真实压测数据不能拍脑袋。拿我们内部一个订单查询接口举例下游接口单机最大吞吐量是50 QPS网关后面挂了4台实例理论集群上限是200 QPS但数据库连接池只有80个连接扣除其他链路的占用实际安全水位只有60 QPS。那么限流参数怎么定我建议这样计算replenishRate设置成安全水位的80%也就是48留出20%的缓冲burstCapacity设置成replenishRate的2倍到3倍允许短时间突发但突发容量太大反而危险。比如replenishRate50, burstCapacity100表示正常情况下每秒放行50个请求桶最多存100个令牌一旦桶被填满极端情况下可以瞬时放行100个请求。实际业务中不同接口的权重不一样网关层不适合一股脑全用同样的参数。我会在后端服务的OpenAPI文档里标注每个接口的建议QPS上限然后路由上按路径拆分限流配置比如登录接口限得严一些查询接口相对宽松写操作一律从严。这样做的原因也很简单网关限流的本质不单是保护网关自身更是保护下游最脆弱的那个环节比如数据库、第三方接口或者慢SQL。4. 熔断模块CircuitBreaker完整落地4.1 CircuitBreaker配置拆解限流解决的是入口过载问题熔断解决的是下游故障问题。当某个下游服务连续失败率达到阈值网关对该服务的调用就会快速失败不再继续往上游发流量让下游有时间恢复。Resilience4j的熔断器有三个状态CLOSED关闭正常访问、OPEN开启直接拒绝、HALF_OPEN半开放少量试探请求。状态切换规则是关闭状态下累计失败率超过阈值熔断器打开打开状态持续一段时间后转为半开半开状态下放行少量请求如果成功则关闭如果失败则重新打开。下面是我用过的比较典型的一套参数resilience4j: circuitbreaker: instances: userServiceCB: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10逐一拆解这些参数的含义slidingWindowSize滑动窗口大小统计最近10次调用的结果。窗口越大统计越平滑但发现故障越慢。minimumNumberOfCalls触发熔断计算前最少需要调用多少次。这个参数是为了避免样本太少导致误判。permittedNumberOfCallsInHalfOpenState半开状态下允许放行的试探请求数量用来验证下游是否恢复。automaticTransitionFromOpenToHalfOpenEnabled打开状态下是否自动转半开。这个开关开启后只要等待时间到就自动放试探流量否则需要一次额外的调用才能触发状态迁移生产环境建议开启。waitDurationInOpenState熔断器打开状态的持续时间。failureRateThreshold失败率阈值我这里是50%按大部分场景够用但如果你下游有重试逻辑建议调低。这里有一个我踩过的坑slidingWindowSize设得太大比如30加上minimumNumberOfCalls默认是5那么在流量不高的情况下统计窗口长时间凑不满样本故障已经持续半天了熔断器还没触发因为调用次数不够。生产环境流量低的服务一定要把minimumNumberOfCalls降低我后面在6.2小节还会展开。4.2 使用ReactiveResilience4JCircuitBreakerFactory实现熔断Spring Cloud Gateway里实现熔断有两种方式一种是用内置的CircuitBreaker过滤器工厂直接配置在路由上简单省事另一种是写全局过滤器把下游调用包在CircularBreaker.run()里灵活但代码复杂。我建议优先用第一种。- id: user-service-route uri: lb://user-service predicates: - Path/api/user/** filters: - name: CircuitBreaker args: name: userServiceCB fallbackUri: forward:/fallback/user这个配置的含义是userServiceCB熔断器打开后请求会转发到网关本地的/fallback/user接口。因为熔断器是Reactive类型网关会创建一个ReactiveResilience4JCircuitBreakerFactory自动关联resilience4j.circuitbreaker.instances.userServiceCB下的配置。有朋友会问这里的CircuitBreaker过滤器底层到底拦截的是什么执行顺序是这样的Gateway先将请求通过lb://负载均衡找到下游实例地址再让CircuitBreaker过滤器包装一次转发动作返回的Mono里如果出现异常且熔断器状态为OPEN就会直接走fallbackUri。所以这个熔断器统计的是网关转发下游失败的结果包括连接超时、响应异常、5xx等。4.3 回退Controller与生产注意事项有了fallbackUri还需要在网关服务里写一个对应的回退接口否则熔断生效后直接返回404体验很差。我一般这样写package com.example.gateway.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; RestController public class FallbackController { RequestMapping(/fallback/user) public MonoMapString, Object fallbackUser() { MapString, Object result new HashMap(); result.put(code, 503); result.put(message, 用户服务暂时不可用请稍后重试); return Mono.just(result); } }生产环境的回退接口需要注意三点第一回退接口本身不能有重逻辑不能再去查Redis、调数据库否则故障期间回退接口也会被拖垮第二要区分不同路由的回退路径比如/fallback/order和/fallback/user不要所有服务共用同一个第三回退接口返回的状态码最好统一约定方便前端和网关日志做告警。还有一个容易忽视的点熔断器打开之后如果下游服务恢复正常熔断器进入半开状态会放3个试探请求。但这3个请求有个超时时间如果下游接口本身响应在3秒以上而waitDurationInOpenState设置只有5秒可能会反复处于打开状态导致下游刚恢复就被再次熔断。这里建议配合TimeLimiter模块给下游调用单独设置超时时间比如2秒强制快速失败避免线程资源被慢请求占用。5. 完整路由配置与压测验证5.1 一个完整的application.yml长什么样我们内部一个网关节点通常要管好几组路由限流、熔断、全局过滤器交叉在一起配置文件的组织结构很重要。下面是一份完整模板可以直接套用server: port: 8080 spring: application: name: gateway-service redis: host: 127.0.0.1 port: 6379 timeout: 2000ms cloud: gateway: default-filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 50 redis-rate-limiter.burstCapacity: 100 redis-rate-limiter.requestedTokens: 1 key-resolver: #{ipKeyResolver} routes: - id: user-service-route uri: lb://user-service predicates: - Path/api/user/** filters: - name: CircuitBreaker args: name: userServiceCB fallbackUri: forward:/fallback/user - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 20 redis-rate-limiter.burstCapacity: 40 redis-rate-limiter.requestedTokens: 1 key-resolver: #{ipKeyResolver} - id: order-service-route uri: lb://order-service predicates: - Path/api/order/** filters: - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order resilience4j: circuitbreaker: instances: userServiceCB: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10 orderServiceCB: registerHealthIndicator: true slidingWindowSize: 15 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 10s failureRateThreshold: 60 eventConsumerBufferSize: 10 management: endpoints: web: exposure: include: health,gateway,circuitbreakerevents endpoint: health: show-details: always这里我特意用了default-filters加路由级filters的双层结构全局默认限流覆盖所有服务作为保底策略路由级限流按具体业务调整这样即使新增路由时忘了配限流也还有兜底。management.endpoints.web.exposure.include可以暴露熔断事件和健康检查指标故障复盘的时候直接看事件详情不用猜。5.2 压测工装与实测结果配置写好了怎么验证有效我本地压测工具习惯用wrk简单直接一条命令就能跑起来下面是我实测的一个场景记录。先启动一个下游模拟服务只提供一个延迟1秒的接口。Gateway路由指向这个服务限流参数是replenishRate20, burstCapacity40熔断参数是failureRateThreshold50, slidingWindowSize10。压测命令wrk -t4 -c100 -d30s http://localhost:8080/api/user/info4个线程100个并发连接压30秒。结果非常经典前2秒正常返回200随后开始出现429因为令牌桶的40个突发令牌被快速消耗完了剩下来不及补充的请求被限流。同时由于模拟接口固定延迟1秒在100并发下网关转发生成大量超时熔断器统计失败率超过50%第10个调用之后熔断器打开后续请求不再转发到下游直接返回503的自定义回退内容。这组结果说明两件事第一限流参数replenishRate20是有效的我们在压测日志里看到接口的TPS稳定在20附近第二熔断器不是限流触发的是被下游超时触发的两者是独立的保护维度别把限流拦截和熔断快速失败混为一谈。实际生产中的话建议先用jmeter做更精细的阶梯压测比如5分钟之内从50并发逐步加到200并发观察熔断器和限流器的触发顺序这个数据比一次30秒的暴力测试更有参考价值。5.3 通过Actuator观察熔断状态配置了registerHealthIndicator: true之后可以通过Actuator的health端点查看每个熔断器的实时状态。我重构项目时最常用的命令是curl http://localhost:8080/actuator/health返回内容里能看到每个CircuitBreaker的状态、失败率、调用次数等。比如{ status: UP, components: { circuitBreakers: { status: UP, details: { userServiceCB: { status: UP, details: { failureRate: 0.0%, failureRateThreshold: 50.0%, maxNumberOfCalls: 10, numberOfCalls: 0 } } } } } }这个场景里故障链路全是手动压测出来的最直观的验证方法就是看熔断器状态从UP变为CIRCUIT_OPEN。在生产环境我会把/actuator/health接到监控系统的探活任务里只要熔断器打开页面告警立刻触发不用等用户投诉才知道系统出问题。6. 常见问题、坑与调优实录6.1 限流不生效或请求全部429怎么排查这个报错分两类一是限流完全没生效二是所有请求全部被限流。我遇到的大部分情况是KeyResolver的SpEL表达式写错了导致所有请求共用同一个Key一旦某个IP触发了限流其他IP也全被误伤。排查方法很直接打开Gateway的日志看内部打印的RequestRateLimiter过滤器日志如果所有请求的限流Key都相同那基本就是这个问题。另一个隐藏坑是Reactive Redis连接不上。限流器每次请求都触发Lua脚本调用如果Redis不可用默认是放行还是拒绝取决于配置的rate-limiter实现但通常会出现大量50x或者超时异常。实际操作中我会把Redis的超时时间设短一点比如timeout: 2000ms同时在运维层面保证Redis和网关之间的网络延迟在1ms以内限流本身不能成为新的瓶颈。还有一点值得说redis-rate-limiter.replenishRate如果误配成0会直接导致所有请求被限流因为桶里永远不会补充令牌。这种低级错误在测试环境经常出现建议上线前先写一个小脚本用固定QPS连续压10分钟观察429数量和Redis内存曲线确认无误后再发布。6.2 熔断生效但fallback就是没走到这类问题我排查过好几轮最后发现大多是fallbackUri的路径没匹配上。fallbackUri配置的是网关内部的forward地址它要求网关本地确实存在对应的RequestMapping。如果路径对不上熔断器虽然打开了但请求转发到网关自己的404前端照样看到500。还有一个容易混淆的点熔断器的统计对象是一次route转发。如果你的路由链里有RewritePath过滤器实际转发地址已经被重写了CircuitBreaker监控的仍然是整个过滤器链的返回结果所以在统计口径上没问题。但如果你在自定义全局过滤器里又把异常吞掉了比如onErrorResume返回了一个正常响应那熔断器永远统计不到失败自然也不会打开。这里要记住一个原则CircuitBreaker过滤器要放在链路靠前的位置并且后续过滤器不能把异常悄悄吞掉。另外waitDurationInOpenState设置成5s之后如果下游恢复很慢熔断器会反复打开半开打开相当于每5秒冲击一次下游。生产环境的建议是把waitDurationInOpenState放宽到30秒以上让下游有充足的时间重启、清缓存、恢复连接池。不要为了快点恢复设太短否则下游刚站起来又被拍下去。6.3 参数调优建议与个人体会我做了几年网关最大的感触是限流和熔断参数不能一成不变要跟着业务和流量的变化持续调整。以下是我总结的一套调优顺序第一步先调minimumNumberOfCalls。低流量服务调到3到5高流量服务可以调到20以上目的是让熔断器在合理样本下快速反应。第二步调failureRateThreshold。像用户服务这种核心链路调到40%比较合理因为一旦超过这个值说明下游已经有明显问题对非核心服务可以调到70%避免频繁熔断误伤。第三步调slidingWindowSize。窗口越大越能反映真实趋势但也让熔断反应变慢。我会建议从10开始压测后再根据触发时间调整。第四步关注burstCapacity。如果平时流量平稳突发不多burstCapacity可以等于replenishRate不用留太大缓冲如果业务有明显波峰比如秒杀、抢购那就把burstCapacity设为目标峰值的1.5倍再配一个紧急降级预案。参数改动前一定要做一次压测记录基线数据。我在实际项目里会用Jenkins流水线固化一个压测任务每次改限流熔断配置都自动跑一轮压力测试生成前后对比报告再决定是否发布。这个习惯看起来麻烦但能省掉很多上线后半夜被叫醒的精力。从我自己的经验看限流熔断这类能力真正难的不是把代码跑通而是把参数调成一个适合业务体质的方案。做完这个项目之后我最大的体会是先让系统在故障场景下死得好看再逐步调成活着不累。最后再分享一个实用的小技巧如果你在网关层同时挂了多套限流熔断规则建议单独写一个测试Controller专门用来模拟故障比如一个永远返回500的接口、一个固定延迟3秒的接口这样每次改完配置不发版不动代码直接请求这个测试接口就能验证熔断逻辑是否按预期工作。这个小工具在我们团队里几乎成了网关开发的标配。