资讯详情

Spring Cloud Gateway限流配置实战:原理、参数与避坑指南

📅 2026/10/24 0:40:38 | 华诺云谱 👁 阅读
Spring Cloud Gateway限流配置实战:原理、参数与避坑指南
网关这个东西平时不吭不响地在流量入口处躺平一旦出事背锅比谁都惨。我印象很深的一次是在模拟项目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个人1burstCapacity桶的最大容量允许的突发请求总量上限缓冲区最多容纳人数1requestedTokens每个请求消耗的令牌数用于让某些请求更“贵”每人进门消耗的票数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用户配了requestedTokens0.5等价于给这部分请求double的配额但这个用法需要重写RedisRateLimiter不建议新手一开始就折腾。3. 从零配置gateway网关限流理论部分讲完下面直接进入实操。我假设你已经有一个可以正常运行的Spring Cloud Gateway项目下面每一步都按生产标准来。3.1 引入正确的依赖限流必须依赖Redis而且必须是响应式Redis客户端。在pom.xml中核心依赖是这样dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-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: - StripPrefix1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 200 redis-rate-limiter.burstCapacity: 400 redis-rate-limiter.requestedTokens: 1 key-resolver: #{ipKeyResolver}这里的几个配置要逐一解释。redis-rate-limiter.replenishRate200允许的稳定速率是每秒200个请求。redis-rate-limiter.burstCapacity400桶里最多积攒400个令牌允许瞬间涌入400个请求。redis-rate-limiter.requestedTokens1每个普通请求消耗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 自定义KeyResolverKeyResolver是限流配置里比较容易迷糊的点。它的作用是决定“当前请求按什么维度来做限流”。默认要做分布式部署时单靠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; } }这里有一个线上环境的重要细节如果网关前面悬挂了云负载均衡器或者Nginxexchange.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如果replenishRate200、burstCapacity400、qps1000那么响应码中应该会有大量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里的限流keyredis-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 MapString, Object getErrorAttributes(ServerRequest request, boolean includeStackTrace) { MapString, 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官方也提供了对应的适配包。我做了一个对比供选型参考维度RequestRateLimiterSentinel网关适配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调太大导致限流形同虚设我有一次给搜索服务配了replenishRate200burstCapacity5000。本意是想让突发流量进来结果因为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字段的变化可以提前观察到某个路由是否频繁被打满。这比等到用户反馈页面打不开再排查要主动得多。在整个配置过程中我觉得最有价值的一点是理解了网关限流不是把自己锁死而是给系统一个优雅的“拒绝”能力——面对超出容量的流量不慌、不崩、有明确的协议反馈等到流量恢复又能自动回到正常状态。这个能力是微服务架构稳定性的底线。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑