网关这个东西,平时不吭不响地在流量入口处躺平,一旦出事,背锅比谁都惨。我印象很深的一次,是在模拟项目X上线前一晚做压测,网关后面接的订单服务直接被打到CPU 100%,接口延迟从50毫秒飙到8秒,紧接着就是雪崩、超时、重试、再把数据库拖垮。当时所有人都在查业务代码,唯独没人怀疑过网关。事后复盘才反应过来:网关层什么都没配,所有流量都是裸奔状态,那一刻我意识到gateway网关限流配置不是锦上添花,而是微服务架构里实打实的保命设施。
后来我花了两天时间,把Spring Cloud Gateway自带的限流能力从原理到配置完完整整捋了一遍,这篇就把实战经验写出来。整体围绕这样几个问题展开:网关限流和业务限流到底怎么分工、RequestRateLimiter底层是怎么限流的、YAML那几行配置每个参数该怎么调、KeyResolver怎么写才能按IP或按用户维度限流,最后再聊聊多实例部署和线上踩过的坑。不管你是刚接触微服务网关的开发者,还是已经在维护生产环境的架构师,这篇文章都应该能给出可直接抄作业的配置思路。
1. 为什么网关层要做限流
刚接微服务那会儿,我也觉得限流嘛,写个接口拦截器,在业务代码里用计数器挡一下不就行了?后来被现实教育了几次之后,才真正理解网关限流的位置感:它和业务限流解决的问题、部署的位置、防护的目标都不一样,两者根本互相替代不了。
1.1 没有限流的网关有多脆弱
先复盘一下那次压测的问题。订单服务本身扛得住日常峰值,但大促流量一上来,入口流量翻了几倍。此时网关没有限流,所有请求不管下游能不能消化,一律透传过去。服务端的线程池被占满之后,后面的请求排队、超时、重试,重试又增加新的流量,形成正反馈恶性循环。最终整个订单链路瘫痪,连健康检查都过不了。
这里的核心问题是:后端服务的容量是确定的,比如数据库连接池30个、线程池200个、下游RPC服务QPS上限5000,但入口流量是不确定的。网关作为所有流量的必经之路,是唯一一个能在流量到达业务代码之前就做出“拒绝”决策的位置。如果这里不做限制,就等于把系统的生死完全交给调用方,这在大流量的生产环境里就是一场豪赌。
所以网关限流的价值不是“限死业务”,而是在流量异常或者超出容量时,先挡住一部分请求,给下游留出喘息空间。
1.2 网关限流和业务限流的定位差异
我之前做过一个对比,整理下来大概是这样的:
| 维度 | 网关限流 | 业务限流 |
|---|---|---|
| 位置 | 流量入口,所有请求必经 | 业务代码内部,特定接口或服务 |
| 粒度 | 粗粒度,按路由、IP、用户等 | 细粒度,按业务场景、SKU、操作类型 |
| 目标 | 保护整个下游服务集群 | 保护单个核心场景或资源 |
| 实现方式 | 网关中间件配置,零侵入 | 业务代码、注解、组件 |
| 典型场景 | 大促防刷、流量洪峰保护 | 秒杀、下单、支付等单点控制 |
实际项目中,两层限流是配合关系。举个例子,秒杀场景里,网关层按IP限流,拦住同一个IP疯狂刷接口;业务层按用户账号和商品维度的令牌数再做一次精准控制,防止超卖。网关限流解决的是“整体流量能不能进来”,业务限流解决的是“进来的流量怎么分配”。
因此就有了一个很关键的架构原则:网关限流必须要配置,且要优先于业务限流。
2. Spring Cloud Gateway限流的核心机制
要配置好gateway网关限流,不能只停留在“照着配几行YAML能用”的层面,因为参数怎么调、出问题了怎么排查,全得靠对底层机制的理解。Spring Cloud Gateway自带了一个叫RequestRateLimiter的过滤器,它默认结合Redis实现了基于令牌桶算法的限流。
2.1 RequestRateLimiter过滤器工作原理
RequestRateLimiter是Gateway提供的内置GatewayFilter之一。它做事情很简单:每个请求过来,先向Redis发起一个请求,按当前路由的key从令牌桶里取令牌,取到了就放行,取不到就直接返回HTTP 429 Too Many Requests。
这个过滤器在路由配置中通过filters段挂载,YAML长这样:
spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 redis-rate-limiter.requestedTokens: 1这里有几个点要弄清楚。第一,RequestRateLimiter只是一个壳,真正干活的bean是RedisRateLimiter。第二,默认情况下,Gateway是按路由ID来隔离限流计数器的,也就是说你配置了几个路由,就有几个独立的令牌桶。第三,限流状态存储在Redis里,所以天然支持分布式,后面单独讲多实例的时候再展开。
另外有一点特别容易忽略:RequestRateLimiter从Redis取令牌走的是Reactive方式,因此网关项目必须引入响应式的Redis依赖,如果用成普通spring-boot-starter-data-redis,启动时会直接报错。
2.2 令牌桶算法是怎么工作的
限流算法有很多种,固定窗口、滑动窗口、漏桶、令牌桶,Spring Cloud Gateway选的是令牌桶。它理解起来很像景区入口的闸机:景区规定每秒钟放行一定数量的游客进入,但门口有一个容量有限的缓冲区,游客可以先在这个缓冲区里等着。
令牌桶的具体逻辑是这样的:系统以固定的速率向桶里补充令牌,比如每秒钟补充10个令牌。每个请求进来,先尝试从桶里拿走一个令牌,拿得到就放行,拿不到就拒绝。桶有一个最大容量,超过容量的令牌会被丢弃,不会无限累积。
用生活化的例子解释:桶就像你家小区门口的临时停车位,物业每10分钟清理出一个空位,但整个临时停车区最多能停20辆车。如果这个时刻恰好有20辆车排队等着,放行一波;如果来车太猛,超过20辆上限,后面的就只能等下一轮。
令牌桶相比固定窗口的优势在于:它可以允许一定的突发流量。固定窗口算法只要当前窗口内请求数超过阈值就一律拒绝,而令牌桶只要桶里还有积攒的令牌,短时间内是可以接受一拨高于平均速率的突发请求的,这对于电商大促时流量短时间飙升的场景很重要。
RedisRateLimiter在实现令牌桶时,是用一段Lua脚本在Redis服务端执行的,整个取令牌逻辑是原子操作,多个网关实例同时操作同一个key不会出现并发竞争问题。这比你自己在业务代码里用AtomicInteger做计数器靠谱得多。
2.3 三个核心参数到底怎么调
RequestRateLimiter的配置中,有三个最核心的参数,关于它们的含义,网上很多文章讲得并不透彻,我用自己的话重新拆一遍。
| 参数 | 含义 | 类比 | 默认值 |
|---|---|---|---|
| replenishRate | 每秒向桶中补充的令牌数,也就是平均允许的持续请求速率 | 每秒放行N个人 | 1 |
| burstCapacity | 桶的最大容量,允许的突发请求总量上限 | 缓冲区最多容纳人数 | 1 |
| requestedTokens | 每个请求消耗的令牌数,用于让某些请求更“贵” | 每人进门消耗的票数 | 1 |
默认情况下三者都是1,意味着每秒最多放行1个请求,突发量也只有1,这显然不能满足实际需要。一般来说,replenishRate应该设置成下游服务能够稳定支撑的QPS值,burstCapacity则设置成replenishRate的2到3倍。
举个例子:下游订单服务经过压测,稳定支撑的QPS是1000,那么yaml里可以配:
redis-rate-limiter.replenishRate: 1000 redis-rate-limiter.burstCapacity: 2500为什么要给2到3倍的突发空间?因为线上流量从来都不是均匀的,往往在秒级内有波峰波谷。比如一个接口平均QPS是500,但某几秒内冲到1500,如果burstCapacity只等于replenishRate,那几秒钟内大部分请求会被误伤。但burstCapacity也不能毫无节制地调大,否则令牌桶会积攒下大量令牌,一旦流量突然集中涌入,后端会直接被打穿。
还有一个容易被忽略的参数requestedTokens。它默认是1,但如果你希望某些高成本接口(比如批量导出、复杂报表查询)消耗更多令牌,可以在路由维度上单独调大。不过实际项目中用得不多,我遇到过一个场景是给VIP用户配了requestedTokens=0.5,等价于给这部分请求double的配额,但这个用法需要重写RedisRateLimiter,不建议新手一开始就折腾。
3. 从零配置gateway网关限流
理论部分讲完,下面直接进入实操。我假设你已经有一个可以正常运行的Spring Cloud Gateway项目,下面每一步都按生产标准来。
3.1 引入正确的依赖
限流必须依赖Redis,而且必须是响应式Redis客户端。在pom.xml中,核心依赖是这样:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>这里要特别强调,一定要使用spring-boot-starter-data-redis-reactive,而不是spring-boot-starter-data-redis。因为Gateway是基于WebFlux构建的,RequestRateLimiter底层的ReactiveRedisTemplate要求非阻塞的Reactive连接。我只引入普通Redis依赖时,项目启动就会报类似“No qualifying bean of type ReactiveRedisTemplate”的错误。如果你是从老项目改造,注意同时移除或忽略掉原先的普通Redis依赖,两个一起引入会引发Bean冲突。
3.2 配置Redis连接
既然限流状态要存Redis,自然得配置连接信息。在application.yml中,和普通Redis配置一样:
spring: data: redis: host: 10.10.10.20 port: 6379 password: yourpassword timeout: 2000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0对于超时时间和连接池配置,多提一嘴。限流过滤器是同步阻塞在请求链路上的,如果Redis出现缓慢或超时,网关的性能也会被拖下水,因此timeout不要设置太长,一般1到2秒,同时连接池的max-active要结合网关实例数和并发量来估算。我见过有人把max-active设成100,生产环境一共就4个网关实例,每个实例Redis连接数轻轻松松破400,直接把Redis的连接数打满,这是典型的过度配置。
3.3 在路由上挂载限流过滤器
配置限流最常见的方式是在路由的filters段中添加RequestRateLimiter。我来写一个完整的例子。
假设网关需要转发所有以/api/order开头的请求到订单服务,并做限流保护:
spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 200 redis-rate-limiter.burstCapacity: 400 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@ipKeyResolver}"这里的几个配置要逐一解释。
- redis-rate-limiter.replenishRate=200:允许的稳定速率是每秒200个请求。
- redis-rate-limiter.burstCapacity=400:桶里最多积攒400个令牌,允许瞬间涌入400个请求。
- redis-rate-limiter.requestedTokens=1:每个普通请求消耗1个令牌。
- key-resolver:指定使用哪个KeyResolver,这里用的是SpEL表达式,指向一个名为ipKeyResolver的Bean,这个Bean必须提前定义好。
需要注意的是,key-resolver在较新的Spring Cloud版本中可以放在args里,也可以在全局配置spring.cloud.gateway.filter.request-rate-limiter.key-resolver下面指定。如果项目里只有一个KeyResolver Bean,其实不需要显式指定;如果有多个,就必须要指定,否则启动会报错。
3.4 自定义KeyResolver
KeyResolver是限流配置里比较容易迷糊的点。它的作用是决定“当前请求按什么维度来做限流”。默认要做分布式部署时,单靠presenter的RemoteAddress是不可靠的,大概率拿到的网关前一层负载均衡的IP,所以自定义KeyResolver是刚需。
最常用的两个维度:按IP限流、按用户限流。先看按IP的写法:
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; import java.net.InetAddress; import java.net.InetSocketAddress; @Configuration public class GatewayRateLimitConfig { @Bean public KeyResolver ipKeyResolver() { return exchange -> { String ip = getClientIp(exchange); return Mono.just(ip); }; } private String getClientIp(ServerWebExchange exchange) { // 优先取X-Forwarded-For里的真实IP,避免被前置负载均衡隐藏 String xff = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); if (xff != null && !xff.isEmpty()) { return xff.split(",")[0].trim(); } InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress(); if (remoteAddress != null) { InetAddress address = remoteAddress.getAddress(); return address != null ? address.getHostAddress() : "unknown"; } return "unknown"; } }这里有一个线上环境的重要细节:如果网关前面悬挂了云负载均衡器或者Nginx,exchange.getRequest().getRemoteAddress()拿到的往往是负载均衡器的IP,而不是用户真实IP。此时限流按IP就变成了按负载均衡器IP,相当于所有用户共用一个限额,配置瞬间失效。所以生产环境里,按IP限流必须解析X-Forwarded-For头。
再来看按用户ID限流的写法。一般网关在鉴权之后,会把用户信息写入请求头,或者直接放到ServerWebExchange的attribute里:
@Bean public KeyResolver userKeyResolver() { return exchange -> { String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id"); if (userId == null || userId.isEmpty()) { return Mono.just("anonymous"); } return Mono.just(userId); }; }还有一个常见需求是按请求路径限流。比如/api/order/search这个接口被爬虫打,但其它接口正常,这时按路径维度隔离就很方便:
@Bean public KeyResolver pathKeyResolver() { return exchange -> Mono.just(exchange.getRequest().getPath().value()); }KeyResolver写好后,注册成Spring Bean即可。如果项目里存在多个KeyResolver类型Bean,并且未通过key-resolver明确指定,Gateway启动时就会报“Multiple KeyResolver beans found”之类的错误。
3.5 验证限流是否生效
配置完以后,怎么证明限流真的工作?我觉得最快的方式是用shell脚本发大量并发请求。
先不启动限流,看看接口正常响应。然后加上限流配置,用curl或者ab工具直接打。以ab为例:
ab -n 1000 -c 50 http://localhost:9000/api/order/list如果replenishRate=200、burstCapacity=400、qps=1000,那么响应码中应该会有大量HTTP 429。用curl单次请求看响应头:
curl -i http://localhost:9000/api/order/list能正常返回时,可以在响应头里看到Gateway加入的限流信息。RedisRateLimiter默认会在响应中附加X-RateLimit-Remaining、X-RateLimit-Burst-Capacity等header。这些响应头可以直观反映当前桶里还剩多少令牌。如果请求过猛被限流,返回的body默认是空的,status是429。
为了更直观,还可以直接观察Redis里的限流key:
redis-cli keys "*request_rate_limiter*" redis-cli mget request_rate_limiter.order-route.tokens request_rate_limiter.order-route.timestamptokens字段就是桶里当前的剩余令牌数。通过对比它随请求数量的变化,基本可以确定限流是否在正常工作。
4. 高级玩法:从能用走向好用
把基础的限流配好之后,只能说达到了“能用”的标准。在生产环境里,我们通常还需要处理路由隔离、限流响应体验、多实例部署一致性问题,以及部分场景下是否需要引入更专业的限流组件。
4.1 不同路由配置不同的限流策略
不同后端服务的容量不一样,全用同一套QPS阈值会误伤。比如订单服务容量大,可以扛1000QPS,但优惠券服务只能扛100QPS,这就需要在路由的filters里分别配置。
订单路由限流:
- id: order-route uri: lb://order-service predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1000 redis-rate-limiter.burstCapacity: 2000优惠券路由限流:
- id: coupon-route uri: lb://coupon-service predicates: - Path=/api/coupon/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200这里要理解一个底层细节:RequestRateLimiter的RedisKey默认是以路由ID为分隔的。Redis里存储的key格式是request_rate_limiter.{路由ID}.tokens。所以两个路由的令牌桶互相独立,互不影响。这个机制给了我们很好的路由隔离能力,但也要注意,一个服务如果拆成多个路由配置,比如按版本拆了v1、v2两条路由,它们各自限流,最终打到后端的总流量是叠加的,别把两条路由的replenishRate分别设成1000,结果后端被打2000的QPS。
4.2 自定义限流返回结果
默认情况下,网关限流后返回的HTTP 429响应body是空的。这对浏览器用户来说体验尚可,但对API调用方来说,往往需要明确知道“我被限流了,多久之后可以重试”。所以生产环境里,我会自定义一下返回结构。
Spring Cloud Gateway提供了DefaultErrorWebExceptionHandler来处理Gateway的异常。我们可以自定义ErrorAttributes,把TooManyRequestsException包装成JSON返回。核心代码如下:
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import java.util.LinkedHashMap; import java.util.Map; @Component public class GatewayErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<>(); Throwable error = getError(request); HttpStatus status = determineHttpStatus(error, request); errorAttributes.put("code", status.value()); errorAttributes.put("message", determineMessage(error, status)); errorAttributes.put("timestamp", System.currentTimeMillis()); return errorAttributes; } private HttpStatus determineHttpStatus(Throwable error, ServerRequest request) { return error instanceof org.springframework.cloud.gateway.support.TooManyRequestsException ? HttpStatus.TOO_MANY_REQUESTS : HttpStatus.INTERNAL_SERVER_ERROR; } private String determineMessage(Throwable error, HttpStatus status) { if (error instanceof org.springframework.cloud.gateway.support.TooManyRequestsException) { return "请求过于频繁,请稍后重试"; } return status.getReasonPhrase(); } }这样做的好处是,调用方拿到一个固定的、结构化的错误码和描述,方便客户端做提示与重试策略。同样在网关层,也可以在GlobalFilter里对响应状态码做进一步处理,比如向响应头写入Retry-After,让调用方知道几秒后再试。
4.3 多实例部署下的限流准确性
Spring Cloud Gateway在生产环境通常不会只部署一个实例,至少是两个起步。此时最关心的问题就是:限流计数是各算各的,还是共享一份?答案是共享。
因为RequestRateLimiter把令牌桶的计数存储在Redis里,所有网关实例操作的是同一批key,所以不管请求被负载均衡到哪个实例,限流的总量始终是准确的。这也是Gateway选择Redis做限流存储的核心原因。
但这里有一个需要注意的坑:KeyResolver如果按IP限流,而网关注入X-Forwarded-For头的解析方式不一致,或不同实例拿到不同的header时,可能导致同一用户在不同实例上被拆分成两个不同的“IP”,限流效果就会打折扣。所以多实例部署时,自定义KeyResolver必须保证维度计算结果一致,尽量不依赖实例本地状态。
还有一个顺带的好处:因为限流状态在Redis,你不必像本地限流那样为每个实例单独配置阈值,replenishRate和burstCapacity直接按“总QPS目标”配置即可,不用除以实例数。
4.4 是否要引入Sentinel这类扩展组件
RedisRateLimiter已经能覆盖大部分场景,但如果有更复杂的限流需求,比如动态规则推送、细粒度指标监控、熔断降级一体,这时候可以考虑在网关上引入Sentinel。Spring Cloud Gateway官方也提供了对应的适配包。
我做了一个对比,供选型参考:
| 维度 | RequestRateLimiter | Sentinel网关适配 | Resilience4j |
|---|---|---|---|
| 配置方式 | YAML、Java | 规则中心配置,支持动态推送 | 代码级配置 |
| 存储 | Redis | 不依赖Redis(默认本地,也可持久化) | 本地状态 |
| 限流算法 | 令牌桶 | 滑动窗口、令牌桶等 | 多种支持 |
| 集群限流 | 支持 | 有专门集群模式 | 不支持,需要自研 |
| 监控面板 | 需要自己接Redis指标 | 有控制台 | 需要第三方 |
| 侵入性 | 低,配置即可 | 中,需要理解规则模型 | 高,代码内嵌 |
| 适合场景 | 大多数微服务网关 | 需要精细流量治理 | 应用级轻量保护 |
以我的实践经验,如果只是要给网关加一层基础兜底限流,直接用内置RequestRateLimiter就够了,配置量小,维护成本低。如果公司已经有完整的监控和配置中心体系,想对网关流量做精细化运营,再上Sentinel不迟。
5. 常见问题排查与避坑实录
配置限流这件事,看起来就是几行YAML,但实际操作过程中踩坑还是很多的。我把遇到过的几个高频问题整理出来,附带排查思路。
5.1 问题一:限流完全没有生效
现象是配置照抄,接口高并发打上去,一个429都没有。
第一排查点:Gateways的路由是否真的命中了。如果请求的Path和predicates不匹配,路由根本不会进入RequestRateLimiter过滤器,自然不限流。可以在idea或启动日志里开启trace日志,观察请求命中了哪个route id。
第二排查点:KeyResolver是否注册成功。如果你的项目里定义了多个KeyResolver Bean,但没有指定key-resolver用哪一个,启动过程可能直接报错。如果没报错但也没生效,查看是否存在多个不同名的Bean被Spring注入到了错误位置。
第三排查点:确认KeyResolver返回的key是否统一。如果按IP解析,但是每次请求拿到的IP都不一样,限流维度就被打散成数千个key,每个key每秒限200,总流量毫无压力。
5.2 问题二:Redis连接异常导致网关启动失败
由于RequestRateLimiter强依赖Redis,网关启动时会创建ReactiveRedisTemplate,如果Redis连接不上,启动日志会报连接异常。这个问题在生产上很致命,因为网关往往是最先启动的基础设施之一。
我的建议是:保证Gateway的Redis和业务Redis隔离,至少用不同的db index,不然限流计数器被业务数据冲掉或者被监控扫描拖慢,对网关都不是好事。另外要对Redis配置合适的超时,确认Lettuce连接池参数没有设置过大的max-active。
5.3 问题三:burstCapacity调太大导致限流形同虚设
我有一次给搜索服务配了replenishRate=200,burstCapacity=5000。本意是想让突发流量进来,结果因为5000的容量太大,桶里长期积攒着大量令牌,搜索服务被压测流量瞬间打满,限流等于没配。
burstCapacity和replenishRate的比例,不应该拍脑袋定,而是按下游服务“能容忍的瞬时突刺量”来定。我的建议是从2倍起步,压测之后再逐步调。如果下游服务本身抗突发能力弱,倍数就调低一点,比如1.5倍。其实最稳的方式是先做压测,找出后端在不出现超时和5xx前提下的最大突发QPS,这个值才是burstCapacity的依据。
5.4 问题四:KeyResolver拿不到真实IP
前面提到了X-Forwarded-For的坑,这里再展开一下。如果你的网关前面还有一层Nginx或者云负载均衡,RemoteAddress永远是那层代理的IP,按IP限流永远只对代理IP限流。
解决方法是自定义KeyResolver,从X-Forwarded-For中取第一个IP。但注意,X-Forwarded-For是可以伪造的,客户端在直连网关时可以直接传一个假的X-Forwarded-For头。因此生产环境里,如果要对IP做严格的限流,信任边界要落在“网关前面的负载均衡是否清洗了该请求头”。一个稳妥的做法是,外层负载均衡统一覆盖X-Forwarded-For头,整理成标准格式,网关只信任这层的处理结果。
5.5 常见问题速查表
顺手整理了一张排查表,遇到问题可以先对着查:
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 限流不生效 | 路由predicates未匹配 | 开启gateway日志,确认route id |
| 限流不生效 | 多个KeyResolver未指定 | 在配置中指定key-resolver |
| 限流不生效 | Key值维度太散或不准 | 检查KeyResolver返回结果是否稳定 |
| 429全部打满 | burstCapacity太小 | 压测后按2到3倍replenishRate调整 |
| 429全部打满 | 网关前面代理IP未归一 | 检查KeyResolver是否有解析XFF逻辑 |
| 启动失败 | 没引reactive redis | 依赖换成spring-boot-starter-data-redis-reactive |
| 启动失败 | Redis连接超时 | 检查网络、密码、Redis服务状态 |
| 大量误伤 | X-Forwarded-For被伪造 | 在负载均衡层统一清洗请求头 |
| 响应体为空 | 默认429返回空body | 自定义ErrorAttributes返回JSON错误体 |
6. 写在最后的几点经验
限流配置配完之后,我建议你在上线前至少做一份压测报告,明确记录三个指标:正常QPS、触发限流的QPS阈值、恢复成功率。这样将来领导问“网关限流到底起没起作用”的时候,你能拿数据说话,而不是只讲道理。
另外一个小技巧:把网关限流的Redis key纳入监控大盘。通过监控request_rate_limiter.*.tokens字段的变化,可以提前观察到某个路由是否频繁被打满。这比等到用户反馈页面打不开再排查,要主动得多。
在整个配置过程中,我觉得最有价值的一点,是理解了网关限流不是把自己锁死,而是给系统一个优雅的“拒绝”能力——面对超出容量的流量,不慌、不崩、有明确的协议反馈,等到流量恢复又能自动回到正常状态。这个能力,是微服务架构稳定性的底线。