拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Cloud Gateway限流熔断实战:从令牌桶到基元律动

去年大促前我们做了一次压测结果比线上故障还难看下单接口的响应时间从平时80ms一路拉到3s随后整个订单服务直接失去响应。回头看监控真正的流量峰值其实只有预估容量的六成问题根本不出在容量上——网关层什么都没做所有流量原封不动地灌到了下游数据库连接池先被打满线程池集体阻塞接着是日志刷不出来告警发不出去运维想临时限流都登不上机器。那次复盘之后我把 Spring Cloud Gateway 的限流和熔断写进了团队的发布规范也重新梳理了网关层到底应该承担什么职责。这篇文章就是基于那段时间的实战经验整理的核心覆盖三块Gateway 内置的 RequestRateLimiter 怎么用、为什么滑动窗口看起来高级但未必适合网关全局限流、以及 CircuitBreaker 熔断降级怎么和限流配合。另外我会单独讲一个我总结的“基元律动限流”思路三个步骤就能在紧急情况下快速兜住流量属于实战里很能救命的笨办法。适合正在做微服务网关、被突发流量打疼过、或者准备给 Gateway 加限流还不太确定方案的同学。1. 事故复盘网关层到底该拦什么不该拦什么1.1 网关不只是一个路由转发器很多人理解 Spring Cloud Gateway 就是“把请求转发到对应服务”这种理解不能说错但很危险。路由只是网关最基本的功能它真正的价值在于作为流量的第一道闸门把不该进来的挡掉把可能引发雪崩的流量削平把失败的请求兜住。我在实际架构里给网关分了四层职责安全层鉴权、防爬、IP黑白名单、基本的参数校验。流量控制层限流、熔断、降级、排队。路由转发层根据 Path、Header、Query 等条件把请求路由到下游。可观测层接入日志、traceId、指标采集记录每一个请求的来龙去脉。限流和熔断都属于流量控制层是网关高可用的核心。没有这一层下游服务做得再健壮也扛不住流量尖峰突然袭来。有一个很常见的误解很多团队觉得“我下游服务自己已经做了限流网关不需要重复做”。这话只对了一半。下游做的限流是服务视角的保护每个实例只能看到打到自己的那部分流量而网关做限流是全局视角的保护可以在流量还没进内网之前就把它控制住。举个例子订单服务有三个实例每个实例限流 500 QPS理论上集群能扛 1500 QPS。但网关如果不管客户端重试加上爬虫循环刷可能一次性打进来 3000 QPS——每个实例都超了 1000 QPS下游单机的限流虽然能保护自己不死但用户体验已经是大量报错。网关先限一道下游才能活得舒服。1.2 流量洪峰下的失败链条我复盘那次事故时把失败链条画了出来你们感受一下请求进来 - 网关原样转发 - 订单服务线程池开始处理 - 由于流量过大下单逻辑里的数据库查询和缓存更新全部变慢 - 线程越积越多 - 连接池等待队列堆满 - 新的请求直接拒绝 - 但拒绝后客户端自动重试 - 重试又打进来 - 网关照样转发 - 服务彻底雪崩。这条链条里最致命的不是“服务处理不过来”而是“网关没有主动丢弃”。限流的意义不是让请求都成功而是主动丢弃一部分保证系统整体不塌。我当时在复盘文档里写了一句网关限流不是让业务更好看而是让系统死得慢一点、甚至可以不死。主动牺牲一部分流量换来的是一整个系统的稳定。2. RequestRateLimiter 核心实现令牌桶和 Redis 的配合逻辑2.1 依赖与基础配置Spring Cloud Gateway 自带的限流过滤器是 RequestRateLimiter默认实现基于 Redis 和令牌桶算法。这个方案在分布式环境下不需要自己维护本地计数多个网关实例共享 Redis 里的令牌数据能做到全局精确限流。引入依赖通常是这样dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependency注意一定得引入 reactive 版的 Redis starterGateway 底层是 WebFlux走的是响应式编程模型如果引成普通 spring-boot-starter-data-redis 会出现类型不匹配的坑。然后在 application.yml 里给对应路由加上 RequestRateLimiter 过滤器spring: application: name: gateway-service 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 redis-rate-limiter.requestedTokens: 1这三个参数是核心我逐个说。replenishRate 是令牌桶每秒补充的令牌数也就是长期稳定状态下允许的平均 QPS。burstCapacity 是令牌桶的最大容量表示在短时间内允许的突发流量上限。requestedTokens 是每个请求消耗的令牌数默认 1如果你希望某些接口消耗更多令牌可以配合 KeyResolver 在不同维度做权重区分。把这两个参数配合起来看假设 replenishRate100burstCapacity200那么系统长时间平均 QPS 是 100但允许瞬时 200 个请求同时进来。这种设计很符合实际业务——秒杀刚开始的一瞬间流量会产生尖峰完全按照平均速率限会把一波有效用户挡在外面完全不限制尖峰又会把下游打死所以给一个可控的 burst 空间是合理的。2.2 令牌桶参数怎么拍从推导到压测参数不是拍脑袋定的我是按这几步算的先对下游服务做压测找到单实例的“安全 QPS”我习惯以响应时间拐点为准。比如 order-service 单实例在 200 QPS 时 RT 是 100ms在 350 QPS 时 RT 突然跳到 800ms那安全 QPS 就取 250 左右留一点余量。算出下游集群总安全容量。比如 3 个实例每个 250 QPS总容量就是 750 QPS。网关限流值取总容量的 70% 到 80%也就是 525 到 600 QPS。为什么不留满因为网关和下游之间存在网络开销、序列化开销、以及某些请求重试产生的额外流量留 20% 的余量能有效避免下游吃满。burstCapacity 一般取 replenishRate 的 2 倍。这个值要根据业务场景调整如果是抢购秒杀类2.5 到 3 倍也合理如果是普通 CRUD 接口1.5 倍足够了。burstCapacity 越大下游被瞬时冲击的峰值就越高风险也随之增大所以不要一味调大。再给大家一个我觉得很关键的提示令牌桶的参数不是配置完就不动了。我每个月会拉一次网关的限流拦截数和下游的 RT 监控如果发现拦截比例过高、但下游 RT 还很低说明限流值太保守应该往上涨一点如果拦截比例很低、但下游 RT 已经显著升高就需要往下调。限流参数是活的值要跟随业务流量变化动态调整后面我会说怎么用配置中心做动态更新。2.3 KeyResolver按 IP、按用户还是按接口维度限流默认情况下 RequestRateLimiter 使用 PrincipalNameKeyResolver它从认证信息中提取用户来作为限流维度。大多数项目都是自定义 KeyResolver因为登录用户没认证之前网关就要限流了等到认证完可能已经晚了。自定义一个按 IP 限流的 KeyResolver 很简单Configuration public class RateLimiterConfig { Bean public KeyResolver ipKeyResolver() { return exchange - { ServerHttpRequest request exchange.getRequest(); String ip request.getHeaders().getFirst(X-Forwarded-For); if (ip null || ip.isEmpty()) { ip Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress(); } else { int index ip.indexOf(,); if (index 0) { ip ip.substring(0, index); } } return Mono.just(ip); }; } }注意这段代码里主动取了 X-Forwarded-For 的第一个地址。因为网关前面通常还有一层 Nginx如果直接取 request.getRemoteAddress()拿到的是 Nginx 的内网 IP所有用户都会映射成同一个 IP限流就直接失效了。X-Forwarded-For 是逗号分隔的链路第一个是真实客户端 IP所以取第一个最可靠。按用户维度就是返回 userId按接口维度就是返回请求的 Path或者把 Path 和用户拼接在一起做组合维度比如userId : request.getPath()。这种组合维度能实现“每个用户每秒最多 N 次但每个接口也有独立的总体上限”限流效果更细腻。有两点需要提醒一是 KeyResolver 返回的 key 会直接作为 Redis 里的 key 组合部分如果 key 的基数特别大Redis 内存会有压力。比如按用户限流用户量一百万限流 key 也会涨到百万量级要用好 Redis 的内存淘汰策略。二是当 KeyResolver 返回空 key 时默认会返回 403 还是 429 取决于配置要注意deny-empty-key相关的处理逻辑别让空 key 直接穿透限流。3. 滑动窗口限流的缺陷为什么网关全局限流不推荐用它3.1 “滑动窗口比固定窗口平滑”这句话只说对了一半很多人刚接触限流时会看到一个对比固定窗口在窗口切换的瞬间可能出现两倍流量尖峰滑动窗口因为粒度更细所以平滑。我第一次看到这个观点时也觉得有道理直到我在网关里真正实现了一版滑动窗口才发现事情没那么简单。固定窗口的临界问题确实存在。假设限流 100 QPS窗口是一秒那么 0:00:00.9 到 0:00:00.9 这 0.1 秒内来了 100 个请求0:00:01.0 到 0:00:01.1 又来了 100 个请求两个窗口都没超但连续 0.2 秒内实际打进来了 200 个请求。滑动窗口通过对窗口进行细粒度划分来解决这个问题比如把 1 秒切分成 10 个 100ms 的子窗口然后随时间逐个滑动。但划分子窗口之后每个子窗口有自己的计数整个窗口内流量多了还是少了需要将所有子窗口的计数累加。你会发现滑动窗口虽然把“窗口切换瞬间的突发”平滑掉了但并没有完全消除临界问题只是把突刺的粒度从“秒级”降到了“子窗口级”。如果两个请求恰好落在相邻子窗口的边界两侧依然可能出现瞬间 2 倍流量的突刺。更关键的是滑动窗口对“持续缓慢增长的流量”感知其实是滞后的。因为窗口累加的是最近一段时间内的总量流量从 50 缓慢涨到 90 的过程中窗口统计值一直在变化等到接近阈值时可能已经涌入了大量请求。令牌桶在这方面的表现反而更直观桶里令牌没了就直接拒绝不存在统计滞后的朦胧感。3.2 Redis 实现滑动窗口的代价ZSET 的内存和带宽问题如果你真的打算在 Spring Cloud Gateway 里以 Redis 为存储实现滑动窗口最常见方案是用 ZSET。每个请求到来时往 ZSET 里 add 一个 member 并设置当前时间戳作为 score然后删除窗口起始点之前的旧 member最后统计 ZSET 里的数量。这个方案的问题非常现实第一内存消耗大。每个请求都会在 Redis 里新增一个 member假设限流阈值是 1000 QPS窗口 1 秒那么每秒钟最多会有 1000 个 member 写入 ZSET。假设窗口是 10 秒高峰期 ZSET 里会积累 1 万个 member。一个 member 加上 score 和哈希开销至少几十字节10 秒窗口就有几百 KB 的内存占用。这还只是一个 KeyResolver 维度如果按用户限流、按接口限流各来一套内存消耗直接爆炸。第二写入压力大。每个请求都要执行 ZADD而且为了清理过期 member还要额外执行 ZREMRANGEBYSCORE通常是打包在 Lua 脚本里。Lua 脚本虽然能保证原子性但脚本执行本身也要占用 Redis 的 CPU。在高并发下Redis 的 CPU 会成为新瓶颈。第三原子性和超时矛盾。要保证滑动窗口统计的准确性清理和统计必须放在一个 Lua 脚本里执行。但脚本执行时间长、队列等待长时Reactive 客户端很容易触发超时一旦超时你不知道脚本执行成功没有这会进一步引入复杂度。回到问题本质在网关这种“每个请求都要过一道”的高频路径上你需要的是 O(1) 级别的判断开销而不是 O(窗口内请求数) 的统计开销。令牌桶在 Redis 里只是对几个 key 做 INCR 和 EXPIRE开销恒定可控这就是 Spring Cloud Gateway 默认选择令牌桶而不是滑动窗口的根本原因。我并不是说滑动窗口一无是处。在单机场景、业务模块内部、或者需要精确控制的业务配额里滑动窗口仍然很合适比如控制某个用户每天最多提现三次这种低频、精确的业务限流滑动窗口实现简单且直观。但放到网关这种高并发、低延迟要求极高的核心路径上它并不划算。选型的时候一定要分清楚场景。4. 熔断降级实战CircuitBreaker 过滤器和 Fallback 兜底4.1 为什么要对“调用下游”这一个动作做熔断限流管的是入口流量熔断管的是下游健康状态。网关所有的流量最终要打到下游服务上哪怕限流已经拦掉了 30% 的请求剩下的 70% 依然有可能把下游打死。一旦下游出现故障网关如果继续把请求转发进去就是在往火坑里填柴——下游的服务线程被拖住数据库连接池被耗尽然后故障像瘟疫一样传染到其他依赖。熔断的职责就是在这时候喊停当检测到下游调用失败的比率超过阈值直接打开断路器后续请求不再转发到下游而是快速失败返回给下游喘息修复的时间。我在 Gateway 里用的是官方的 CircuitBreaker 过滤器底层基于 Spring Cloud CircuitBreaker 抽象默认实现是 Resilience4j。它和 Ribbon、Hystrix 这些老代代不同不需要你单独起线程池基于响应式语义处理性能开销小很多。4.2 配置示例从断路器参数到 Fallback 接口先加上依赖dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-circuitbreaker-reactor-resilience4j/artifactId /dependency然后在网关路由中这样配置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 - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/order断路器本身的参数通过 Resilience4j 配置控制resilience4j: circuitbreaker: instances: orderServiceCB: slidingWindowSize: 20 minimumNumberOfCalls: 5 failureRateThreshold: 50 waitDurationInOpenState: 10s permittedNumberOfCallsInHalfOpenState: 3这里每个参数我都解释一下因为很多人抄配置但不理解含义出问题不知道怎么调。slidingWindowSize 表示滑动窗口的大小我这里设置 20意思是只统计最近 20 次调用的结果。minimumNumberOfCalls 是触发熔断的最低调用次数只有窗口内调用次数达到 5 次之后才会根据失败率判断是否熔断。它是为了避免调用次数过少时统计波动太大比如刚上线只来了 2 个请求都失败了并不一定代表下游真的挂了可能只是临时抖动。failureRateThreshold 失败率阈值50 表示最近 20 次调用里如果失败比例超过 50%就打开断路器。waitDurationInOpenState 是断路器打开状态的持续时间10 秒后自动转为半开状态。permittedNumberOfCallsInHalfOpenState 是半开状态下允许探测的请求数设为 3意思是熔断 10 秒后放 3 个探测请求过去如果这 3 个请求成功就认为下游恢复了关闭断路器如果失败继续熔断一段时间。Fallback 转发到的是一个网关内部的接口需要写一个 ControllerRestController RequestMapping(/fallback) public class FallbackController { RequestMapping(/order) public MonoResponseEntityMapString, Object orderFallback() { MapString, Object body new HashMap(); body.put(code, 503); body.put(message, 服务繁忙请稍后重试); return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(body)); } }实际业务里fallback 里除了返回固定文案更推荐做几件事查缓存返回旧数据、返回默认的空列表、或者把请求写入 MQ 做异步补偿。这样用户体验远胜过直接看到 503但也要注意 fallback 本身也要控制开销别 fallback 里又去查数据库结果数据库已经扛不住了。4.3 限流和熔断的协作谁先谁后、怎么编排在过滤器链里我习惯把 RequestRateLimiter 放在 CircuitBreaker 前面。这个顺序是有讲究的先限流把大量过载的流量挡在门外再熔断对已经放进来但下游无法处理的流量做快速失败。如果反过来先熔断后限流熔断器需要处理所有进来的流量包括那些本来可以限掉的恶意流量这会干扰熔断器对下游健康状态的判断。一个合格的网关限流是在“入口”做减法熔断是在“调用下游之前”做闸刀两者不是二选一的关系。实战里我遇到过一个比较典型的场景流量突然涨到平时的 8 倍限流把整体流量压到下游容量的 80%结果下游一个第三方支付接口因为上游专线抖动响应时间从 300ms 涨到 5s调用它的接口失败率迅速升高。这时候如果没有熔断限流放进来的请求都会卡在支付调用上线程一点一点被吃掉。最后就是限流保住了入口流量的总量熔断保住了线程池不被慢调用拖死两个机制缺一不可。每次发布前我建议做一次熔断演练手动在下游服务加一个延迟 10 秒的接口然后从网关发起压测观察熔断器是否按预期打开、fallback 是否返回、熔断恢复后半开状态是否正常工作。这些演练能在故障真正发生前帮你发现配置上的低级问题。5. 基元律动限流三个最简单步骤兜住高并发5.1 什么是基元律动限流聊完成熟组件之后我想说说自己总结的一套思路也就是标题里提到的“基元律动限流”。我给它下的定义是用限流领域最基础的原语组件——信号量、计数器、令牌桶、漏桶——作为“基元”结合流量本身的波峰波谷“律动”特征快速设计出一套可用的限流方案。它不是某个框架的专有名词而是一种应对“紧急情况”的思考方式不依赖复杂组件只要抓住流量节奏用最基础的工具也能把系统保护住。我发现很多团队一到紧急情况就慌然后开始东搜西找想找到某个“终极限流算法”。其实在大促加固、故障应急、临时保护某个热点接口这些场景里你根本不需要引入复杂框架用一个信号量、一段计数代码配合对流量特征的分析就能很快解决问题。基元律动限流的本质就是三个步骤选基元、定律动、接兜底。5.2 三个步骤详解第一步选基元。先确认你的场景适合哪种限流原语。如果你在网关这种分布式入口做全局限流用 Redis 令牌桶也就是 Gateway 的 RequestRateLimiter。如果你只想保护单个实例的某个局部资源用 JVM 内部的信号量 Semaphore 就够了开销最低。如果你需要非常精确地限制“某段时间内绝对不允许超过 N 次”用计数窗口。如果消费速度恒定、你想平滑请求速率用漏桶思路比如通过队列做速率整形。每种基元对应的问题都不一样选错了会走弯路。比如你明明只需要保护单机线程池却引入了 Redis 令牌桶结果 Redis 抖动成了新的风险源这就是基元选错了。第二步定律动。这一步是很多限流方案失效的根源——参数是拍脑袋定的没有跟着流量的“律动”来。所谓律动就是流量在时间轴上的波峰、波谷、周期、毛刺。我实际操作时会做三件事拉最近两周的网关流量监控看每天的波峰出现在几点、波谷是多少、日常波动幅度大概多大。这一步找到“常态律动”。做一次压测或者基于历史大促数据估算业务尖峰能达到多少持续多久找到“峰值律动”。结合下游服务容量定出限流阈值和突发容量。举个例子你发现某接口日常每秒 50 QPS波峰 200 QPS下游安全容量是 500 QPS那么限流阈值可以设 400 QPSburst 容量设 600既不给下游造成压力又给正常波峰留足了空间。这套数字不是猜的是从“律动”里读出来的。第三步接兜底。限流之后请求去哪是大部分方案没想清楚的一步。限流本身不是目的保护系统稳定才是。被限掉的流量必须有一个明确去向常见三种兜底直接返回 429 提示语告诉客户端“请求太频繁”客户端可以退避重试。排队等待比如请求进入内存队列由后台线程限速消费应对削峰场景。降级返回返回缓存数据、默认值保证主流程可用。我强烈建议兜底逻辑必须和限流逻辑一起上线只限流不兜底跟只报警不处理是一样的效果。拿我们那次出事系统举例如果提前想好了兜底方案大促那波用户最多看到“稍后重试”的提示而不是订单服务彻底雪崩。5.3 一个最小可行的基元限流实现这里我给出一个用 Semaphore 实现的最小限流器适合单机场景快速保护某个资源。它不是复杂架构方案但恰好能体现“基元”这个思路Component public class SemaphoreRateLimiter { private final Semaphore semaphore new Semaphore(200); public boolean tryAcquire() { return semaphore.tryAcquire(); } public void release() { semaphore.release(); } }在过滤器里这样配合使用Component public class SemaphoreFilter implements GlobalFilter, Ordered { Autowired private SemaphoreRateLimiter rateLimiter; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (!rateLimiter.tryAcquire()) { exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } return chain.filter(exchange).doFinally(signalType - rateLimiter.release()); } Override public int getOrder() { return -500; } }信号量为 200意思是这个网关实例同时最多处理 200 个请求多余的直接返回 429。这里有两个关键点一是 tryAcquire 要放在 filter 链前面尽量少做耗时操作二是释放动作必须放进 doFinally 里保证请求无论成功失败、是否抛异常都能把信号量还回来否则一段时间后信号量被耗尽所有请求都会变成 429这个坑我见过太多人踩。这段代码虽然简单但在单体应用、临时加固某些热点接口时非常有效。当然生产环境我还是推荐用成熟组件自研限流器在异常处理、监控埋点、动态配置这些方面很难做到全面。基元律动的价值在于让你在紧急情况下能快速思考、快速行动而不是花一下午研究新框架。6. 生产环境实测中的坑与优化6.1 Redis 单点故障会不会连累网关限流Gateway 的 RequestRateLimiter 强依赖 Redis那 Redis 挂了网关会怎么样这是每次分享必被问到的问题。默认情况下如果 Redis 连接不上RequestRateLimiter 会抛出异常从而导致请求失败。这意味着限流组件本身成了可用性隐患——你想保护系统结果保护组件先挂了所有请求全部失败。解决思路有两种。第一种是给 Redis 做高可用部署哨兵或者集群模式这解决的是 Redis 自身故障的问题。第二种是在限流不可用时做到“fail-open”或“fail-closed”的取舍。fail-openRedis 挂了就放行所有请求保证可用性但下游可能被打垮fail-closedRedis 挂了就拒绝所有请求保证下游安全但用户体验极差。我个人的经验是网关限流组件优先保证可用性选择 fail-open。因为如果 Redis 挂了说明系统已经处于异常状态这时候再拒绝所有请求等于主动停服不如放流量过去让下游服务的自我保护机制比如 Sentinel、Resilience4j去承担兜底。当然这个决策要看业务容忍度某些核心交易链路宁可拒绝也不能放水那就选 fail-closed但你必须配套好降级页面和告警。实现 fail-open 不复杂可以自定义一个全局异常处理捕获 RequestRateLimiter 抛出的 Redis 连接异常返回放行响应。或者用 Resilience4j 的 CircuitBreaker 包一层限流调用Redis 故障时自动降级。6.2 限流参数的动态配置别写死在 yml 里限流参数如果写死在 application.yml 里改一次要重新发布一次网关这在线上完全不可接受。流量波峰每天都在变业务活动来了要临时放量这个时候重新发布会发生两件尴尬的事一是发布过程本身要花几分钟流量早就冲垮下游了二是改配置变成高危操作谁也不敢在生产上动。我建议把限流参数放到 Nacos 或 Apollo 配置中心结合 Spring Cloud Bus 做动态刷新。一个简单的做法是在配置中心维护一段配置gateway: rate-limit: replenishRate: 100 burstCapacity: 200然后在代码里用 ConfigurationProperties 绑定这段配置Component ConfigurationProperties(prefix gateway.rate-limit) public class RateLimitProperties { private Integer replenishRate 100; private Integer burstCapacity 200; // getter/setter }再把它动态设置到 RouteDefinition 的过滤器参数里。这里要做的事稍微有点绕核心思路是监听路由刷新事件把 RequestRateLimiter 过滤器的参数替换成最新值。Spring Cloud Gateway 自身提供了 RefreshRoutesEvent你可以在配置变更后发布这个事件网关会重新加载路由以及路由上挂的过滤器参数。动态限流看起来是“进阶功能”但我建议你从一开始就规划好哪怕先不用配置中心也要把限流参数抽成配置变量不要散落在各个路由配置里。6.3 限流和熔断的监控被拦掉的请求也是重要数据限流器工作得正不正常、熔断器有没有频繁打开这些状态必须被监控到否则限流熔断就是黑盒。Gateway 的 RequestRateLimiter 会暴露一些指标结合 Prometheus Grafana 可以做可视化。需要关注的核心指标包括每秒请求总数。被限流拒绝的请求数429 响应数。Redis 命令执行耗时和异常数。各路由的请求成功率和失败率。CircuitBreaker 的状态切换事件。我在 Grafana 里建了三个核心面板第一个是网关全局 QPS 和限流拦截率拦截率要是超过 10%说明业务容量规划可能需要调整第二个是每个下游路由的 RT 和错误率RT 上涨是熔断器打开的前兆第三个是熔断器状态一旦有断路器进入 Open 状态立即告警。日志方面也要配合我习惯在被限流的请求里打一条 warn 日志记录限流维度 key、来源 IP、请求路径、时间戳这样即使没有监控面板也能通过日志快速定位是哪个维度触发了限流。日志要控制量可以用采样记录没必要每个被限流的请求都打否则日志系统先成为瓶颈。还有一个很少人提的坑当你给网关接入监控时压测工具生成的流量也会被统计如果你的压测机 IP 没有做白名单这些压测流量会被限流拦掉影响压测结果。我建议压测前先把压测机 IP 加到单独的 KeyResolver 维度里或者临时放行压测流量。最后再分享两个小技巧第一个技巧压测时不要只看平均响应时间要看 P99 和超时比例。网关限流熔断配置的好不好最直接的验证指标就是压测下 P99 是否平缓。如果 P99 突然抬升说明某个环节开始排队这时候不要急着调大限流值先定位是线程池、连接池还是 Redis 的瓶颈。第二个技巧网关的线程模型要重视。Gateway 响应式编程的线程数是有限的默认事件循环线程数是 CPU 核数的 2 倍。如果在过滤链路里写了阻塞调用比如在 filter 里查数据库、调第三方 API会把事件循环线程卡死整个网关性能直线下降。限流熔断配置再完美也救不了一个在过滤链里做同步阻塞操作的网关。务必记住网关过滤链路里一切耗时操作都要异步化。我写过几年网关最大的感受是限流和熔断并不是把配置抄上去就完事它们是整个系统稳定性工程里最需要结合业务场景持续调优的部分。每次大促、每次故障、每次流量特征的变化都在逼着你重新审视那些参数。如果你能顺着这篇文章的思路结合自己系统的流量律动去调整我相信你线上会比之前安稳很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门